Guide to the Secure Configuration of Oracle Linux 8

with profile DISA STIG for Oracle Linux 8
This profile contains configuration checks that align to the DISA STIG for Oracle Linux 8 V1R6.
This guide presents a catalog of security-relevant configuration settings for Oracle Linux 8. It is a rendering of content structured in the eXtensible Configuration Checklist Description Format (XCCDF) in order to support security automation. The SCAP content is is available in the scap-security-guide package which is developed at https://www.open-scap.org/security-policies/scap-security-guide.

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

Profile Information

Profile TitleDISA STIG for Oracle Linux 8
Profile IDxccdf_org.ssgproject.content_profile_stig

CPE Platforms

  • cpe:/o:oracle:linux:8

Revision History

Current version: 0.1.68

  • draft (as of 2023-06-15)

Table of Contents

  1. System Settings
    1. Installing and Maintaining Software
    2. Account and Access Control
    3. System Accounting with auditd
    4. GRUB2 bootloader configuration
    5. Configure Syslog
    6. Network Configuration and Firewalls
    7. File Permissions and Masks
    8. SELinux
  2. Services
    1. Base Services
    2. Application Whitelisting Daemon
    3. FTP Server
    4. Kerberos
    5. Mail Server Software
    6. NFS and RPC
    7. Network Time Protocol
    8. Obsolete Services
    9. Hardware RNG Entropy Gatherer Daemon
    10. SSH Server
    11. System Security Services Daemon
    12. USBGuard daemon
    13. X Window System

Checklist

Group   Guide to the Secure Configuration of Oracle Linux 8   Group contains 106 groups and 407 rules
Group   System Settings   Group contains 79 groups and 349 rules
[ref]   Contains rules that check correct system settings.
Group   Installing and Maintaining Software   Group contains 17 groups and 63 rules
[ref]   The following sections contain information on security-relevant choices during the initial operating system installation process and the setup of software updates.
Group   System and Software Integrity   Group contains 8 groups and 27 rules
[ref]   System and software integrity can be gained by installing antivirus, increasing system encryption strength with FIPS, verifying installed software, enabling SELinux, installing an Intrusion Prevention System, etc. However, installing or enabling integrity checking tools cannot prevent intrusions, but they can detect that an intrusion may have occurred. Requirements for integrity checking may be highly dependent on the environment in which the system will be used. Snapshot-based approaches such as AIDE may induce considerable overhead in the presence of frequent software updates.
Group   Software Integrity Checking   Group contains 1 group and 9 rules
[ref]   Both the AIDE (Advanced Intrusion Detection Environment) software and the RPM package management system provide mechanisms for verifying the integrity of installed software. AIDE uses snapshots of file metadata (such as hashes) and compares these to current system files in order to detect changes.

The RPM package management system can conduct integrity checks by comparing information in its metadata database with files installed on the system.
Group   Verify Integrity with AIDE   Group contains 9 rules
[ref]   AIDE conducts integrity checks by comparing information about files with previously-gathered information. Ideally, the AIDE database is created immediately after initial system configuration, and then again after any software update. AIDE is highly configurable, with further configuration information located in /usr/share/doc/aide-VERSION.

Rule   Install AIDE   [ref]

The aide package can be installed with the following command:
$ sudo yum install aide
Rationale:
The AIDE package must be installed if it is to be available for integrity checking.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_aide_installed
Identifiers and References

References:  BP28(R51), 1, 11, 12, 13, 14, 15, 16, 2, 3, 5, 7, 8, 9, 5.10.1.3, APO01.06, BAI01.06, BAI02.01, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS04.07, DSS05.02, DSS05.03, DSS05.05, DSS05.07, DSS06.02, DSS06.06, CCI-002696, CCI-002699, CCI-001744, 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 4.1, SR 6.2, SR 7.6, 1034, 1288, 1341, 1417, A.11.2.4, A.12.1.2, A.12.2.1, A.12.4.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.14.2.7, A.15.2.1, A.8.2.3, CM-6(a), DE.CM-1, DE.CM-7, PR.DS-1, PR.DS-6, PR.DS-8, PR.IP-1, PR.IP-3, Req-11.5, 11.5.2, SRG-OS-000445-GPOS-00199, OL08-00-010359, SV-252654r880573_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=aide


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

Complexity:low
Disruption:low
Strategy:enable
include install_aide

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

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

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

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

Rule   Build and Test AIDE Database   [ref]

Run the following command to generate a new database:
$ sudo /usr/sbin/aide --init
By default, the database will be written to the file /var/lib/aide/aide.db.new.gz. Storing the database, the configuration file /etc/aide.conf, and the binary /usr/sbin/aide (or hashes of these files), in a secure location (such as on read-only media) provides additional assurance about their integrity. The newly-generated database can be installed as follows:
$ sudo cp /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
To initiate a manual check, run the following command:
$ sudo /usr/sbin/aide --check
If this check produces any unexpected output, investigate.
Rationale:
For AIDE to be effective, an initial database of "known-good" information about files must be captured and it should be able to be verified against the installed files.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_aide_build_database
Identifiers and References

References:  BP28(R51), 1, 11, 12, 13, 14, 15, 16, 2, 3, 5, 7, 8, 9, 5.10.1.3, APO01.06, BAI01.06, BAI02.01, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS04.07, DSS05.02, DSS05.03, DSS05.05, DSS05.07, DSS06.02, DSS06.06, 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 4.1, SR 6.2, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.4.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.14.2.7, A.15.2.1, A.8.2.3, CM-6(a), DE.CM-1, DE.CM-7, PR.DS-1, PR.DS-6, PR.DS-8, PR.IP-1, PR.IP-3, Req-11.5, 11.5.2, SRG-OS-000445-GPOS-00199, OL08-00-010359, SV-252654r880573_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Ensure AIDE is installed
  package:
    name: '{{ item }}'
    state: present
  with_items:
  - aide
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.3
  - DISA-STIG-OL08-00-010359
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_build_database
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Build and Test AIDE Database
  command: /usr/sbin/aide --init
  changed_when: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.3
  - DISA-STIG-OL08-00-010359
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_build_database
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Check whether the stock AIDE Database exists
  stat:
    path: /var/lib/aide/aide.db.new.gz
  register: aide_database_stat
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.3
  - DISA-STIG-OL08-00-010359
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_build_database
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Stage AIDE Database
  copy:
    src: /var/lib/aide/aide.db.new.gz
    dest: /var/lib/aide/aide.db.gz
    backup: true
    remote_src: true
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (aide_database_stat.stat.exists is defined and aide_database_stat.stat.exists)
  tags:
  - CJIS-5.10.1.3
  - DISA-STIG-OL08-00-010359
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_build_database
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

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

/usr/sbin/aide --init
/bin/cp -p /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz

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

Rule   Configure AIDE to Verify the Audit Tools   [ref]

The operating system file integrity tool must be configured to protect the integrity of the audit tools.
Rationale:
Protecting the integrity of the tools used for auditing purposes is a critical step toward ensuring the integrity of audit information. Audit information includes all information (e.g., audit records, audit settings, and audit reports) needed to successfully audit information system activity. Audit tools include but are not limited to vendor-provided and open-source audit tools needed to successfully view and manipulate audit information system activity and records. Audit tools include custom queries and report generators. It is not uncommon for attackers to replace the audit tools or inject code into the existing tools to provide the capability to hide or erase system activity from the audit logs. To address this risk, audit tools must be cryptographically signed to provide the capability to identify when the audit tools have been modified, manipulated, or replaced. An example is a checksum hash of the file or files.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_aide_check_audit_tools
Identifiers and References

References:  CCI-001496, AU-9(3), AU-9(3).1, SRG-OS-000278-GPOS-00108, OL08-00-030650, SV-248810r880559_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Ensure aide is installed
  package:
    name: '{{ item }}'
    state: present
  with_items:
  - aide
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030650
  - NIST-800-53-AU-9(3)
  - NIST-800-53-AU-9(3).1
  - aide_check_audit_tools
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set audit_tools fact
  set_fact:
    audit_tools:
    - /usr/sbin/auditctl
    - /usr/sbin/auditd
    - /usr/sbin/augenrules
    - /usr/sbin/aureport
    - /usr/sbin/ausearch
    - /usr/sbin/autrace
    - /usr/sbin/rsyslogd
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030650
  - NIST-800-53-AU-9(3)
  - NIST-800-53-AU-9(3).1
  - aide_check_audit_tools
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure existing AIDE configuration for audit tools are correct
  lineinfile:
    path: /etc/aide.conf
    regexp: ^{{ item }}\s
    line: '{{ item }} p+i+n+u+g+s+b+acl+xattrs+sha512'
  with_items: '{{ audit_tools }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030650
  - NIST-800-53-AU-9(3)
  - NIST-800-53-AU-9(3).1
  - aide_check_audit_tools
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Configure AIDE to properly protect audit tools
  lineinfile:
    path: /etc/aide.conf
    line: '{{ item }} p+i+n+u+g+s+b+acl+xattrs+sha512'
  with_items: '{{ audit_tools }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030650
  - NIST-800-53-AU-9(3)
  - NIST-800-53-AU-9(3).1
  - aide_check_audit_tools
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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










if grep -i '^.*/usr/sbin/auditctl.*$' /etc/aide.conf; then
sed -i "s#.*/usr/sbin/auditctl.*#/usr/sbin/auditctl p+i+n+u+g+s+b+acl+xattrs+sha512#" /etc/aide.conf
else
echo "/usr/sbin/auditctl p+i+n+u+g+s+b+acl+xattrs+sha512" >> /etc/aide.conf
fi

if grep -i '^.*/usr/sbin/auditd.*$' /etc/aide.conf; then
sed -i "s#.*/usr/sbin/auditd.*#/usr/sbin/auditd p+i+n+u+g+s+b+acl+xattrs+sha512#" /etc/aide.conf
else
echo "/usr/sbin/auditd p+i+n+u+g+s+b+acl+xattrs+sha512" >> /etc/aide.conf
fi

if grep -i '^.*/usr/sbin/ausearch.*$' /etc/aide.conf; then
sed -i "s#.*/usr/sbin/ausearch.*#/usr/sbin/ausearch p+i+n+u+g+s+b+acl+xattrs+sha512#" /etc/aide.conf
else
echo "/usr/sbin/ausearch p+i+n+u+g+s+b+acl+xattrs+sha512" >> /etc/aide.conf
fi

if grep -i '^.*/usr/sbin/aureport.*$' /etc/aide.conf; then
sed -i "s#.*/usr/sbin/aureport.*#/usr/sbin/aureport p+i+n+u+g+s+b+acl+xattrs+sha512#" /etc/aide.conf
else
echo "/usr/sbin/aureport p+i+n+u+g+s+b+acl+xattrs+sha512" >> /etc/aide.conf
fi

if grep -i '^.*/usr/sbin/autrace.*$' /etc/aide.conf; then
sed -i "s#.*/usr/sbin/autrace.*#/usr/sbin/autrace p+i+n+u+g+s+b+acl+xattrs+sha512#" /etc/aide.conf
else
echo "/usr/sbin/autrace p+i+n+u+g+s+b+acl+xattrs+sha512" >> /etc/aide.conf
fi

if grep -i '^.*/usr/sbin/augenrules.*$' /etc/aide.conf; then
sed -i "s#.*/usr/sbin/augenrules.*#/usr/sbin/augenrules p+i+n+u+g+s+b+acl+xattrs+sha512#" /etc/aide.conf
else
echo "/usr/sbin/augenrules p+i+n+u+g+s+b+acl+xattrs+sha512" >> /etc/aide.conf
fi

if grep -i '^.*/usr/sbin/rsyslogd.*$' /etc/aide.conf; then
sed -i "s#.*/usr/sbin/rsyslogd.*#/usr/sbin/rsyslogd p+i+n+u+g+s+b+acl+xattrs+sha512#" /etc/aide.conf
else
echo "/usr/sbin/rsyslogd p+i+n+u+g+s+b+acl+xattrs+sha512" >> /etc/aide.conf
fi

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

Rule   Configure Notification of Post-AIDE Scan Details   [ref]

AIDE should notify appropriate personnel of the details of a scan after the scan has been run. If AIDE has already been configured for periodic execution in /etc/crontab, append the following line to the existing AIDE line:
 | /bin/mail -s "$(hostname) - AIDE Integrity Check" root@localhost
Otherwise, add the following line to /etc/crontab:
05 4 * * * root /usr/sbin/aide --check | /bin/mail -s "$(hostname) - AIDE Integrity Check" root@localhost
AIDE can be executed periodically through other means; this is merely one example.
Rationale:
Unauthorized changes to the baseline configuration could make the system vulnerable to various attacks or allow unauthorized access to the operating system. Changes to operating system configurations can have unintended side effects, some of which may be relevant to security.

Detecting such changes and providing an automated response can help avoid unintended, negative consequences that could ultimately affect the security state of the operating system. The operating system's Information Management Officer (IMO)/Information System Security Officer (ISSO) and System Administrators (SAs) must be notified via email and/or monitoring system trap when there is an unauthorized modification of a configuration item.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_aide_scan_notification
Identifiers and References

References:  BP28(R51), 1, 11, 12, 13, 15, 16, 2, 3, 5, 7, 8, 9, BAI01.06, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS05.02, DSS05.05, DSS05.07, CCI-001744, CCI-002699, CCI-002702, 4.3.4.3.2, 4.3.4.3.3, SR 6.2, SR 7.6, A.12.1.2, A.12.4.1, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.14.2.7, A.15.2.1, CM-6(a), CM-3(5), DE.CM-1, DE.CM-7, PR.IP-1, PR.IP-3, SRG-OS-000363-GPOS-00150, SRG-OS-000446-GPOS-00200, SRG-OS-000447-GPOS-00201, OL08-00-010360, SV-248573r902806_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: XCCDF Value var_aide_scan_notification_email # promote to variable
  set_fact:
    var_aide_scan_notification_email: !!str root@localhost
  tags:
    - always

- name: Ensure AIDE is installed
  package:
    name:
    - aide
    - crontabs
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010360
  - NIST-800-53-CM-3(5)
  - NIST-800-53-CM-6(a)
  - aide_scan_notification
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Configure Notification of Post-AIDE Scan Details
  cron:
    name: run AIDE check
    minute: 5
    hour: 4
    weekday: 0
    user: root
    job: /usr/sbin/aide  --check | /bin/mail -s "$(hostname) - AIDE Integrity Check"
      {{ var_aide_scan_notification_email }}
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010360
  - NIST-800-53-CM-3(5)
  - NIST-800-53-CM-6(a)
  - aide_scan_notification
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

if ! rpm -q --quiet "aide" ; then
    yum install -y "aide"
fi
if ! rpm -q --quiet "crontabs" ; then
    yum install -y "crontabs"
fi
var_aide_scan_notification_email='root@localhost'


CRONTAB=/etc/crontab
CRONDIRS='/etc/cron.d /etc/cron.daily /etc/cron.weekly /etc/cron.monthly'

# NOTE: on some platforms, /etc/crontab may not exist
if [ -f /etc/crontab ]; then
	CRONTAB_EXIST=/etc/crontab
fi

if [ -f /var/spool/cron/root ]; then
	VARSPOOL=/var/spool/cron/root
fi

if ! grep -qR '^.*/usr/sbin/aide\s*\-\-check.*|.*\/bin\/mail\s*-s\s*".*"\s*.*@.*$' $CRONTAB_EXIST $VARSPOOL $CRONDIRS; then
	echo "0 5 * * * root /usr/sbin/aide  --check | /bin/mail -s \"\$(hostname) - AIDE Integrity Check\" $var_aide_scan_notification_email" >> $CRONTAB
fi

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

Rule   Configure AIDE to Verify Access Control Lists (ACLs)   [ref]

By default, the acl option is added to the FIPSR ruleset in AIDE. If using a custom ruleset or the acl option is missing, add acl to the appropriate ruleset. For example, add acl to the following line in /etc/aide.conf:
FIPSR = p+i+n+u+g+s+m+c+acl+selinux+xattrs+sha256
AIDE rules can be configured in multiple ways; this is merely one example that is already configured by default. The remediation provided with this rule adds acl to all rule sets available in /etc/aide.conf
Rationale:
ACLs can provide permissions beyond those permitted through the file mode and must be verified by the file integrity tools.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_aide_verify_acls
Identifiers and References

References:  BP28(R51), 2, 3, APO01.06, BAI03.05, BAI06.01, DSS06.02, CCI-000366, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, A.11.2.4, A.12.2.1, A.12.5.1, A.14.1.2, A.14.1.3, A.14.2.4, SI-7, SI-7(1), CM-6(a), PR.DS-6, PR.DS-8, SRG-OS-000480-GPOS-00227, OL08-00-040310, SV-248897r880561_rule


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

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

aide_conf="/etc/aide.conf"

groups=$(LC_ALL=C grep "^[A-Z][A-Za-z_]*" $aide_conf | grep -v "^ALLXTRAHASHES" | cut -f1 -d '=' | tr -d ' ' | sort -u)

for group in $groups
do
	config=$(grep "^$group\s*=" $aide_conf | cut -f2 -d '=' | tr -d ' ')

	if ! [[ $config = *acl* ]]
	then
		if [[ -z $config ]]
		then
			config="acl"
		else
			config=$config"+acl"
		fi
	fi
	sed -i "s/^$group\s*=.*/$group = $config/g" $aide_conf
done

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

Rule   Configure AIDE to Verify Extended Attributes   [ref]

By default, the xattrs option is added to the FIPSR ruleset in AIDE. If using a custom ruleset or the xattrs option is missing, add xattrs to the appropriate ruleset. For example, add xattrs to the following line in /etc/aide.conf:
FIPSR = p+i+n+u+g+s+m+c+acl+selinux+xattrs+sha256
AIDE rules can be configured in multiple ways; this is merely one example that is already configured by default. The remediation provided with this rule adds xattrs to all rule sets available in /etc/aide.conf
Rationale:
Extended attributes in file systems are used to contain arbitrary data and file metadata with security implications.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_aide_verify_ext_attributes
Identifiers and References

References:  BP28(R51), 2, 3, APO01.06, BAI03.05, BAI06.01, DSS06.02, CCI-000366, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, A.11.2.4, A.12.2.1, A.12.5.1, A.14.1.2, A.14.1.3, A.14.2.4, SI-7, SI-7(1), CM-6(a), PR.DS-6, PR.DS-8, SRG-OS-000480-GPOS-00227, OL08-00-040300, SV-248896r780254_rule


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

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

aide_conf="/etc/aide.conf"

groups=$(LC_ALL=C grep "^[A-Z][A-Za-z_]*" $aide_conf | grep -v "^ALLXTRAHASHES" | cut -f1 -d '=' | tr -d ' ' | sort -u)

for group in $groups
do
	config=$(grep "^$group\s*=" $aide_conf | cut -f2 -d '=' | tr -d ' ')

	if ! [[ $config = *xattrs* ]]
	then
		if [[ -z $config ]]
		then
			config="xattrs"
		else
			config=$config"+xattrs"
		fi
	fi
	sed -i "s/^$group\s*=.*/$group = $config/g" $aide_conf
done

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

Rule   Audit Tools Must Be Group-owned by Root   [ref]

Oracle Linux 8 systems providing tools to interface with audit information will leverage user permissions and roles identifying the user accessing the tools, and the corresponding rights the user enjoys, to make access decisions regarding the access to audit tools. Audit tools include, but are not limited to, vendor-provided and open source audit tools needed to successfully view and manipulate audit information system activity and records. Audit tools include custom queries and report generators. Audit tools must have the correct group owner.
Rationale:
Protecting audit information also includes identifying and protecting the tools used to view and manipulate log data. Therefore, protecting audit tools is necessary to prevent unauthorized operations on audit information.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_audit_tools_group_ownership
Identifiers and References

References:  CCI-001493, CCI-001494, CCI-001495, AU-9, SRG-OS-000256-GPOS-00097, SRG-OS-000257-GPOS-00098, SRG-OS-000258-GPOS-00099, OL08-00-030640, SV-248809r779993_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /sbin/auditctl
  stat:
    path: /sbin/auditctl
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /sbin/auditctl
  file:
    path: /sbin/auditctl
    group: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/aureport
  stat:
    path: /sbin/aureport
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /sbin/aureport
  file:
    path: /sbin/aureport
    group: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/ausearch
  stat:
    path: /sbin/ausearch
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /sbin/ausearch
  file:
    path: /sbin/ausearch
    group: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/autrace
  stat:
    path: /sbin/autrace
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /sbin/autrace
  file:
    path: /sbin/autrace
    group: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/auditd
  stat:
    path: /sbin/auditd
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /sbin/auditd
  file:
    path: /sbin/auditd
    group: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/rsyslogd
  stat:
    path: /sbin/rsyslogd
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /sbin/rsyslogd
  file:
    path: /sbin/rsyslogd
    group: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/augenrules
  stat:
    path: /sbin/augenrules
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /sbin/augenrules
  file:
    path: /sbin/augenrules
    group: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030640
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

chgrp 0 /sbin/auditctl
chgrp 0 /sbin/aureport
chgrp 0 /sbin/ausearch
chgrp 0 /sbin/autrace
chgrp 0 /sbin/auditd
chgrp 0 /sbin/rsyslogd
chgrp 0 /sbin/augenrules

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

Rule   Audit Tools Must Be Owned by Root   [ref]

Oracle Linux 8 systems providing tools to interface with audit information will leverage user permissions and roles identifying the user accessing the tools, and the corresponding rights the user enjoys, to make access decisions regarding the access to audit tools. Audit tools include, but are not limited to, vendor-provided and open source audit tools needed to successfully view and manipulate audit information system activity and records. Audit tools include custom queries and report generators. Audit tools must have the correct owner.
Rationale:
Protecting audit information also includes identifying and protecting the tools used to view and manipulate log data. Therefore, protecting audit tools is necessary to prevent unauthorized operations on audit information.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_audit_tools_ownership
Identifiers and References

References:  CCI-001493, CCI-001494, CCI-001495, AU-9, SRG-OS-000256-GPOS-00097, SRG-OS-000257-GPOS-00098, SRG-OS-000258-GPOS-00099, OL08-00-030630, SV-248808r779990_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /sbin/auditctl
  stat:
    path: /sbin/auditctl
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /sbin/auditctl
  file:
    path: /sbin/auditctl
    owner: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/aureport
  stat:
    path: /sbin/aureport
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /sbin/aureport
  file:
    path: /sbin/aureport
    owner: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/ausearch
  stat:
    path: /sbin/ausearch
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /sbin/ausearch
  file:
    path: /sbin/ausearch
    owner: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/autrace
  stat:
    path: /sbin/autrace
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /sbin/autrace
  file:
    path: /sbin/autrace
    owner: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/auditd
  stat:
    path: /sbin/auditd
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /sbin/auditd
  file:
    path: /sbin/auditd
    owner: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/rsyslogd
  stat:
    path: /sbin/rsyslogd
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /sbin/rsyslogd
  file:
    path: /sbin/rsyslogd
    owner: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/augenrules
  stat:
    path: /sbin/augenrules
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /sbin/augenrules
  file:
    path: /sbin/augenrules
    owner: '0'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030630
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

chown 0 /sbin/auditctl
chown 0 /sbin/aureport
chown 0 /sbin/ausearch
chown 0 /sbin/autrace
chown 0 /sbin/auditd
chown 0 /sbin/rsyslogd
chown 0 /sbin/augenrules

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

Rule   Audit Tools Must Have a Mode of 0755 or Less Permissive   [ref]

Oracle Linux 8 systems providing tools to interface with audit information will leverage user permissions and roles identifying the user accessing the tools, and the corresponding rights the user enjoys, to make access decisions regarding the access to audit tools. Audit tools include, but are not limited to, vendor-provided and open source audit tools needed to successfully view and manipulate audit information system activity and records. Audit tools include custom queries and report generators. Audit tools must have a mode of 0755 or less permissive.
Rationale:
Protecting audit information also includes identifying and protecting the tools used to view and manipulate log data. Therefore, protecting audit tools is necessary to prevent unauthorized operations on audit information.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_audit_tools_permissions
Identifiers and References

References:  CCI-001493, AU-9, SRG-OS-000256-GPOS-00097, SRG-OS-000257-GPOS-00098, SRG-OS-000258-GPOS-00099, OL08-00-030620, SV-248807r779987_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /sbin/auditctl
  stat:
    path: /sbin/auditctl
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-ws,o-wt on /sbin/auditctl
  file:
    path: /sbin/auditctl
    mode: u-s,g-ws,o-wt
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/aureport
  stat:
    path: /sbin/aureport
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-ws,o-wt on /sbin/aureport
  file:
    path: /sbin/aureport
    mode: u-s,g-ws,o-wt
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/ausearch
  stat:
    path: /sbin/ausearch
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-ws,o-wt on /sbin/ausearch
  file:
    path: /sbin/ausearch
    mode: u-s,g-ws,o-wt
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/autrace
  stat:
    path: /sbin/autrace
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-ws,o-wt on /sbin/autrace
  file:
    path: /sbin/autrace
    mode: u-s,g-ws,o-wt
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/auditd
  stat:
    path: /sbin/auditd
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-ws,o-wt on /sbin/auditd
  file:
    path: /sbin/auditd
    mode: u-s,g-ws,o-wt
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/rsyslogd
  stat:
    path: /sbin/rsyslogd
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-ws,o-wt on /sbin/rsyslogd
  file:
    path: /sbin/rsyslogd
    mode: u-s,g-ws,o-wt
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /sbin/augenrules
  stat:
    path: /sbin/augenrules
  register: file_exists
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-ws,o-wt on /sbin/augenrules
  file:
    path: /sbin/augenrules
    mode: u-s,g-ws,o-wt
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030620
  - NIST-800-53-AU-9
  - configure_strategy
  - file_audit_tools_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

chmod u-s,g-ws,o-wt /sbin/auditctl

chmod u-s,g-ws,o-wt /sbin/aureport

chmod u-s,g-ws,o-wt /sbin/ausearch

chmod u-s,g-ws,o-wt /sbin/autrace

chmod u-s,g-ws,o-wt /sbin/auditd

chmod u-s,g-ws,o-wt /sbin/rsyslogd

chmod u-s,g-ws,o-wt /sbin/augenrules

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Federal Information Processing Standard (FIPS)   Group contains 3 rules
[ref]   The Federal Information Processing Standard (FIPS) is a computer security standard which is developed by the U.S. Government and industry working groups to validate the quality of cryptographic modules. The FIPS standard provides four security levels to ensure adequate coverage of different industries, implementation of cryptographic modules, and organizational sizes and requirements.

FIPS 140-2 is the current standard for validating that mechanisms used to access cryptographic modules utilize authentication that meets industry and government requirements. For government systems, this allows Security Levels 1, 2, 3, or 4 for use on Oracle Linux 8.

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

Rule   Enable Dracut FIPS Module   [ref]

To enable FIPS mode, run the following command:
fips-mode-setup --enable
To enable FIPS, the system requires that the fips module is added in dracut configuration. Check if /etc/dracut.conf.d/40-fips.conf contain add_dracutmodules+=" fips "
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.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_enable_dracut_fips_module
Identifiers and References

References:  CCI-000068, CCI-000803, CCI-002450, 1446, CIP-003-8 R4.2, CIP-007-3 R5.1, SC-12(2), SC-12(3), IA-7, SC-13, CM-6(a), SC-12, FCS_RBG_EXT.1, SRG-OS-000478-GPOS-00223, OL08-00-010020, SV-248524r877398_rule


Complexity:medium
Disruption:medium
Reboot:true
Strategy:restrict
- name: Check to see the current status of FIPS mode
  command: /usr/bin/fips-mode-setup --check
  register: is_fips_enabled
  changed_when: false
  failed_when: false
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and not ( lookup("env", "container") == "bwrap-osbuild" ) )
  tags:
  - DISA-STIG-OL08-00-010020
  - 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_dracut_fips_module
  - high_severity
  - medium_complexity
  - medium_disruption
  - reboot_required
  - restrict_strategy

- name: Enable FIPS mode
  command: /usr/bin/fips-mode-setup --enable
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and not ( lookup("env", "container") == "bwrap-osbuild" ) )
  - is_fips_enabled.stdout.find('FIPS mode is enabled.') == -1
  tags:
  - DISA-STIG-OL08-00-010020
  - 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_dracut_fips_module
  - high_severity
  - medium_complexity
  - medium_disruption
  - reboot_required
  - restrict_strategy

- name: Enable Dracut FIPS Module
  lineinfile:
    path: /etc/dracut.conf.d/40-fips.conf
    line: add_dracutmodules+=" fips "
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and not ( lookup("env", "container") == "bwrap-osbuild" ) )
  tags:
  - DISA-STIG-OL08-00-010020
  - 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_dracut_fips_module
  - high_severity
  - medium_complexity
  - medium_disruption
  - reboot_required
  - restrict_strategy

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

fips-mode-setup --enable
FIPS_CONF="/etc/dracut.conf.d/40-fips.conf"
if ! grep "^add_dracutmodules+=\" fips \"" $FIPS_CONF; then
    echo "add_dracutmodules+=\" fips \"" >> $FIPS_CONF
fi

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

Rule   Enable FIPS Mode   [ref]

To enable FIPS mode, run the following command:
fips-mode-setup --enable

The fips-mode-setup command will configure the system in FIPS mode by automatically configuring the following:
  • Setting the kernel FIPS mode flag (/proc/sys/crypto/fips_enabled) to 1
  • Creating /etc/system-fips
  • Setting the system crypto policy in /etc/crypto-policies/config to FIPS
  • Loading the Dracut fips module
Warning:  The system needs to be rebooted for these changes to take effect.
Warning:  This rule DOES NOT CHECK if the components of the operating system are FIPS certified. You can find the list of FIPS certified modules at https://csrc.nist.gov/projects/cryptographic-module-validation-program/validated-modules/search. This rule checks if the system is running in FIPS mode. See the rule description for more information about what it means.
Rationale:
Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data. The operating system must implement cryptographic modules adhering to the higher standards approved by the federal government since this provides assurance they have been tested and validated.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_enable_fips_mode
Identifiers and References

References:  CCI-000068, CCI-000803, CCI-002450, 1446, CIP-003-8 R4.2, CIP-007-3 R5.1, CM-3(6), SC-12(2), SC-12(3), IA-7, SC-13, CM-6(a), SC-12, FCS_COP.1(1), FCS_COP.1(2), FCS_COP.1(3), FCS_COP.1(4), FCS_CKM.1, FCS_CKM.2, FCS_TLSC_EXT.1, FCS_RBG_EXT.1, SRG-OS-000478-GPOS-00223, SRG-OS-000396-GPOS-00176, OL08-00-010020, SV-248524r877398_rule


Complexity:medium
Disruption:medium
Reboot:true
Strategy:restrict
- name: XCCDF Value var_system_crypto_policy # promote to variable
  set_fact:
    var_system_crypto_policy: !!str FIPS
  tags:
    - always

- name: Check to see the current status of FIPS mode
  command: /usr/bin/fips-mode-setup --check
  register: is_fips_enabled
  changed_when: false
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and not ( lookup("env", "container") == "bwrap-osbuild" ) )
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010020
  - NIST-800-53-CM-3(6)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-7
  - NIST-800-53-SC-12
  - NIST-800-53-SC-12(2)
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SC-13
  - enable_fips_mode
  - high_severity
  - medium_complexity
  - medium_disruption
  - reboot_required
  - restrict_strategy

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

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

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

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

var_system_crypto_policy='FIPS'


fips-mode-setup --enable

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

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

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

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

Rule   Set kernel parameter 'crypto.fips_enabled' to 1   [ref]

System running in FIPS mode is indicated by kernel parameter 'crypto.fips_enabled'. This parameter should be set to 1 in FIPS mode. To enable FIPS mode, run the following command:
fips-mode-setup --enable
To enable strict FIPS compliance, the fips=1 kernel option needs to be added to the kernel boot parameters during system installation so key generation is done with FIPS-approved algorithms and continuous monitoring tests in place.
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.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_crypto_fips_enabled
Identifiers and References

References:  CCI-000068, CCI-000803, CCI-000877, CCI-001453, CCI-002418, CCI-002450, CCI-002890, CCI-003123, CIP-003-8 R4.2, CIP-007-3 R5.1, SC-12(2), SC-12(3), IA-7, SC-13, CM-6(a), SC-12, SRG-OS-000033-GPOS-00014, SRG-OS-000125-GPOS-00065, SRG-OS-000250-GPOS-00093, SRG-OS-000393-GPOS-00173, SRG-OS-000394-GPOS-00174, SRG-OS-000396-GPOS-00176, SRG-OS-000423-GPOS-00187, SRG-OS-000478-GPOS-00223, OL08-00-010020, SV-248524r877398_rule

Group   System Cryptographic Policies   Group contains 12 rules
[ref]   Linux has the capability to centrally configure cryptographic polices. The command update-crypto-policies is used to set the policy applicable for the various cryptographic back-ends, such as SSL/TLS libraries. The configured cryptographic policies will be the default policy used by these backends unless the application user configures them otherwise. When the system has been configured to use the centralized cryptographic policies, the administrator is assured that any application that utilizes the supported backends will follow a policy that adheres to the configured profile. Currently the supported backends are:
  • GnuTLS library
  • OpenSSL library
  • NSS library
  • OpenJDK
  • Libkrb5
  • BIND
  • OpenSSH
Applications and languages which rely on any of these backends will follow the system policies as well. Examples are apache httpd, nginx, php, and others.

Rule   Configure BIND to use System Crypto Policy   [ref]

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.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_configure_bind_crypto_policy
Identifiers and References

References:  CIP-003-8 R4.2, CIP-007-3 R5.1, SC-13, SC-12(2), SC-12(3), SRG-OS-000423-GPOS-00187, SRG-OS-000426-GPOS-00190, OL08-00-010020, SV-248524r877398_rule



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

Rule   Configure System Cryptography Policy   [ref]

To configure the system cryptography policy to use ciphers only from the FIPS policy, run the following command:
$ sudo update-crypto-policies --set FIPS
The rule checks if settings for selected crypto policy are configured as expected. Configuration files in the /etc/crypto-policies/back-ends are either symlinks to correct files provided by Crypto-policies package or they are regular files in case crypto policy customizations are applied. Crypto policies may be customized by crypto policy modules, in which case it is delimited from the base policy using a colon.
Warning:  The system needs to be rebooted for these changes to take effect.
Warning:  System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process.
Rationale:
Centralized cryptographic policies simplify applying secure ciphers across an operating system and the applications that run on that operating system. Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_configure_crypto_policy
Identifiers and References

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


Complexity:low
Disruption:low
Strategy:restrict
- name: XCCDF Value var_system_crypto_policy # promote to variable
  set_fact:
    var_system_crypto_policy: !!str FIPS
  tags:
    - always

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

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


var_system_crypto_policy='FIPS'


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

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

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

Rule   Configure GnuTLS library to use DoD-approved TLS Encryption   [ref]

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. GnuTLS is supported by system crypto policy, but the GnuTLS configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that /etc/crypto-policies/back-ends/gnutls.config contains the following line and is not commented out: +VERS-ALL:-VERS-DTLS0.9:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS1.0
Rationale:
Overriding the system crypto policy makes the behavior of the GnuTLS library violate expectations, and makes system configuration more fragmented.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configure_gnutls_tls_crypto_policy
Identifiers and References

References:  CCI-001453, AC-17(2), SRG-OS-000250-GPOS-00093, SRG-OS-000423-GPOS-00187, OL08-00-010295, SV-248566r877394_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: set_fact'
  set_fact:
    path: /etc/crypto-policies/back-ends/gnutls.config
    correct_value: +VERS-ALL:-VERS-DTLS0.9:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS1.0
    lineinfile_reg: \+VERS-ALL:-VERS-DTLS0\.9:-VERS-SSL3\.0:-VERS-TLS1\.0:-VERS-TLS1\.1:-VERS-DTLS1\.0
  tags:
  - DISA-STIG-OL08-00-010295
  - NIST-800-53-AC-17(2)
  - configure_gnutls_tls_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: stat'
  stat:
    path: '{{ path }}'
    follow: true
  register: gnutls_file
  tags:
  - DISA-STIG-OL08-00-010295
  - NIST-800-53-AC-17(2)
  - configure_gnutls_tls_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: Add'
  lineinfile:
    path: '{{ path }}'
    regexp: '{{ lineinfile_reg }}'
    line: '{{ correct_value }}'
    create: true
  when: not gnutls_file.stat.exists or gnutls_file.stat.size <= correct_value|length
  tags:
  - DISA-STIG-OL08-00-010295
  - NIST-800-53-AC-17(2)
  - configure_gnutls_tls_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Configure GnuTLS library to use DoD-approved TLS Encryption
  block:

  - name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: Existing value
      check'
    lineinfile:
      path: '{{ path }}'
      create: false
      regexp: '{{ lineinfile_reg }}'
      state: absent
    check_mode: true
    changed_when: false
    register: gnutls

  - name: 'Configure GnuTLS library to use DoD-approved TLS Encryption: Update'
    replace:
      path: '{{ path }}'
      regexp: (\+VERS-ALL(?::-VERS-[A-Z]+\d\.\d)+)
      replace: '{{ correct_value }}'
    when: gnutls.found is defined and gnutls.found != 1
  when: gnutls_file.stat.exists and gnutls_file.stat.size > correct_value|length
  tags:
  - DISA-STIG-OL08-00-010295
  - NIST-800-53-AC-17(2)
  - configure_gnutls_tls_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

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

CONF_FILE=/etc/crypto-policies/back-ends/gnutls.config
correct_value='+VERS-ALL:-VERS-DTLS0.9:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS1.0'

grep -q ${correct_value} ${CONF_FILE}

if [[ $? -ne 0 ]]; then
    # We need to get the existing value, using PCRE to maintain same regex
    existing_value=$(grep -Po '(\+VERS-ALL(?::-VERS-[A-Z]+\d\.\d)+)' ${CONF_FILE})

    if [[ ! -z ${existing_value} ]]; then
        # replace existing_value with correct_value
        sed -i "s/${existing_value}/${correct_value}/g" ${CONF_FILE}
    else
        # ***NOTE*** #
        # This probably means this file is not here or it's been modified
        # unintentionally.
        # ********** #
        # echo correct_value to end
        echo ${correct_value} >> ${CONF_FILE}
    fi
fi

Rule   Configure Kerberos to use System Crypto Policy   [ref]

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.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_configure_kerberos_crypto_policy
Identifiers and References

References:  0418, 1055, 1402, CIP-003-8 R4.2, CIP-007-3 R5.1, SC-13, SC-12(2), SC-12(3), SRG-OS-000120-GPOS-00061, OL08-00-010020, SV-248524r877398_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:configure
- 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:
  - DISA-STIG-OL08-00-010020
  - NIST-800-53-SC-12(2)
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SC-13
  - configure_kerberos_crypto_policy
  - configure_strategy
  - high_severity
  - low_complexity
  - low_disruption
  - reboot_required

Complexity:low
Disruption:low
Reboot:true
Strategy:configure

rm -f /etc/krb5.conf.d/crypto-policies
ln -s /etc/crypto-policies/back-ends/krb5.config /etc/krb5.conf.d/crypto-policies

Rule   Configure Libreswan to use System Crypto Policy   [ref]

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.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_configure_libreswan_crypto_policy
Identifiers and References

References:  CIP-003-8 R4.2, CIP-007-3 R5.1, CM-6(a), MA-4(6), SC-13, SC-12(2), SC-12(3), FCS_IPSEC_EXT.1.4, FCS_IPSEC_EXT.1.6, Req-2.2, 2.2, SRG-OS-000033-GPOS-00014, OL08-00-010020, SV-248524r877398_rule


Complexity:low
Disruption:low
Strategy:restrict
- 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:
  - DISA-STIG-OL08-00-010020
  - NIST-800-53-CM-6(a)
  - NIST-800-53-MA-4(6)
  - NIST-800-53-SC-12(2)
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SC-13
  - PCI-DSS-Req-2.2
  - PCI-DSSv4-2.2
  - configure_libreswan_crypto_policy
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - restrict_strategy


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
        # the file might not end with a new line
        echo -e '\ninclude /etc/crypto-policies/back-ends/libreswan.config' >> "$CONFIG_FILE"
    fi
    return 0
}

remediate_libreswan_crypto_policy

Rule   Configure OpenSSL library to use System Crypto Policy   [ref]

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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configure_openssl_crypto_policy
Identifiers and References

References:  CCI-001453, CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1, AC-17(a), AC-17(2), CM-6(a), MA-4(6), SC-13, SC-12(2), SC-12(3), Req-2.2, 2.2, SRG-OS-000250-GPOS-00093, OL08-00-010293, SV-248564r877394_rule


Complexity:low
Disruption:medium
- name: Test for crypto_policy group
  command: grep '^\s*\[\s*crypto_policy\s*]' /etc/pki/tls/openssl.cnf
  register: test_crypto_policy_group
  failed_when: test_crypto_policy_group.rc not in [0, 1]
  changed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010293
  - NIST-800-53-AC-17(2)
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-MA-4(6)
  - NIST-800-53-SC-12(2)
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SC-13
  - PCI-DSS-Req-2.2
  - PCI-DSSv4-2.2
  - 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:
  - DISA-STIG-OL08-00-010293
  - NIST-800-53-AC-17(2)
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-MA-4(6)
  - NIST-800-53-SC-12(2)
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SC-13
  - PCI-DSS-Req-2.2
  - PCI-DSSv4-2.2
  - 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:
  - DISA-STIG-OL08-00-010293
  - NIST-800-53-AC-17(2)
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-MA-4(6)
  - NIST-800-53-SC-12(2)
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SC-13
  - PCI-DSS-Req-2.2
  - PCI-DSSv4-2.2
  - configure_openssl_crypto_policy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy


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*(?:=\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

Rule   Configure OpenSSL library to use TLS Encryption   [ref]

Crypto Policies are means of enforcing certain cryptographic settings for selected applications including OpenSSL. OpenSSL is by default configured to modify its configuration based on currently configured Crypto Policy. Editing the Crypto Policy back-end is not recommended. Check the crypto-policies(7) man page and choose a policy that configures TLS protocol to version 1.2 or higher, for example DEFAULT, FUTURE or FIPS policy. Or create and apply a custom policy that restricts minimum TLS version to 1.2. For example for versions prior to crypto-policies-20210617-1.gitc776d3e.el8.noarch this is expected:
$ sudo grep -i MinProtocol /etc/crypto-policies/back-ends/opensslcnf.config

MinProtocol = TLSv1.2
Or for version crypto-policies-20210617-1.gitc776d3e.el8.noarch and newer this is expected:
$ sudo grep -i MinProtocol /etc/crypto-policies/back-ends/opensslcnf.config

TLS.MinProtocol = TLSv1.2
DTLS.MinProtocol = DTLSv1.2
Warning:  This rule doesn't come with a remediation, automatically changing the crypto-policies may be too disruptive. Ensure the variable xccdf_org.ssgproject.content_value_var_system_crypto_policy is set to a Crypto Policy that satisfies OpenSSL minimum TLS protocol version 1.2. Custom policies may be applied too.
Rationale:
Without cryptographic integrity protections, information can be altered by unauthorized users without detection.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configure_openssl_tls_crypto_policy
Identifiers and References

References:  CCI-001453, AC-17(2), SRG-OS-000125-GPOS-00065, SRG-OS-000250-GPOS-00093, SRG-OS-000393-GPOS-00173, SRG-OS-000394-GPOS-00174, OL08-00-010294, SV-248565r877394_rule

Rule   Configure SSH to use System Crypto Policy   [ref]

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. SSH is supported by crypto policy, but the SSH configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that the CRYPTO_POLICY variable is either commented or not set at all in the /etc/sysconfig/sshd.
Rationale:
Overriding the system crypto policy makes the behavior of the SSH service violate expectations, and makes system configuration more fragmented.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configure_ssh_crypto_policy
Identifiers and References

References:  CCI-001453, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.312(e)(1), 164.312(e)(2)(ii), CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1, AC-17(a), AC-17(2), CM-6(a), MA-4(6), SC-13, FCS_SSH_EXT.1, FCS_SSHS_EXT.1, FCS_SSHC_EXT.1, Req-2.2, 2.2, SRG-OS-000250-GPOS-00093, OL08-00-010287, SV-248560r877394_rule


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


SSH_CONF="/etc/sysconfig/sshd"

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

Rule   Configure SSH Client to Use FIPS 140-2 Validated Ciphers: openssh.config   [ref]

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. OpenSSH is supported by system crypto policy, but the OpenSSH configuration may be set up incorrectly. To check that Crypto Policies settings for ciphers are configured correctly, ensure that /etc/crypto-policies/back-ends/openssh.config contains the following line and is not commented out:
Ciphers aes256-ctr,aes192-ctr,aes128-ctr
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:
Overriding the system crypto policy makes the behavior of the OpenSSH client violate expectations, and makes system configuration more fragmented. By specifying a cipher list with the order of ciphers being in a “strongest to weakest” orientation, the system will automatically attempt to use the strongest cipher for securing SSH connections.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_harden_sshd_ciphers_openssh_conf_crypto_policy
Identifiers and References

References:  CCI-000068, CCI-000877, CCI-001453, CCI-002418, CCI-002890, CCI-003123, AC-17(2), SRG-OS-000033-GPOS-00014, SRG-OS-000125-GPOS-00065, SRG-OS-000250-GPOS-00093, SRG-OS-000393-GPOS-00173, SRG-OS-000394-GPOS-00174, SRG-OS-000423-GPOS-00187, OL08-00-010020, SV-248524r877398_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: XCCDF Value sshd_approved_ciphers # promote to variable
  set_fact:
    sshd_approved_ciphers: !!str aes256-ctr,aes192-ctr,aes128-ctr
  tags:
    - always

- name: 'Configure SSH Daemon to Use FIPS 140-2 Validated Ciphers: openssh.config'
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/crypto-policies/back-ends/openssh.config
      create: false
      regexp: ^.*Ciphers\s+
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/crypto-policies/back-ends/openssh.config
    lineinfile:
      path: /etc/crypto-policies/back-ends/openssh.config
      create: false
      regexp: ^.*Ciphers\s+
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/crypto-policies/back-ends/openssh.config
    lineinfile:
      path: /etc/crypto-policies/back-ends/openssh.config
      create: true
      regexp: ^.*Ciphers\s+
      line: Ciphers {{ sshd_approved_ciphers }}
      state: present
  tags:
  - DISA-STIG-OL08-00-010020
  - NIST-800-53-AC-17(2)
  - harden_sshd_ciphers_openssh_conf_crypto_policy
  - high_severity
  - low_complexity
  - low_disruption
  - reboot_required
  - restrict_strategy


sshd_approved_ciphers='aes256-ctr,aes192-ctr,aes128-ctr'


if [ -e "/etc/crypto-policies/back-ends/openssh.config" ] ; then
    
    LC_ALL=C sed -i "/^.*Ciphers\s\+/d" "/etc/crypto-policies/back-ends/openssh.config"
else
    touch "/etc/crypto-policies/back-ends/openssh.config"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/crypto-policies/back-ends/openssh.config"

cp "/etc/crypto-policies/back-ends/openssh.config" "/etc/crypto-policies/back-ends/openssh.config.bak"
# Insert at the end of the file
printf '%s\n' "Ciphers ${sshd_approved_ciphers}" >> "/etc/crypto-policies/back-ends/openssh.config"
# Clean up after ourselves.
rm "/etc/crypto-policies/back-ends/openssh.config.bak"

Rule   Configure SSH Server to Use FIPS 140-2 Validated Ciphers: opensshserver.config   [ref]

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. OpenSSH is supported by system crypto policy, but the OpenSSH configuration may be set up incorrectly. To check that Crypto Policies settings for ciphers are configured correctly, ensure that /etc/crypto-policies/back-ends/opensshserver.config contains the following text and is not commented out:
-oCiphers=aes256-ctr,aes192-ctr,aes128-ctr
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:
Overriding the system crypto policy makes the behavior of the OpenSSH server violate expectations, and makes system configuration more fragmented. By specifying a cipher list with the order of ciphers being in a “strongest to weakest” orientation, the system will automatically attempt to use the strongest cipher for securing SSH connections.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_harden_sshd_ciphers_opensshserver_conf_crypto_policy
Identifiers and References

References:  CCI-000877, CCI-001453, AC-17(2), SRG-OS-000125-GPOS-00065, SRG-OS-000250-GPOS-00093, OL08-00-010291, SV-248562r877395_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: XCCDF Value sshd_approved_ciphers # promote to variable
  set_fact:
    sshd_approved_ciphers: !!str aes256-ctr,aes192-ctr,aes128-ctr
  tags:
    - always

- name: 'Configure SSH Server to Use FIPS 140-2 Validated Ciphers: opensshserver.config:
    Set facts'
  set_fact:
    path: /etc/crypto-policies/back-ends/opensshserver.config
    correct_value: -oCiphers={{ sshd_approved_ciphers }}
  tags:
  - DISA-STIG-OL08-00-010291
  - NIST-800-53-AC-17(2)
  - harden_sshd_ciphers_opensshserver_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: 'Configure SSH Server to Use FIPS 140-2 Validated Ciphers: opensshserver.config:
    Stat'
  stat:
    path: '{{ path }}'
    follow: true
  register: opensshserver_file
  tags:
  - DISA-STIG-OL08-00-010291
  - NIST-800-53-AC-17(2)
  - harden_sshd_ciphers_opensshserver_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: 'Configure SSH Server to Use FIPS 140-2 Validated Ciphers: opensshserver.config:
    Create'
  lineinfile:
    path: '{{ path }}'
    line: CRYPTO_POLICY='{{ correct_value }}'
    create: true
  when: not opensshserver_file.stat.exists or opensshserver_file.stat.size <= correct_value|length
  tags:
  - DISA-STIG-OL08-00-010291
  - NIST-800-53-AC-17(2)
  - harden_sshd_ciphers_opensshserver_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: 'Configure SSH Server to Use FIPS 140-2 Validated Ciphers: opensshserver.config'
  block:

  - name: Existing value check
    lineinfile:
      path: '{{ path }}'
      create: false
      regexp: '{{ correct_value }}'
      state: absent
    check_mode: true
    changed_when: false
    register: opensshserver

  - name: Update/Correct value
    replace:
      path: '{{ path }}'
      regexp: (-oCiphers=\S+)
      replace: '{{ correct_value }}'
    when: opensshserver.found is defined and opensshserver.found != 1
  when: opensshserver_file.stat.exists and opensshserver_file.stat.size > correct_value|length
  tags:
  - DISA-STIG-OL08-00-010291
  - NIST-800-53-AC-17(2)
  - harden_sshd_ciphers_opensshserver_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy


sshd_approved_ciphers='aes256-ctr,aes192-ctr,aes128-ctr'


CONF_FILE=/etc/crypto-policies/back-ends/opensshserver.config
correct_value="-oCiphers=${sshd_approved_ciphers}"

# Test if file exists
test -f ${CONF_FILE} || touch ${CONF_FILE}

# Ensure CRYPTO_POLICY is not commented out
sed -i 's/#CRYPTO_POLICY=/CRYPTO_POLICY=/' ${CONF_FILE}

grep -q "'${correct_value}'" ${CONF_FILE}

if [[ $? -ne 0 ]]; then
    # We need to get the existing value, using PCRE to maintain same regex
    existing_value=$(grep -Po '(-oCiphers=\S+)' ${CONF_FILE})

    if [[ ! -z ${existing_value} ]]; then
        # replace existing_value with correct_value
        sed -i "s/${existing_value}/${correct_value}/g" ${CONF_FILE}
    else
        # ***NOTE*** #
        # This probably means this file is not here or it's been modified
        # unintentionally.
        # ********** #
        # echo correct_value to end
        echo "CRYPTO_POLICY='${correct_value}'" >> ${CONF_FILE}
    fi
fi

Rule   Configure SSH Client to Use FIPS 140-2 Validated MACs: openssh.config   [ref]

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. OpenSSH is supported by system crypto policy, but the OpenSSH configuration may be set up incorrectly. To check that Crypto Policies settings are configured correctly, ensure that /etc/crypto-policies/back-ends/openssh.config contains the following line and is not commented out: MACs hmac-sha2-512,hmac-sha2-256
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:
Overriding the system crypto policy makes the behavior of the OpenSSH client violate expectations, and makes system configuration more fragmented.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_harden_sshd_macs_openssh_conf_crypto_policy
Identifiers and References

References:  CCI-000877, CCI-001453, AC-17(2), SRG-OS-000125-GPOS-00065, SRG-OS-000250-GPOS-00093, OL08-00-010290, SV-248561r877395_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: XCCDF Value sshd_approved_macs # promote to variable
  set_fact:
    sshd_approved_macs: !!str hmac-sha2-512,hmac-sha2-256
  tags:
    - always

- name: 'Configure SSH Daemon to Use FIPS 140-2 Validated MACs: openssh.config'
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/crypto-policies/back-ends/openssh.config
      create: false
      regexp: ^.*MACs\s+
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/crypto-policies/back-ends/openssh.config
    lineinfile:
      path: /etc/crypto-policies/back-ends/openssh.config
      create: false
      regexp: ^.*MACs\s+
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/crypto-policies/back-ends/openssh.config
    lineinfile:
      path: /etc/crypto-policies/back-ends/openssh.config
      create: true
      regexp: ^.*MACs\s+
      line: MACs {{ sshd_approved_macs }}
      state: present
  tags:
  - DISA-STIG-OL08-00-010290
  - NIST-800-53-AC-17(2)
  - harden_sshd_macs_openssh_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy


sshd_approved_macs='hmac-sha2-512,hmac-sha2-256'


if [ -e "/etc/crypto-policies/back-ends/openssh.config" ] ; then
    
    LC_ALL=C sed -i "/^.*MACs\s\+/d" "/etc/crypto-policies/back-ends/openssh.config"
else
    touch "/etc/crypto-policies/back-ends/openssh.config"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/crypto-policies/back-ends/openssh.config"

cp "/etc/crypto-policies/back-ends/openssh.config" "/etc/crypto-policies/back-ends/openssh.config.bak"
# Insert at the end of the file
printf '%s\n' "MACs ${sshd_approved_macs}" >> "/etc/crypto-policies/back-ends/openssh.config"
# Clean up after ourselves.
rm "/etc/crypto-policies/back-ends/openssh.config.bak"

Rule   Configure SSH Server to Use FIPS 140-2 Validated MACs: opensshserver.config   [ref]

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. OpenSSH is supported by system crypto policy, but the OpenSSH configuration may be set up incorrectly. To check that Crypto Policies settings are configured correctly, ensure that /etc/crypto-policies/back-ends/opensshserver.config contains the following text and is not commented out: -oMACS=hmac-sha2-512,hmac-sha2-256
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:
Overriding the system crypto policy makes the behavior of the OpenSSH server violate expectations, and makes system configuration more fragmented.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_harden_sshd_macs_opensshserver_conf_crypto_policy
Identifiers and References

References:  CCI-000877, CCI-001453, AC-17(2), SRG-OS-000125-GPOS-00065, SRG-OS-000250-GPOS-00093, OL08-00-010290, SV-248561r877395_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: XCCDF Value sshd_approved_macs # promote to variable
  set_fact:
    sshd_approved_macs: !!str hmac-sha2-512,hmac-sha2-256
  tags:
    - always

- name: 'Configure SSH Server to Use FIPS 140-2 Validated MACs: opensshserver.config:
    Set facts'
  set_fact:
    path: /etc/crypto-policies/back-ends/opensshserver.config
    correct_value: -oMACs={{ sshd_approved_macs }}
  tags:
  - DISA-STIG-OL08-00-010290
  - NIST-800-53-AC-17(2)
  - harden_sshd_macs_opensshserver_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: 'Configure SSH Server to Use FIPS 140-2 Validated MACs: opensshserver.config:
    Stat'
  stat:
    path: '{{ path }}'
    follow: true
  register: opensshserver_file
  tags:
  - DISA-STIG-OL08-00-010290
  - NIST-800-53-AC-17(2)
  - harden_sshd_macs_opensshserver_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: 'Configure SSH Server to Use FIPS 140-2 Validated MACs: opensshserver.config:
    Create'
  lineinfile:
    path: '{{ path }}'
    line: CRYPTO_POLICY='{{ correct_value }}'
    create: true
  when: not opensshserver_file.stat.exists or opensshserver_file.stat.size <= correct_value|length
  tags:
  - DISA-STIG-OL08-00-010290
  - NIST-800-53-AC-17(2)
  - harden_sshd_macs_opensshserver_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: 'Configure SSH Server to Use FIPS 140-2 Validated MACs: opensshserver.config'
  block:

  - name: Existing value check
    lineinfile:
      path: '{{ path }}'
      create: false
      regexp: '{{ correct_value }}'
      state: absent
    check_mode: true
    changed_when: false
    register: opensshserver

  - name: Update/Correct value
    replace:
      path: '{{ path }}'
      regexp: (-oMACs=\S+)
      replace: '{{ correct_value }}'
    when: opensshserver.found is defined and opensshserver.found != 1
  when: opensshserver_file.stat.exists and opensshserver_file.stat.size > correct_value|length
  tags:
  - DISA-STIG-OL08-00-010290
  - NIST-800-53-AC-17(2)
  - harden_sshd_macs_opensshserver_conf_crypto_policy
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy


sshd_approved_macs='hmac-sha2-512,hmac-sha2-256'


CONF_FILE=/etc/crypto-policies/back-ends/opensshserver.config
correct_value="-oMACs=${sshd_approved_macs}"

# Test if file exists
test -f ${CONF_FILE} || touch ${CONF_FILE}

# Ensure CRYPTO_POLICY is not commented out
sed -i 's/#CRYPTO_POLICY=/CRYPTO_POLICY=/' ${CONF_FILE}

grep -q "'${correct_value}'" ${CONF_FILE}

if [[ $? -ne 0 ]]; then
    # We need to get the existing value, using PCRE to maintain same regex
    existing_value=$(grep -Po '(-oMACs=\S+)' ${CONF_FILE})

    if [[ ! -z ${existing_value} ]]; then
        # replace existing_value with correct_value
        sed -i "s/${existing_value}/${correct_value}/g" ${CONF_FILE}
    else
        # ***NOTE*** #
        # This probably means this file is not here or it's been modified
        # unintentionally.
        # ********** #
        # echo correct_value to end
        echo "CRYPTO_POLICY='${correct_value}'" >> ${CONF_FILE}
    fi
fi
Group   Operating System Vendor Support and Certification   Group contains 1 rule
[ref]   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.

Rule   The Installed Operating System Is Vendor Supported   [ref]

The installed operating system must be maintained by a vendor. Oracle Linux is supported by Oracle Corporation. As the Oracle Linux vendor, Oracle Corporation 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.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_installed_OS_is_vendor_supported
Identifiers and References

References:  18, 20, 4, APO12.01, APO12.02, APO12.03, APO12.04, BAI03.10, DSS05.01, DSS05.02, CCI-000366, 4.2.3, 4.2.3.12, 4.2.3.7, 4.2.3.9, A.12.6.1, A.14.2.3, A.16.1.3, A.18.2.2, A.18.2.3, CM-6(a), MA-6, SA-13(a), ID.RA-1, PR.IP-12, SRG-OS-000480-GPOS-00227, OL08-00-010000, SV-248521r779129_rule

Group   Endpoint Protection Software   Group contains 2 groups and 2 rules
[ref]   Endpoint protection security software that is not provided or supported by Oracle Corporation can be installed to provide complementary or duplicative security capabilities to those provided by the base platform. Add-on software may not be appropriate for some specialized systems.
Group   McAfee Endpoint Security Software   Group contains 1 group and 2 rules
[ref]   In DoD environments, McAfee Host-based Security System (HBSS) and VirusScan Enterprise for Linux (VSEL) is required to be installed on all systems.
Group   McAfee Endpoint Security for Linux (ENSL)   Group contains 2 rules
[ref]   McAfee Endpoint Security for Linux (ENSL) is a suite of software applications used to monitor, detect, and defend computer networks and systems.

Rule   Install McAfee Endpoint Security for Linux (ENSL)   [ref]

Install McAfee Endpoint Security for Linux antivirus software which is provided for DoD systems and uses signatures to search for the presence of viruses on the filesystem. The McAfeeTP package can be installed with the following command:
$ sudo yum install McAfeeTP
Warning:  Due to McAfee Endpoint Security for Linux (ENSL) being 3rd party software, automated remediation is not available for this configuration check.
Rationale:
Virus scanning software can be used to detect if a system has been compromised by computer viruses, as well as to limit their spread to other systems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_mcafeetp_installed
Identifiers and References

References:  CCI-001263, CCI-000366, SI-2(2), SRG-OS-000191-GPOS-00080, OL08-00-010001, SV-248522r779132_rule

Rule   Ensure McAfee Endpoint Security for Linux (ENSL) is running   [ref]

Install McAfee Endpoint Security for Linux antivirus software which is provided for DoD systems and uses signatures to search for the presence of viruses on the filesystem.
Warning:  Due to McAfee Endpoint Security for Linux (ENSL) being 3rd party software, automated remediation is not available for this configuration check.
Rationale:
Virus scanning software can be used to detect if a system has been compromised by computer viruses, as well as to limit their spread to other systems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_agent_mfetpd_running
Identifiers and References

References:  CCI-001263, CCI-000366, SI-2(2), SRG-OS-000191-GPOS-00080, OL08-00-010001, SV-248522r779132_rule

Group   Disk Partitioning   Group contains 7 rules
[ref]   To ensure separation and protection of data, there are top-level system directories which should be placed on their own physical partition or logical volume. The installer's default partitioning scheme creates separate logical volumes for /, /boot, and swap.
  • If starting with any of the default layouts, check the box to \"Review and modify partitioning.\" This allows for the easy creation of additional logical volumes inside the volume group already created, though it may require making /'s logical volume smaller to create space. In general, using logical volumes is preferable to using partitions because they can be more easily adjusted later.
  • If creating a custom layout, create the partitions mentioned in the previous paragraph (which the installer will require anyway), as well as separate ones described in the following sections.
If a system has already been installed, and the default partitioning scheme was used, it is possible but nontrivial to modify it to create separate logical volumes for the directories listed above. The Logical Volume Manager (LVM) makes this possible. See the LVM HOWTO at http://tldp.org/HOWTO/LVM-HOWTO/ for more detailed information on LVM.

Rule   Encrypt Partitions   [ref]

Oracle Linux 8 natively supports partition encryption through the Linux Unified Key Setup-on-disk-format (LUKS) technology. The easiest way to encrypt a partition is during installation time.

For manual installations, select the Encrypt checkbox during partition creation to encrypt the partition. When this option is selected the system will prompt for a passphrase to use in decrypting the partition. The passphrase will subsequently need to be entered manually every time the system boots.

For automated/unattended installations, it is possible to use Kickstart by adding the --encrypted and --passphrase= options to the definition of each partition to be encrypted. For example, the following line would encrypt the root partition:
part / --fstype=ext4 --size=100 --onpart=hda1 --encrypted --passphrase=PASSPHRASE
Any PASSPHRASE is stored in the Kickstart in plaintext, and the Kickstart must then be protected accordingly. Omitting the --passphrase= option from the partition definition will cause the installer to pause and interactively ask for the passphrase during installation.

By default, the Anaconda installer uses aes-xts-plain64 cipher with a minimum 512 bit key size which should be compatible with FIPS enabled.

Detailed information on encrypting partitions using LUKS or LUKS ciphers can be found on the Oracle Linux 8 Documentation web site:
https://docs.oracle.com/en/operating-systems/oracle-linux/8/install/install-InstallingOracleLinuxManually.html#system-options .
Rationale:
The risk of a system's physical compromise, particularly mobile systems such as laptops, places its data at risk of compromise. Encrypting this data mitigates the risk of its loss if the system is lost.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_encrypt_partitions
Identifiers and References

References:  13, 14, APO01.06, BAI02.01, BAI06.01, DSS04.07, DSS05.03, DSS05.04, DSS05.07, DSS06.02, DSS06.06, 3.13.16, CCI-001199, CCI-002475, CCI-002476, 164.308(a)(1)(ii)(D), 164.308(b)(1), 164.310(d), 164.312(a)(1), 164.312(a)(2)(iii), 164.312(a)(2)(iv), 164.312(b), 164.312(c), 164.314(b)(2)(i), 164.312(d), SR 3.4, SR 4.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R4.2, CIP-007-3 R5.1, CM-6(a), SC-28, SC-28(1), SC-13, AU-9(3), PR.DS-1, PR.DS-5, SRG-OS-000405-GPOS-00184, SRG-OS-000185-GPOS-00079, SRG-OS-000404-GPOS-00183, OL08-00-010030, SV-248525r853748_rule

Rule   Ensure /home Located On Separate Partition   [ref]

If user home directories will be stored locally, create a separate partition for /home at installation time (or migrate it later using LVM). If /home will be mounted from another system such as an NFS server, then creating a separate partition is not necessary at installation time, and the mountpoint can instead be configured later.
Rationale:
Ensuring that /home is mounted on its own partition enables the setting of more restrictive mount options, and also helps ensure that users cannot trivially fill partitions used for log or audit data storage.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_partition_for_home
Identifiers and References

References:  BP28(R12), 12, 15, 8, APO13.01, DSS05.02, CCI-000366, CCI-001208, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.13.1.1, A.13.2.1, A.14.1.3, CM-6(a), SC-5(2), PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-010800, SV-248648r902800_rule


Complexity:low
Disruption:high
Strategy:enable

part /home

Rule   Ensure /tmp Located On Separate Partition   [ref]

The /tmp directory is a world-writable directory used for temporary file storage. Ensure it has its own partition or logical volume at installation time, or migrate it using LVM.
Rationale:
The /tmp partition is used as temporary storage by many programs. Placing /tmp in its own partition enables the setting of more restrictive mount options, which can help protect programs which use it.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_partition_for_tmp
Identifiers and References

References:  BP28(R12), 12, 15, 8, APO13.01, DSS05.02, CCI-000366, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.13.1.1, A.13.2.1, A.14.1.3, CM-6(a), SC-5(2), PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-010543, SV-248611r779399_rule


Complexity:low
Disruption:high
Strategy:enable

part /tmp

Rule   Ensure /var Located On Separate Partition   [ref]

The /var directory is used by daemons and other system services to store frequently-changing data. Ensure that /var has its own partition or logical volume at installation time, or migrate it using LVM.
Rationale:
Ensuring that /var is mounted on its own partition enables the setting of more restrictive mount options. This helps protect system services such as daemons or other programs which use it. It is not uncommon for the /var directory to contain world-writable directories installed by other software packages.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_partition_for_var
Identifiers and References

References:  BP28(R12), 12, 15, 8, APO13.01, DSS05.02, CCI-000366, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.13.1.1, A.13.2.1, A.14.1.3, CM-6(a), SC-5(2), PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-010540, SV-248608r902793_rule


Complexity:low
Disruption:high
Strategy:enable

part /var

Rule   Ensure /var/log Located On Separate Partition   [ref]

System logs are stored in the /var/log directory. Ensure that /var/log has its own partition or logical volume at installation time, or migrate it using LVM.
Rationale:
Placing /var/log in its own partition enables better separation between log files and other files in /var/.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_partition_for_var_log
Identifiers and References

References:  BP28(R12), BP28(R47), 1, 12, 14, 15, 16, 3, 5, 6, 8, APO11.04, APO13.01, BAI03.05, DSS05.02, DSS05.04, DSS05.07, MEA02.01, CCI-000366, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, CIP-007-3 R6.5, CM-6(a), AU-4, SC-5(2), PR.PT-1, PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-010541, SV-248609r902795_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/log

Rule   Ensure /var/log/audit Located On Separate Partition   [ref]

Audit logs are stored in the /var/log/audit directory. Ensure that /var/log/audit has its own partition or logical volume at installation time, or migrate it using LVM. Make absolutely certain that it is large enough to store all audit logs that will be created by the auditing daemon.
Rationale:
Placing /var/log/audit in its own partition enables better separation between audit files and other files, and helps ensure that auditing cannot be halted due to the partition running out of space.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_partition_for_var_log_audit
Identifiers and References

References:  BP28(R43), 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 8, APO11.04, APO13.01, BAI03.05, BAI04.04, DSS05.02, DSS05.04, DSS05.07, MEA02.01, CCI-000366, CCI-001849, 164.312(a)(2)(ii), 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.2, SR 7.6, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.17.2.1, CIP-007-3 R6.5, CM-6(a), AU-4, SC-5(2), PR.DS-4, PR.PT-1, PR.PT-4, FMT_SMF_EXT.1, SRG-OS-000341-GPOS-00132, SRG-OS-000480-GPOS-00227, OL08-00-010542, SV-248610r779396_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/log/audit

Rule   Ensure /var/tmp Located On Separate Partition   [ref]

The /var/tmp directory is a world-writable directory used for temporary file storage. Ensure it has its own partition or logical volume at installation time, or migrate it using LVM.
Rationale:
The /var/tmp partition is used as temporary storage by many programs. Placing /var/tmp in its own partition enables the setting of more restrictive mount options, which can help protect programs which use it.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_partition_for_var_tmp
Identifiers and References

References:  BP28(R12), SRG-OS-000480-GPOS-00227, OL08-00-010544, SV-248612r902797_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/tmp
Group   GNOME Desktop Environment   Group contains 3 groups and 9 rules
[ref]   GNOME is a graphical desktop environment bundled with many Linux distributions that allow users to easily interact with the operating system graphically rather than textually. The GNOME Graphical Display Manager (GDM) provides login, logout, and user switching contexts as well as display server management.

GNOME is developed by the GNOME Project and is considered the default Oracle Linux Graphical environment.

For more information on GNOME and the GNOME Project, see https://www.gnome.org.
Group   Configure GNOME Login Screen   Group contains 3 rules

Rule   Disable the GNOME3 Login User List   [ref]

In the default graphical environment, users logging directly into the system are greeted with a login screen that displays all known users. This functionality should be disabled by setting disable-user-list to true.

To disable, add or edit disable-user-list to /etc/dconf/db/local.d/00-security-settings. For example:
[org/gnome/login-screen]
disable-user-list=true
Once the setting has been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example:
/org/gnome/login-screen/disable-user-list
After the settings have been set, run dconf update.
Rationale:
Leaving the user list enabled is a security risk since it allows anyone with physical access to the system to quickly enumerate known user accounts without logging in.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_disable_user_list
Identifiers and References

References:  CM-6(a), AC-23, SRG-OS-000480-GPOS-00227, OL08-00-020032, SV-248673r779585_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020032
  - NIST-800-53-AC-23
  - NIST-800-53-CM-6(a)
  - dconf_gnome_disable_user_list
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Disable the GNOME3 Login User List
  ini_file:
    dest: /etc/dconf/db/local.d/00-security-settings
    section: org/gnome/login-screen
    option: disable-user-list
    value: 'true'
    no_extra_spaces: true
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020032
  - NIST-800-53-AC-23
  - NIST-800-53-CM-6(a)
  - dconf_gnome_disable_user_list
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Prevent user modification of GNOME3 disablement of Login User List
  lineinfile:
    path: /etc/dconf/db/local.d/locks/00-security-settings-lock
    regexp: ^/org/gnome/login-screen/disable-user-list$
    line: /org/gnome/login-screen/disable-user-list
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020032
  - NIST-800-53-AC-23
  - NIST-800-53-CM-6(a)
  - dconf_gnome_disable_user_list
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020032
  - NIST-800-53-AC-23
  - NIST-800-53-CM-6(a)
  - dconf_gnome_disable_user_list
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

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

mkdir -p "${DBDIR}"

# Comment out the configurations in databases different from the target one
if [ "${#SETTINGSFILES[@]}" -ne 0 ]
then
    if grep -q "^\\s*disable-user-list\\s*=" "${SETTINGSFILES[@]}"
    then
        
        sed -Ei "s/(^\s*)disable-user-list(\s*=)/#\1disable-user-list\2/g" "${SETTINGSFILES[@]}"
    fi
fi


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

escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")"
if grep -q "^\\s*disable-user-list\\s*=" "${DCONFFILE}"
then
        sed -i "s/\\s*disable-user-list\\s*=\\s*.*/disable-user-list=${escaped_value}/g" "${DCONFFILE}"
    else
        sed -i "\\|\\[org/gnome/login-screen\\]|a\\disable-user-list=${escaped_value}" "${DCONFFILE}"
fi

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

mkdir -p "${LOCKSFOLDER}"

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

if ! grep -qr "^/org/gnome/login-screen/disable-user-list$" /etc/dconf/db/local.d/
then
    echo "/org/gnome/login-screen/disable-user-list" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock"
fi

dconf update

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

Rule   Enable the GNOME3 Screen Locking On Smartcard Removal   [ref]

In the default graphical environment, screen locking on smartcard removal can be enabled by setting removal-action to 'lock-screen'.

To enable, add or edit removal-action to /etc/dconf/db/local.d/00-security-settings. For example:
[org/gnome/settings-daemon/peripherals/smartcard]
removal-action='lock-screen'
Once the setting has been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example:
/org/gnome/settings-daemon/peripherals/smartcard/removal-action
After the settings have been set, run dconf update.
Rationale:
Locking the screen automatically when removing the smartcard can prevent undesired access to system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_lock_screen_on_smartcard_removal
Identifiers and References

References:  CCI-000056, CCI-000058, SRG-OS-000028-GPOS-00009, SRG-OS-000030-GPOS-00011, OL08-00-020050, SV-248679r818655_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020050
  - dconf_gnome_lock_screen_on_smartcard_removal
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Detect if removal-action can be found on /etc/dconf/db/local.d/
  find:
    path: /etc/dconf/db/local.d/
    contains: ^\s*removal-action
  register: dconf_gnome_lock_screen_on_smartcard_removal_config_files
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020050
  - dconf_gnome_lock_screen_on_smartcard_removal
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Configure removal-action - default file
  ini_file:
    dest: /etc/dconf/db/local.d//00-security-settings
    section: org/gnome/settings-daemon/peripherals/smartcard
    option: removal-action
    value: '''lock-screen'''
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - dconf_gnome_lock_screen_on_smartcard_removal_config_files is defined and dconf_gnome_lock_screen_on_smartcard_removal_config_files.matched
    == 0
  tags:
  - DISA-STIG-OL08-00-020050
  - dconf_gnome_lock_screen_on_smartcard_removal
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Configure removal-action - existing files
  ini_file:
    dest: '{{ item.path }}'
    section: org/gnome/settings-daemon/peripherals/smartcard
    option: removal-action
    value: '''lock-screen'''
    create: true
  with_items: '{{ dconf_gnome_lock_screen_on_smartcard_removal_config_files.files
    }}'
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - dconf_gnome_lock_screen_on_smartcard_removal_config_files is defined and dconf_gnome_lock_screen_on_smartcard_removal_config_files.matched
    > 0
  tags:
  - DISA-STIG-OL08-00-020050
  - dconf_gnome_lock_screen_on_smartcard_removal
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Detect if lock for removal-action can be found on /etc/dconf/db/local.d/
  find:
    path: /etc/dconf/db/local.d/locks
    contains: ^\s*removal-action
  register: dconf_gnome_lock_screen_on_smartcard_removal_lock_files
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020050
  - dconf_gnome_lock_screen_on_smartcard_removal
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Prevent user modification removal-action - default file
  lineinfile:
    path: /etc/dconf/db/local.d/locks/00-security-settings-lock
    regexp: ^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$
    line: /org/gnome/settings-daemon/peripherals/smartcard/removal-action
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - dconf_gnome_lock_screen_on_smartcard_removal_lock_files is defined and dconf_gnome_lock_screen_on_smartcard_removal_lock_files.matched
    == 0
  tags:
  - DISA-STIG-OL08-00-020050
  - dconf_gnome_lock_screen_on_smartcard_removal
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Prevent user modification removal-action - existing files
  lineinfile:
    path: '{{ item.path }}'
    regexp: ^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$
    line: /org/gnome/settings-daemon/peripherals/smartcard/removal-action
    create: true
  with_items: '{{ dconf_gnome_lock_screen_on_smartcard_removal_lock_files.files }}'
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - dconf_gnome_lock_screen_on_smartcard_removal_lock_files is defined and dconf_gnome_lock_screen_on_smartcard_removal_lock_files.matched
    > 0
  tags:
  - DISA-STIG-OL08-00-020050
  - dconf_gnome_lock_screen_on_smartcard_removal
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update - removal-action
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020050
  - dconf_gnome_lock_screen_on_smartcard_removal
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

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

mkdir -p "${DBDIR}"

# Comment out the configurations in databases different from the target one
if [ "${#SETTINGSFILES[@]}" -ne 0 ]
then
    if grep -q "^\\s*removal-action\\s*=" "${SETTINGSFILES[@]}"
    then
        
        sed -Ei "s/(^\s*)removal-action(\s*=)/#\1removal-action\2/g" "${SETTINGSFILES[@]}"
    fi
fi


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

escaped_value="$(sed -e 's/\\/\\\\/g' <<< "'lock-screen'")"
if grep -q "^\\s*removal-action\\s*=" "${DCONFFILE}"
then
        sed -i "s/\\s*removal-action\\s*=\\s*.*/removal-action=${escaped_value}/g" "${DCONFFILE}"
    else
        sed -i "\\|\\[org/gnome/settings-daemon/peripherals/smartcard\\]|a\\removal-action=${escaped_value}" "${DCONFFILE}"
fi

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

mkdir -p "${LOCKSFOLDER}"

# Comment out the configurations in databases different from the target one
if [[ ! -z "${LOCKFILES}" ]]
then
    sed -i -E "s|^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$|#&|" "${LOCKFILES[@]}"
fi

if ! grep -qr "^/org/gnome/settings-daemon/peripherals/smartcard/removal-action$" /etc/dconf/db/local.d/
then
    echo "/org/gnome/settings-daemon/peripherals/smartcard/removal-action" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock"
fi

dconf update

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Configure GNOME Screen Locking   Group contains 5 rules
[ref]   In the default GNOME3 desktop, the screen can be locked by selecting the user name in the far right corner of the main panel and selecting Lock.

The following sections detail commands to enforce idle activation of the screensaver, screen locking, a blank-screen screensaver, and an idle activation time.

Because users should be trained to lock the screen when they step away from the computer, the automatic locking feature is only meant as a backup.

The root account can be screen-locked; however, the root account should never be used to log into an X Windows environment and should only be used to for direct login via console in emergency circumstances.

For more information about enforcing preferences in the GNOME3 environment using the DConf configuration system, see http://wiki.gnome.org/dconf and the man page dconf(1).

Rule   Set GNOME3 Screensaver Inactivity Timeout   [ref]

The idle time-out value for inactivity in the GNOME3 desktop is configured via the idle-delay setting must be set under an appropriate configuration file(s) in the /etc/dconf/db/local.d directory and locked in /etc/dconf/db/local.d/locks directory to prevent user modification.

For example, to configure the system for a 15 minute delay, add the following to /etc/dconf/db/local.d/00-security-settings:
[org/gnome/desktop/session]
idle-delay=uint32 900
Rationale:
A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operating system session prior to vacating the vicinity, GNOME3 can be configured to identify when a user's session has idled and take action to initiate a session lock.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_delay
Identifiers and References

References:  1, 12, 15, 16, 5.5.5, DSS05.04, DSS05.10, DSS06.10, 3.1.10, CCI-000057, CCI-000060, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-11(a), CM-6(a), PR.AC-7, FMT_MOF_EXT.1, Req-8.1.8, 8.2.8, SRG-OS-000029-GPOS-00010, SRG-OS-000031-GPOS-00012, OL08-00-020060, SV-248680r779606_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020060
  - NIST-800-171-3.1.10
  - NIST-800-53-AC-11(a)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_idle_delay
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy
- name: XCCDF Value inactivity_timeout_value # promote to variable
  set_fact:
    inactivity_timeout_value: !!str 900
  tags:
    - always

- name: Set GNOME3 Screensaver Inactivity Timeout
  ini_file:
    dest: /etc/dconf/db/local.d/00-security-settings
    section: org/gnome/desktop/session
    option: idle-delay
    value: uint32 {{ inactivity_timeout_value }}
    create: true
    no_extra_spaces: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020060
  - NIST-800-171-3.1.10
  - NIST-800-53-AC-11(a)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_idle_delay
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020060
  - NIST-800-171-3.1.10
  - NIST-800-53-AC-11(a)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_idle_delay
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

inactivity_timeout_value='900'


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

mkdir -p "${DBDIR}"

# Comment out the configurations in databases different from the target one
if [ "${#SETTINGSFILES[@]}" -ne 0 ]
then
    if grep -q "^\\s*idle-delay\\s*=" "${SETTINGSFILES[@]}"
    then
        
        sed -Ei "s/(^\s*)idle-delay(\s*=)/#\1idle-delay\2/g" "${SETTINGSFILES[@]}"
    fi
fi


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

escaped_value="$(sed -e 's/\\/\\\\/g' <<< "uint32 ${inactivity_timeout_value}")"
if grep -q "^\\s*idle-delay\\s*=" "${DCONFFILE}"
then
        sed -i "s/\\s*idle-delay\\s*=\\s*.*/idle-delay=${escaped_value}/g" "${DCONFFILE}"
    else
        sed -i "\\|\\[org/gnome/desktop/session\\]|a\\idle-delay=${escaped_value}" "${DCONFFILE}"
fi

dconf update

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

Rule   Set GNOME3 Screensaver Lock Delay After Activation Period   [ref]

To activate the locking delay of the screensaver in the GNOME3 desktop when the screensaver is activated, add or set lock-delay to uint32 5 in /etc/dconf/db/local.d/00-security-settings. For example:
[org/gnome/desktop/screensaver]
lock-delay=uint32 5
After the settings have been set, run dconf update.
Rationale:
A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to logout because of the temporary nature of the absense.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_delay
Identifiers and References

References:  1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, 3.1.10, CCI-000056, CCI-000057, CCI-000060, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-11(a), CM-6(a), PR.AC-7, FMT_MOF_EXT.1, Req-8.1.8, SRG-OS-000029-GPOS-00010, SRG-OS-000031-GPOS-00012, OL08-00-020031, SV-248672r779582_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020031
  - NIST-800-171-3.1.10
  - NIST-800-53-AC-11(a)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - dconf_gnome_screensaver_lock_delay
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy
- name: XCCDF Value var_screensaver_lock_delay # promote to variable
  set_fact:
    var_screensaver_lock_delay: !!str 5
  tags:
    - always

- name: Set GNOME3 Screensaver Lock Delay After Activation Period
  ini_file:
    dest: /etc/dconf/db/local.d/00-security-settings
    section: org/gnome/desktop/screensaver
    option: lock-delay
    value: uint32 {{ var_screensaver_lock_delay }}
    create: true
    no_extra_spaces: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020031
  - NIST-800-171-3.1.10
  - NIST-800-53-AC-11(a)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - dconf_gnome_screensaver_lock_delay
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020031
  - NIST-800-171-3.1.10
  - NIST-800-53-AC-11(a)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - dconf_gnome_screensaver_lock_delay
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

var_screensaver_lock_delay='5'


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

mkdir -p "${DBDIR}"

# Comment out the configurations in databases different from the target one
if [ "${#SETTINGSFILES[@]}" -ne 0 ]
then
    if grep -q "^\\s*lock-delay\\s*=" "${SETTINGSFILES[@]}"
    then
        
        sed -Ei "s/(^\s*)lock-delay(\s*=)/#\1lock-delay\2/g" "${SETTINGSFILES[@]}"
    fi
fi


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

escaped_value="$(sed -e 's/\\/\\\\/g' <<< "uint32 ${var_screensaver_lock_delay}")"
if grep -q "^\\s*lock-delay\\s*=" "${DCONFFILE}"
then
        sed -i "s/\\s*lock-delay\\s*=\\s*.*/lock-delay=${escaped_value}/g" "${DCONFFILE}"
    else
        sed -i "\\|\\[org/gnome/desktop/screensaver\\]|a\\lock-delay=${escaped_value}" "${DCONFFILE}"
fi

dconf update

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

Rule   Enable GNOME3 Screensaver Lock After Idle Period   [ref]

To activate locking of the screensaver in the GNOME3 desktop when it is activated, add or set lock-enabled to true in /etc/dconf/db/local.d/00-security-settings. For example:
[org/gnome/desktop/screensaver]
lock-enabled=true
Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example:
/org/gnome/desktop/screensaver/lock-enabled
After the settings have been set, run dconf update.
Rationale:
A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to logout because of the temporary nature of the absense.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled
Identifiers and References

References:  1, 12, 15, 16, 5.5.5, DSS05.04, DSS05.10, DSS06.10, 3.1.10, CCI-000056, CCI-000058, CCI-000060, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), PR.AC-7, FMT_MOF_EXT.1, Req-8.1.8, 8.2.8, SRG-OS-000028-GPOS-00009, SRG-OS-000030-GPOS-00011, OL08-00-020030, SV-248671r779579_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_distribution == 'SLES'
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Enable GNOME3 Screensaver Lock After Idle Period
  ini_file:
    dest: /etc/dconf/db/local.d/00-security-settings
    section: org/gnome/desktop/screensaver
    option: lock-enabled
    value: 'true'
    create: true
    no_extra_spaces: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_distribution != 'SLES'
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Prevent user modification of GNOME lock-enabled
  lineinfile:
    path: /etc/dconf/db/local.d/locks/00-security-settings-lock
    regexp: ^/org/gnome/desktop/screensaver/lock-enabled$
    line: /org/gnome/desktop/screensaver/lock-enabled
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_distribution != 'SLES'
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Enable GNOME3 Screensaver Lock After Idle Period
  ini_file:
    dest: /etc/dconf/db/local.d/00-security-settings
    section: org/gnome/desktop/lockdown
    option: disable-lock-screen
    value: 'false'
    create: true
    no_extra_spaces: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_distribution == 'SLES'
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Prevent user modification of GNOME disable-lock-screen
  lineinfile:
    path: /etc/dconf/db/local.d/locks/00-security-settings-lock
    regexp: ^/org/gnome/desktop/lockdown/disable-lock-screen$
    line: /org/gnome/desktop/lockdown/disable-lock-screen
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_distribution == 'SLES'
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Check GNOME3 screenserver disable-lock-screen false
  command: gsettings get org.gnome.desktop.lockdown disable-lock-screen
  register: cmd_out
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_distribution == 'SLES'
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Update GNOME3 screenserver disable-lock-screen false
  command: gsettings set org.gnome.desktop.lockdown disable-lock-screen false
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_distribution == 'SLES'
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.5
  - DISA-STIG-OL08-00-020030
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_screensaver_lock_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

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

mkdir -p "${DBDIR}"

# Comment out the configurations in databases different from the target one
if [ "${#SETTINGSFILES[@]}" -ne 0 ]
then
    if grep -q "^\\s*lock-enabled\\s*=" "${SETTINGSFILES[@]}"
    then
        
        sed -Ei "s/(^\s*)lock-enabled(\s*=)/#\1lock-enabled\2/g" "${SETTINGSFILES[@]}"
    fi
fi


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

escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")"
if grep -q "^\\s*lock-enabled\\s*=" "${DCONFFILE}"
then
        sed -i "s/\\s*lock-enabled\\s*=\\s*.*/lock-enabled=${escaped_value}/g" "${DCONFFILE}"
    else
        sed -i "\\|\\[org/gnome/desktop/screensaver\\]|a\\lock-enabled=${escaped_value}" "${DCONFFILE}"
fi

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

mkdir -p "${LOCKSFOLDER}"

# Comment out the configurations in databases different from the target one
if [[ ! -z "${LOCKFILES}" ]]
then
    sed -i -E "s|^/org/gnome/desktop/screensaver/lock-enabled$|#&|" "${LOCKFILES[@]}"
fi

if ! grep -qr "^/org/gnome/desktop/screensaver/lock-enabled$" /etc/dconf/db/local.d/
then
    echo "/org/gnome/desktop/screensaver/lock-enabled" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock"
fi

dconf update

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

Rule   Ensure Users Cannot Change GNOME3 Screensaver Settings   [ref]

If not already configured, ensure that users cannot change GNOME3 screensaver lock settings by adding /org/gnome/desktop/screensaver/lock-delay to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example:
/org/gnome/desktop/screensaver/lock-delay
After the settings have been set, run dconf update.
Rationale:
A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operating system session prior to vacating the vicinity, GNOME desktops can be configured to identify when a user's session has idled and take action to initiate the session lock. As such, users should not be allowed to change session settings.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_user_locks
Identifiers and References

References:  1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, 3.1.10, CCI-000057, CCI-000060, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), PR.AC-7, FMT_MOF_EXT.1, SRG-OS-000029-GPOS-00010, SRG-OS-000031-GPOS-00012, OL08-00-020080, SV-248682r779612_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020080
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - dconf_gnome_screensaver_user_locks
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Prevent user modification of GNOME lock-delay
  lineinfile:
    path: /etc/dconf/db/local.d/locks/00-security-settings-lock
    regexp: ^/org/gnome/desktop/screensaver/lock-delay$
    line: /org/gnome/desktop/screensaver/lock-delay
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020080
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - dconf_gnome_screensaver_user_locks
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020080
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - dconf_gnome_screensaver_user_locks
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

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

mkdir -p "${LOCKSFOLDER}"

# Comment out the configurations in databases different from the target one
if [[ ! -z "${LOCKFILES}" ]]
then
    sed -i -E "s|^/org/gnome/desktop/screensaver/lock-delay$|#&|" "${LOCKFILES[@]}"
fi

if ! grep -qr "^/org/gnome/desktop/screensaver/lock-delay$" /etc/dconf/db/local.d/
then
    echo "/org/gnome/desktop/screensaver/lock-delay" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock"
fi

dconf update

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

Rule   Ensure Users Cannot Change GNOME3 Session Idle Settings   [ref]

If not already configured, ensure that users cannot change GNOME3 session idle settings by adding /org/gnome/desktop/session/idle-delay to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example:
/org/gnome/desktop/session/idle-delay
After the settings have been set, run dconf update.
Rationale:
A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operating system session prior to vacating the vicinity, GNOME desktops can be configured to identify when a user's session has idled and take action to initiate the session lock. As such, users should not be allowed to change session settings.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_session_idle_user_locks
Identifiers and References

References:  1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, 3.1.10, CCI-000057, CCI-000060, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), PR.AC-7, FMT_MOF_EXT.1, Req-8.1.8, 8.2.8, SRG-OS-000029-GPOS-00010, SRG-OS-000031-GPOS-00012, OL08-00-020081, SV-248683r779615_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020081
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_session_idle_user_locks
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Prevent user modification of GNOME Session idle-delay
  lineinfile:
    path: /etc/dconf/db/local.d/locks/00-security-settings-lock
    regexp: ^/org/gnome/desktop/session/idle-delay$
    line: /org/gnome/desktop/session/idle-delay
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020081
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_session_idle_user_locks
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020081
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - dconf_gnome_session_idle_user_locks
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

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

mkdir -p "${LOCKSFOLDER}"

# Comment out the configurations in databases different from the target one
if [[ ! -z "${LOCKFILES}" ]]
then
    sed -i -E "s|^/org/gnome/desktop/session/idle-delay$|#&|" "${LOCKFILES[@]}"
fi

if ! grep -qr "^/org/gnome/desktop/session/idle-delay$" /etc/dconf/db/local.d/
then
    echo "/org/gnome/desktop/session/idle-delay" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock"
fi

dconf update

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   GNOME System Settings   Group contains 1 rule
[ref]   GNOME provides configuration and functionality to a graphical desktop environment that changes grahical configurations or allow a user to perform actions that users normally would not be able to do in non-graphical mode such as remote access configuration, power policies, Geo-location, etc. Configuring such settings in GNOME will prevent accidential graphical configuration changes by users from taking place.

Rule   Disable Ctrl-Alt-Del Reboot Key Sequence in GNOME3   [ref]

By default, GNOME will reboot the system if the Ctrl-Alt-Del key sequence is pressed.

To configure the system to ignore the Ctrl-Alt-Del key sequence from the Graphical User Interface (GUI) instead of rebooting the system, add or set logout to '' in /etc/dconf/db/local.d/00-security-settings. For example:
[org/gnome/settings-daemon/plugins/media-keys]
logout=''
Once the settings have been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example:
/org/gnome/settings-daemon/plugins/media-keys/logout
After the settings have been set, run dconf update.
Rationale:
A locally logged-in user who presses Ctrl-Alt-Del, when at the console, can reboot the system. If accidentally pressed, as could happen in the case of mixed OS environment, this can create the risk of short-term loss of availability of systems due to unintentional reboot.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_disable_ctrlaltdel_reboot
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.2, CCI-000366, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), CM-7(b), PR.AC-4, PR.DS-5, SRG-OS-000480-GPOS-00227, OL08-00-040171, SV-248870r780176_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-040171
  - NIST-800-171-3.1.2
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(b)
  - dconf_gnome_disable_ctrlaltdel_reboot
  - high_severity
  - low_complexity
  - medium_disruption
  - no_reboot_needed
  - unknown_strategy

- name: Disable Ctrl-Alt-Del Reboot Key Sequence in GNOME3
  ini_file:
    dest: /etc/dconf/db/local.d/00-security-settings
    section: org/gnome/settings-daemon/plugins/media-keys
    option: logout
    value: ''''''
    create: true
    no_extra_spaces: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040171
  - NIST-800-171-3.1.2
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(b)
  - dconf_gnome_disable_ctrlaltdel_reboot
  - high_severity
  - low_complexity
  - medium_disruption
  - no_reboot_needed
  - unknown_strategy

- name: Prevent user modification of GNOME disablement of Ctrl-Alt-Del
  lineinfile:
    path: /etc/dconf/db/local.d/locks/00-security-settings-lock
    regexp: ^/org/gnome/settings-daemon/plugins/media-keys/logout$
    line: /org/gnome/settings-daemon/plugins/media-keys/logout
    create: true
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040171
  - NIST-800-171-3.1.2
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(b)
  - dconf_gnome_disable_ctrlaltdel_reboot
  - high_severity
  - low_complexity
  - medium_disruption
  - no_reboot_needed
  - unknown_strategy

- name: Dconf Update
  command: dconf update
  when:
  - '"gdm" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040171
  - NIST-800-171-3.1.2
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(b)
  - dconf_gnome_disable_ctrlaltdel_reboot
  - high_severity
  - low_complexity
  - medium_disruption
  - no_reboot_needed
  - unknown_strategy

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

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

mkdir -p "${DBDIR}"

# Comment out the configurations in databases different from the target one
if [ "${#SETTINGSFILES[@]}" -ne 0 ]
then
    if grep -q "^\\s*logout\\s*=" "${SETTINGSFILES[@]}"
    then
        
        sed -Ei "s/(^\s*)logout(\s*=)/#\1logout\2/g" "${SETTINGSFILES[@]}"
    fi
fi


[ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}"
if ! grep -q "\\[org/gnome/settings-daemon/plugins/media-keys\\]" "${DCONFFILE}"
then
    printf '%s\n' "[org/gnome/settings-daemon/plugins/media-keys]" >> ${DCONFFILE}
fi

escaped_value="$(sed -e 's/\\/\\\\/g' <<< "''")"
if grep -q "^\\s*logout\\s*=" "${DCONFFILE}"
then
        sed -i "s/\\s*logout\\s*=\\s*.*/logout=${escaped_value}/g" "${DCONFFILE}"
    else
        sed -i "\\|\\[org/gnome/settings-daemon/plugins/media-keys\\]|a\\logout=${escaped_value}" "${DCONFFILE}"
fi

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

mkdir -p "${LOCKSFOLDER}"

# Comment out the configurations in databases different from the target one
if [[ ! -z "${LOCKFILES}" ]]
then
    sed -i -E "s|^/org/gnome/settings-daemon/plugins/media-keys/logout$|#&|" "${LOCKFILES[@]}"
fi

if ! grep -qr "^/org/gnome/settings-daemon/plugins/media-keys/logout$" /etc/dconf/db/local.d/
then
    echo "/org/gnome/settings-daemon/plugins/media-keys/logout" >> "/etc/dconf/db/local.d/locks/00-security-settings-lock"
fi

dconf update

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Sudo   Group contains 6 rules
[ref]   Sudo, which stands for "su 'do'", provides the ability to delegate authority to certain users, groups of users, or system administrators. When configured for system users and/or groups, Sudo can allow a user or group to execute privileged commands that normally only root is allowed to execute.

For more information on Sudo and addition Sudo configuration options, see https://www.sudo.ws.

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

The sudo !authenticate option, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that the !authenticate option does not exist in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/.
Rationale:
Without re-authentication, users may access resources or perform tasks for which they do not have authorization.

When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudo_remove_no_authenticate
Identifiers and References

References:  BP28(R5), BP28(R59), 1, 12, 15, 16, 5, DSS05.04, DSS05.10, DSS06.03, DSS06.10, CCI-002038, 4.3.3.5.1, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-11, CM-6(a), PR.AC-1, PR.AC-7, SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158, OL08-00-010381, SV-248582r880551_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find /etc/sudoers.d/ files
  find:
    paths:
    - /etc/sudoers.d/
  register: sudoers
  tags:
  - DISA-STIG-OL08-00-010381
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-11
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_remove_no_authenticate

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

Complexity:low
Disruption:low
Strategy:restrict

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

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

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

The sudo NOPASSWD tag, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that the NOPASSWD tag does not exist in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/.
Rationale:
Without re-authentication, users may access resources or perform tasks for which they do not have authorization.

When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudo_remove_nopasswd
Identifiers and References

References:  BP28(R5), BP28(R59), 1, 12, 15, 16, 5, DSS05.04, DSS05.10, DSS06.03, DSS06.10, CCI-002038, 4.3.3.5.1, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-11, CM-6(a), PR.AC-1, PR.AC-7, SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158, OL08-00-010380, SV-248581r860915_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find /etc/sudoers.d/ files
  find:
    paths:
    - /etc/sudoers.d/
  register: sudoers
  tags:
  - DISA-STIG-OL08-00-010380
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-11
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_remove_nopasswd

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

Complexity:low
Disruption:low
Strategy:restrict

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

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

Rule   Require Re-Authentication When Using the sudo Command   [ref]

The sudo timestamp_timeout tag sets the amount of time sudo password prompt waits. The default timestamp_timeout value is 5 minutes. The timestamp_timeout should be configured by making sure that the timestamp_timeout tag exists in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/. If the value is set to an integer less than 0, the user's time stamp will not expire and the user will not have to re-authenticate for privileged actions until the user's session is terminated.
Rationale:
Without re-authentication, users may access resources or perform tasks for which they do not have authorization.

When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudo_require_reauthentication
Identifiers and References

References:  CCI-002038, IA-11, SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158, OL08-00-010384, SV-248585r880557_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010384
  - NIST-800-53-IA-11
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_require_reauthentication
- name: XCCDF Value var_sudo_timestamp_timeout # promote to variable
  set_fact:
    var_sudo_timestamp_timeout: !!str 0
  tags:
    - always

- name: Find out if /etc/sudoers.d/* files contain 'Defaults timestamp_timeout' to
    be deduplicated
  find:
    path: /etc/sudoers.d
    patterns: '*'
    contains: ^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=.*
  register: sudoers_d_defaults_timestamp_timeout
  when: '"sudo" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010384
  - NIST-800-53-IA-11
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_require_reauthentication

- name: Remove found occurrences of 'Defaults timestamp_timeout' from /etc/sudoers.d/*
    files
  lineinfile:
    path: '{{ item.path }}'
    regexp: ^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=.*
    state: absent
  with_items: '{{ sudoers_d_defaults_timestamp_timeout.files }}'
  when: '"sudo" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010384
  - NIST-800-53-IA-11
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_require_reauthentication

- name: Ensure timestamp_timeout is enabled with the appropriate value in /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    regexp: ^[\s]*Defaults\s(.*)\btimestamp_timeout[\s]*=[\s]*[-]?\w+\b(.*)$
    line: Defaults \1timestamp_timeout={{ var_sudo_timestamp_timeout }}\2
    validate: /usr/sbin/visudo -cf %s
    backrefs: true
  register: edit_sudoers_timestamp_timeout_option
  when: '"sudo" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010384
  - NIST-800-53-IA-11
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_require_reauthentication

- name: Enable timestamp_timeout option with appropriate value in /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    line: Defaults timestamp_timeout={{ var_sudo_timestamp_timeout }}
    validate: /usr/sbin/visudo -cf %s
  when:
  - '"sudo" in ansible_facts.packages'
  - edit_sudoers_timestamp_timeout_option is defined and not edit_sudoers_timestamp_timeout_option.changed
  tags:
  - DISA-STIG-OL08-00-010384
  - NIST-800-53-IA-11
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_require_reauthentication

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q sudo; then

var_sudo_timestamp_timeout='0'


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

if /usr/sbin/visudo -qcf /etc/sudoers; then
    cp /etc/sudoers /etc/sudoers.bak
    if ! grep -P '^[\s]*Defaults.*timestamp_timeout[\s]*=[\s]*[-]?\w+.*$' /etc/sudoers; then
        # sudoers file doesn't define Option timestamp_timeout
        echo "Defaults timestamp_timeout=${var_sudo_timestamp_timeout}" >> /etc/sudoers
    else
        # sudoers file defines Option timestamp_timeout, remediate if appropriate value is not set
        if ! grep -P "^[\s]*Defaults.*timestamp_timeout[\s]*=[\s]*${var_sudo_timestamp_timeout}.*$" /etc/sudoers; then
            
            sed -Ei "s/(^[[:blank:]]*Defaults.*timestamp_timeout[[:blank:]]*=)[[:blank:]]*[-]?\w+(.*$)/\1${var_sudo_timestamp_timeout}\2/" /etc/sudoers
        fi
    fi
    
    # Check validity of sudoers and cleanup bak
    if /usr/sbin/visudo -qcf /etc/sudoers; then
        rm -f /etc/sudoers.bak
    else
        echo "Fail to validate remediated /etc/sudoers, reverting to original file."
        mv /etc/sudoers.bak /etc/sudoers
        false
    fi
else
    echo "Skipping remediation, /etc/sudoers failed to validate"
    false
fi

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

Rule   The operating system must restrict privilege elevation to authorized personnel   [ref]

The sudo command allows a user to execute programs with elevated (administrator) privileges. It prompts the user for their password and confirms your request to execute a command by checking a file, called sudoers. Restrict privileged actions by removing the following entries from the sudoers file: ALL ALL=(ALL) ALL ALL ALL=(ALL:ALL) ALL
Warning:  This rule doesn't come with a remediation, as the exact requirement allows exceptions, and removing lines from the sudoers file can make the system non-administrable.
Rationale:
If the "sudoers" file is not configured correctly, any user defined on the system can initiate privileged actions on the target system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudo_restrict_privilege_elevation_to_authorized
Identifiers and References

References:  CCI-000366, CM-6(b), CM-6(iv), SRG-OS-000480-GPOS-00227, OL08-00-010382, SV-248583r779315_rule

Rule   Ensure sudo only includes the default configuration directory   [ref]

Administrators can configure authorized sudo users via drop-in files, and it is possible to include other directories and configuration files from the file currently being parsed. Make sure that /etc/sudoers only includes drop-in configuration files from /etc/sudoers.d, or that no drop-in file is included. Either the /etc/sudoers should contain only one #includedir directive pointing to /etc/sudoers.d, and no file in /etc/sudoers.d/ should include other files or directories; Or the /etc/sudoers should not contain any #include, @include, #includedir or @includedir directives. Note that the '#' character doesn't denote a comment in the configuration file.
Rationale:
Some sudo configurtion options allow users to run programs without re-authenticating. Use of these configuration options makes it easier for one compromised accound to be used to compromise other accounts.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudoers_default_includedir
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010379, SV-252655r880549_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Check for duplicate values
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^#includedir.*$
    state: absent
  check_mode: true
  changed_when: false
  register: dupes
  tags:
  - DISA-STIG-OL08-00-010379
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sudoers_default_includedir

- name: Deduplicate values from /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^#includedir.*$
    state: absent
  when: dupes.found is defined and dupes.found > 1
  tags:
  - DISA-STIG-OL08-00-010379
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sudoers_default_includedir

- name: Insert correct line into /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^#includedir.*$
    line: '#includedir /etc/sudoers.d'
    state: present
  tags:
  - DISA-STIG-OL08-00-010379
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sudoers_default_includedir

- name: Ensure sudoers doesn't include other non-default file
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^[#@]include[\s]+.*$
    state: absent
  tags:
  - DISA-STIG-OL08-00-010379
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sudoers_default_includedir

- name: Ensure sudoers doesn't have non-default includedir
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^@includedir[\s]+.*$
    state: absent
  tags:
  - DISA-STIG-OL08-00-010379
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sudoers_default_includedir

- name: Find out if /etc/sudoers.d/* files contain file or directory includes
  find:
    path: /etc/sudoers.d
    patterns: '*'
    contains: ^[#@]include(dir)?\s.*$
  register: sudoers_d_includes
  tags:
  - DISA-STIG-OL08-00-010379
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sudoers_default_includedir

- name: Remove found occurrences of file and directory includes from /etc/sudoers.d/*
    files
  lineinfile:
    path: '{{ item.path }}'
    regexp: ^[#@]include(dir)?\s.*$
    state: absent
  with_items: '{{ sudoers_d_includes.files }}'
  tags:
  - DISA-STIG-OL08-00-010379
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sudoers_default_includedir


sudoers_config_file="/etc/sudoers"
sudoers_config_dir="/etc/sudoers.d"
sudoers_includedir_count=$(grep -c "#includedir" "$sudoers_config_file")
if [ "$sudoers_includedir_count" -gt 1 ]; then
    sed -i "/#includedir/d" "$sudoers_config_file"
    echo "#includedir /etc/sudoers.d" >> "$sudoers_config_file"
elif [ "$sudoers_includedir_count" -eq 0 ]; then
    echo "#includedir /etc/sudoers.d" >> "$sudoers_config_file"
else
    if ! grep -q "^#includedir /etc/sudoers.d" "$sudoers_config_file"; then
        sed -i "s|^#includedir.*|#includedir /etc/sudoers.d|g" "$sudoers_config_file"
    fi
fi

sed -Ei "/^#include\s/d; /^@includedir\s/d" "$sudoers_config_file"

if grep -Pr "^[#@]include(dir)?\s" "$sudoers_config_dir" ; then
    sed -Ei "/^[#@]include(dir)?\s/d" "$sudoers_config_dir"/*
fi

Rule   Ensure invoking users password for privilege escalation when using sudo   [ref]

The sudoers security policy requires that users authenticate themselves before they can use sudo. When sudoers requires authentication, it validates the invoking user's credentials. The expected output for:
 sudo cvtsudoers -f sudoers /etc/sudoers | grep -E '^Defaults !?(rootpw|targetpw|runaspw)$' 
 Defaults !targetpw
      Defaults !rootpw
      Defaults !runaspw 
or if cvtsudoers not supported:
 sudo find /etc/sudoers /etc/sudoers.d \( \! -name '*~' -a \! -name '*.*' \) -exec grep -E --with-filename '^[[:blank:]]*Defaults[[:blank:]](.*[[:blank:]])?!?\b(rootpw|targetpw|runaspw)' -- {} \; 
 /etc/sudoers:Defaults !targetpw
      /etc/sudoers:Defaults !rootpw
      /etc/sudoers:Defaults !runaspw 
Rationale:
If the rootpw, targetpw, or runaspw flags are defined and not disabled, by default the operating system will prompt the invoking user for the "root" user password.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudoers_validate_passwd
Identifiers and References

References:  CCI-000366, CCI-002227, CM-6(b), CM-6.1(iv), SRG-OS-000480-GPOS-00227, OL08-00-010383, SV-248584r880554_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find out if /etc/sudoers.d/* files contain Defaults targetpw to be deduplicated
  find:
    path: /etc/sudoers.d
    patterns: '*'
    contains: ^Defaults targetpw$
  register: sudoers_d_defaults
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Remove found occurrences of Defaults targetpw from /etc/sudoers.d/* files
  lineinfile:
    path: '{{ item.path }}'
    regexp: ^Defaults targetpw$
    state: absent
  with_items: '{{ sudoers_d_defaults.files }}'
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Find out if /etc/sudoers.d/* files contain Defaults rootpw to be deduplicated
  find:
    path: /etc/sudoers.d
    patterns: '*'
    contains: ^Defaults rootpw$
  register: sudoers_d_defaults
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Remove found occurrences of Defaults rootpw from /etc/sudoers.d/* files
  lineinfile:
    path: '{{ item.path }}'
    regexp: ^Defaults rootpw$
    state: absent
  with_items: '{{ sudoers_d_defaults.files }}'
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Find out if /etc/sudoers.d/* files contain Defaults runaspw to be deduplicated
  find:
    path: /etc/sudoers.d
    patterns: '*'
    contains: ^Defaults runaspw$
  register: sudoers_d_defaults
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Remove found occurrences of Defaults runaspw from /etc/sudoers.d/* files
  lineinfile:
    path: '{{ item.path }}'
    regexp: ^Defaults runaspw$
    state: absent
  with_items: '{{ sudoers_d_defaults.files }}'
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Remove any ocurrences of Defaults targetpw in /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    regexp: ^Defaults targetpw$
    validate: /usr/sbin/visudo -cf %s
    state: absent
  register: sudoers_file_defaults
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Remove any ocurrences of Defaults rootpw in /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    regexp: ^Defaults rootpw$
    validate: /usr/sbin/visudo -cf %s
    state: absent
  register: sudoers_file_defaults
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Remove any ocurrences of Defaults runaspw in /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    regexp: ^Defaults runaspw$
    validate: /usr/sbin/visudo -cf %s
    state: absent
  register: sudoers_file_defaults
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Check for duplicate values
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !targetpw$
    state: absent
  check_mode: true
  changed_when: false
  register: dupes
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Deduplicate values from /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !targetpw$
    state: absent
  when: dupes.found is defined and dupes.found > 1
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Insert correct line into /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !targetpw$
    line: Defaults !targetpw
    state: present
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Check for duplicate values
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !rootpw$
    state: absent
  check_mode: true
  changed_when: false
  register: dupes
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Deduplicate values from /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !rootpw$
    state: absent
  when: dupes.found is defined and dupes.found > 1
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Insert correct line into /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !rootpw$
    line: Defaults !rootpw
    state: present
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Check for duplicate values
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !runaspw$
    state: absent
  check_mode: true
  changed_when: false
  register: dupes
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Deduplicate values from /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !runaspw$
    state: absent
  when: dupes.found is defined and dupes.found > 1
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

- name: Insert correct line into /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    create: false
    regexp: ^Defaults !runaspw$
    line: Defaults !runaspw
    state: present
  tags:
  - DISA-STIG-OL08-00-010383
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudoers_validate_passwd

if grep -x '^Defaults targetpw$' /etc/sudoers; then
    sed -i "/Defaults targetpw/d" /etc/sudoers \;
fi
if grep -x '^Defaults targetpw$' /etc/sudoers.d/*; then
    find /etc/sudoers.d/ -type f -exec sed -i "/Defaults targetpw/d" {} \;
fi
if grep -x '^Defaults rootpw$' /etc/sudoers; then
    sed -i "/Defaults rootpw/d" /etc/sudoers \;
fi
if grep -x '^Defaults rootpw$' /etc/sudoers.d/*; then
    find /etc/sudoers.d/ -type f -exec sed -i "/Defaults rootpw/d" {} \;
fi
if grep -x '^Defaults runaspw$' /etc/sudoers; then
    sed -i "/Defaults runaspw/d" /etc/sudoers \;
fi
if grep -x '^Defaults runaspw$' /etc/sudoers.d/*; then
    find /etc/sudoers.d/ -type f -exec sed -i "/Defaults runaspw/d" {} \;
fi

if [ -e "/etc/sudoers" ] ; then
    
    LC_ALL=C sed -i "/Defaults !targetpw/d" "/etc/sudoers"
else
    touch "/etc/sudoers"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/sudoers"

cp "/etc/sudoers" "/etc/sudoers.bak"
# Insert at the end of the file
printf '%s\n' "Defaults !targetpw" >> "/etc/sudoers"
# Clean up after ourselves.
rm "/etc/sudoers.bak"
if [ -e "/etc/sudoers" ] ; then
    
    LC_ALL=C sed -i "/Defaults !rootpw/d" "/etc/sudoers"
else
    touch "/etc/sudoers"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/sudoers"

cp "/etc/sudoers" "/etc/sudoers.bak"
# Insert at the end of the file
printf '%s\n' "Defaults !rootpw" >> "/etc/sudoers"
# Clean up after ourselves.
rm "/etc/sudoers.bak"
if [ -e "/etc/sudoers" ] ; then
    
    LC_ALL=C sed -i "/Defaults !runaspw/d" "/etc/sudoers"
else
    touch "/etc/sudoers"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/sudoers"

cp "/etc/sudoers" "/etc/sudoers.bak"
# Insert at the end of the file
printf '%s\n' "Defaults !runaspw" >> "/etc/sudoers"
# Clean up after ourselves.
rm "/etc/sudoers.bak"
Group   System Tooling / Utilities   Group contains 8 rules
[ref]   The following checks evaluate the system for recommended base packages -- both for installation and removal.

Rule   Install rng-tools Package   [ref]

The rng-tools package can be installed with the following command:
$ sudo yum install rng-tools
Rationale:
rng-tools provides hardware random number generator tools, such as those used in the formation of x509/PKI certificates.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_package_rng-tools_installed
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010472, SV-248600r779366_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=rng-tools


[[packages]]
name = "rng-tools"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_rng-tools

class install_rng-tools {
  package { 'rng-tools':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure rng-tools is installed
  package:
    name: rng-tools
    state: present
  tags:
  - DISA-STIG-OL08-00-010472
  - enable_strategy
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - package_rng-tools_installed

Complexity:low
Disruption:low
Strategy:enable

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

Rule   Uninstall abrt-libs Package   [ref]

The abrt-libs package can be removed with the following command:
$ sudo yum erase abrt-libs
Rationale:
abrt-libs provides libraries for the ABRT package.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_abrt-libs_removed
Identifiers and References

References:  CCI-000381, SRG-OS-000095-GPOS-00049, OL08-00-040001, SV-248824r780038_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=abrt-libs

Complexity:low
Disruption:low
Strategy:disable
include remove_abrt-libs

class remove_abrt-libs {
  package { 'abrt-libs':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure abrt-libs is removed
  package:
    name: abrt-libs
    state: absent
  tags:
  - DISA-STIG-OL08-00-040001
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_abrt-libs_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove abrt-libs
#	   from the system, and may remove any packages
#	   that depend on abrt-libs. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "abrt-libs" ; then

    yum remove -y "abrt-libs"

fi

Rule   Uninstall abrt-server-info-page Package   [ref]

The abrt-server-info-page package can be removed with the following command:
$ sudo yum erase abrt-server-info-page
Rationale:
abrt-server-info-page provides a web page with summary of ABRT services.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_abrt-server-info-page_removed
Identifiers and References

References:  CCI-000381, SRG-OS-000095-GPOS-00049, OL08-00-040001, SV-248824r780038_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=abrt-server-info-page

Complexity:low
Disruption:low
Strategy:disable
include remove_abrt-server-info-page

class remove_abrt-server-info-page {
  package { 'abrt-server-info-page':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure abrt-server-info-page is removed
  package:
    name: abrt-server-info-page
    state: absent
  tags:
  - DISA-STIG-OL08-00-040001
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_abrt-server-info-page_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove abrt-server-info-page
#	   from the system, and may remove any packages
#	   that depend on abrt-server-info-page. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "abrt-server-info-page" ; then

    yum remove -y "abrt-server-info-page"

fi

Rule   Uninstall gssproxy Package   [ref]

The gssproxy package can be removed with the following command:
$ sudo yum erase gssproxy
Rationale:
gssproxy is a proxy for GSS API credential handling.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_gssproxy_removed
Identifiers and References

References:  CCI-000381, CCI-000366, SRG-OS-000095-GPOS-00049, SRG-OS-000480-GPOS-00227, OL08-00-040370, SV-248904r780278_rule


Complexity:low
Disruption:low
Strategy:disable
include remove_gssproxy

class remove_gssproxy {
  package { 'gssproxy':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure gssproxy is removed
  package:
    name: gssproxy
    state: absent
  tags:
  - DISA-STIG-OL08-00-040370
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_gssproxy_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove gssproxy
#	   from the system, and may remove any packages
#	   that depend on gssproxy. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "gssproxy" ; then

    yum remove -y "gssproxy"

fi

Rule   Uninstall iprutils Package   [ref]

The iprutils package can be removed with the following command:
$ sudo yum erase iprutils
Rationale:
iprutils provides a suite of utlilities to manage and configure SCSI devices supported by the ipr SCSI storage device driver.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_iprutils_removed
Identifiers and References

References:  CCI-000366, SRG-OS-000095-GPOS-00049, SRG-OS-000480-GPOS-00227, OL08-00-040380, SV-248905r780281_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=iprutils

Complexity:low
Disruption:low
Strategy:disable
include remove_iprutils

class remove_iprutils {
  package { 'iprutils':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure iprutils is removed
  package:
    name: iprutils
    state: absent
  tags:
  - DISA-STIG-OL08-00-040380
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_iprutils_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove iprutils
#	   from the system, and may remove any packages
#	   that depend on iprutils. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "iprutils" ; then

    yum remove -y "iprutils"

fi

Rule   Uninstall krb5-workstation Package   [ref]

The krb5-workstation package can be removed with the following command:
$ sudo yum erase krb5-workstation
Rationale:
Kerberos is a network authentication system. The krb5-workstation package contains the basic Kerberos programs (kinit, klist, kdestroy, kpasswd).
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_krb5-workstation_removed
Identifiers and References

References:  CCI-000803, SRG-OS-000095-GPOS-00049, SRG-OS-000120-GPOS-00061, OL08-00-010162, SV-248546r779204_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=krb5-workstation

Complexity:low
Disruption:low
Strategy:disable
include remove_krb5-workstation

class remove_krb5-workstation {
  package { 'krb5-workstation':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure krb5-workstation is removed
  package:
    name: krb5-workstation
    state: absent
  tags:
  - DISA-STIG-OL08-00-010162
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_krb5-workstation_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove krb5-workstation
#	   from the system, and may remove any packages
#	   that depend on krb5-workstation. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "krb5-workstation" ; then

    yum remove -y "krb5-workstation"

fi

Rule   Uninstall libreport-plugin-logger Package   [ref]

The libreport-plugin-logger package can be removed with the following command:
$ sudo yum erase libreport-plugin-logger
Rationale:
libreport-plugin-logger is a ABRT plugin to report bugs into the Red Hat Support system.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_package_libreport-plugin-logger_removed
Identifiers and References

References:  CCI-000381, SRG-OS-000095-GPOS-00049, OL08-00-040001, SV-248824r780038_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=libreport-plugin-logger

Complexity:low
Disruption:low
Strategy:disable
include remove_libreport-plugin-logger

class remove_libreport-plugin-logger {
  package { 'libreport-plugin-logger':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure libreport-plugin-logger is removed
  package:
    name: libreport-plugin-logger
    state: absent
  tags:
  - DISA-STIG-OL08-00-040001
  - disable_strategy
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - package_libreport-plugin-logger_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove libreport-plugin-logger
#	   from the system, and may remove any packages
#	   that depend on libreport-plugin-logger. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "libreport-plugin-logger" ; then

    yum remove -y "libreport-plugin-logger"

fi

Rule   Uninstall tuned Package   [ref]

The tuned package can be removed with the following command:
$ sudo yum erase tuned
Rationale:
tuned contains a daemon that tunes the system settings dynamically. It does so by monitoring the usage of several system components periodically. Based on that information, components will then be put into lower or higher power savings modes to adapt to the current usage.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_tuned_removed
Identifiers and References

References:  CCI-000366, SRG-OS-000095-GPOS-00049, SRG-OS-000480-GPOS-00227, OL08-00-040390, SV-248906r780284_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=tuned

Complexity:low
Disruption:low
Strategy:disable
include remove_tuned

class remove_tuned {
  package { 'tuned':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure tuned is removed
  package:
    name: tuned
    state: absent
  tags:
  - DISA-STIG-OL08-00-040390
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_tuned_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove tuned
#	   from the system, and may remove any packages
#	   that depend on tuned. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "tuned" ; then

    yum remove -y "tuned"

fi
Group   Updating Software   Group contains 6 rules
[ref]   The yum command line tool is used to install and update software packages. The system also provides a graphical software update tool in the System menu, in the Administration submenu, called Software Update.

Oracle Linux 8 systems contain an installed software catalog called the RPM database, which records metadata of installed packages. Consistently using yum or the graphical Software Update for all software installation allows for insight into the current inventory of installed software on the system.

Rule   Ensure yum Removes Previous Package Versions   [ref]

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.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_clean_components_post_updating
Identifiers and References

References:  18, 20, 4, APO12.01, APO12.02, APO12.03, APO12.04, BAI03.10, DSS05.01, DSS05.02, 3.4.8, CCI-002617, 4.2.3, 4.2.3.12, 4.2.3.7, 4.2.3.9, A.12.6.1, A.14.2.3, A.16.1.3, A.18.2.2, A.18.2.3, SI-2(6), CM-11(a), CM-11(b), CM-6(a), ID.RA-1, PR.IP-12, SRG-OS-000437-GPOS-00194, OL08-00-010440, SV-248595r853773_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010440
  - 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:
  - DISA-STIG-OL08-00-010440
  - 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

# 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

Rule   Ensure gpgcheck Enabled In Main yum Configuration   [ref]

The gpgcheck option controls whether RPM packages' signatures are always checked prior to installation. To configure yum to check package signatures before installing them, ensure the following line appears in /etc/yum.conf in the [main] section:
gpgcheck=1
Rationale:
Changes to any software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor.
Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization.
Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA).
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_ensure_gpgcheck_globally_activated
Identifiers and References

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


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.10.4.1
  - DISA-STIG-OL08-00-010370
  - NIST-800-171-3.4.8
  - NIST-800-53-CM-11(a)
  - NIST-800-53-CM-11(b)
  - NIST-800-53-CM-5(3)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SA-12
  - NIST-800-53-SA-12(10)
  - NIST-800-53-SC-12
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SI-7
  - PCI-DSS-Req-6.2
  - PCI-DSSv4-6.3.3
  - configure_strategy
  - ensure_gpgcheck_globally_activated
  - high_severity
  - low_complexity
  - medium_disruption
  - no_reboot_needed

- name: Ensure GPG check is globally activated
  ini_file:
    dest: /etc/yum.conf
    section: main
    option: gpgcheck
    value: 1
    no_extra_spaces: true
    create: false
  when: '"yum" in ansible_facts.packages'
  tags:
  - CJIS-5.10.4.1
  - DISA-STIG-OL08-00-010370
  - NIST-800-171-3.4.8
  - NIST-800-53-CM-11(a)
  - NIST-800-53-CM-11(b)
  - NIST-800-53-CM-5(3)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SA-12
  - NIST-800-53-SA-12(10)
  - NIST-800-53-SC-12
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SI-7
  - PCI-DSS-Req-6.2
  - PCI-DSSv4-6.3.3
  - configure_strategy
  - ensure_gpgcheck_globally_activated
  - high_severity
  - low_complexity
  - medium_disruption
  - no_reboot_needed

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

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

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

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

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

Rule   Ensure gpgcheck Enabled for Local Packages   [ref]

yum should be configured to verify the signature(s) of local packages prior to installation. To configure yum to verify signatures of local packages, set the localpkg_gpgcheck to 1 in /etc/yum.conf.
Rationale:
Changes to any software components can have significant effects to the overall security of the operating system. This requirement ensures the software has not been tampered and has been provided by a trusted vendor.

Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages
Identifiers and References

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


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

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

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

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

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

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

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

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

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

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

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

To ensure signature checking is not disabled for any repos, remove any lines from files in /etc/yum.repos.d of the form:
gpgcheck=0
Rationale:
Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA)."
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_ensure_gpgcheck_never_disabled
Identifiers and References

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


Complexity:low
Disruption:medium
Strategy:enable
- name: Grep for yum repo section names
  shell: |
    set -o pipefail
    grep -HEr '^\[.+\]' -r /etc/yum.repos.d/
  register: repo_grep_results
  failed_when: repo_grep_results.rc not in [0, 1]
  changed_when: false
  tags:
  - CJIS-5.10.4.1
  - DISA-STIG-OL08-00-010370
  - NIST-800-171-3.4.8
  - NIST-800-53-CM-11(a)
  - NIST-800-53-CM-11(b)
  - NIST-800-53-CM-5(3)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SA-12
  - NIST-800-53-SA-12(10)
  - NIST-800-53-SC-12
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SI-7
  - PCI-DSS-Req-6.2
  - PCI-DSSv4-6.3.3
  - enable_strategy
  - ensure_gpgcheck_never_disabled
  - high_severity
  - low_complexity
  - medium_disruption
  - no_reboot_needed

- name: Set gpgcheck=1 for each yum repo
  ini_file:
    path: '{{ item[0] }}'
    section: '{{ item[1] }}'
    option: gpgcheck
    value: '1'
    no_extra_spaces: true
  loop: '{{ repo_grep_results.stdout | regex_findall( ''(.+\.repo):\[(.+)\]\n?'' )
    }}'
  tags:
  - CJIS-5.10.4.1
  - DISA-STIG-OL08-00-010370
  - NIST-800-171-3.4.8
  - NIST-800-53-CM-11(a)
  - NIST-800-53-CM-11(b)
  - NIST-800-53-CM-5(3)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SA-12
  - NIST-800-53-SA-12(10)
  - NIST-800-53-SC-12
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SI-7
  - PCI-DSS-Req-6.2
  - PCI-DSSv4-6.3.3
  - enable_strategy
  - ensure_gpgcheck_never_disabled
  - high_severity
  - low_complexity
  - medium_disruption
  - no_reboot_needed


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

Rule   Ensure Oracle Linux GPG Key Installed   [ref]

To ensure the system can cryptographically verify base software packages come from Oracle (and to connect to the Unbreakable Linux Network to receive them), the Oracle GPG key must properly be installed. To install the Oracle GPG key, run:
$ sudo uln_register
If the system is not connected to the Internet, then install the Oracle GPG key from trusted media such as the Oracle installation CD-ROM or DVD. Assuming the disc is mounted in /media/cdrom, use the following command as the root user to import it into the keyring:
$ sudo rpm --import /media/cdrom/RPM-GPG-KEY-oracle
Alternatively, the key may be pre-loaded during the Oracle installation. In such cases, the key can be installed by running the following command:
sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle
Rationale:
Changes to software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor. The Oracle GPG key is necessary to cryptographically verify packages are from Oracle.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_ensure_oracle_gpgkey_installed
Identifiers and References

References:  11, 2, 3, 9, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, CCI-001749, 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), CM-11(a), CM-11(b), PR.DS-6, PR.DS-8, PR.IP-1, Req-6.2, OL08-00-010019, 1.2.2, SV-256978r902784_rule


# OL fingerprints below retrieved from: https://linux.oracle.com/security/gpg/#gpg
readonly OL_RELEASE_FINGERPRINT="76FD3DB13AB67410B89DB10E82562EA9AD986DA3"
readonly OL_AUXILIARY_FINGERPRINT=""

FINGERPRINTS_REGEX="${OL_RELEASE_FINGERPRINT}"

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

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

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

# Verify /etc/pki/rpm-gpg directory permissions are safe
if [ "${RPM_GPG_DIR_PERMS}" -le "755" ]
then
  # If they are safe, try to obtain fingerprints from the key file
  # (to ensure there won't be e.g. CRC error)
  
    readarray -t GPG_OUT < <(gpg --show-keys --with-fingerprint --with-colons "$OL_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
    # Filter just hexadecimal fingerprints from gpg's output from
    # processing of a key file
    echo "${GPG_OUT[*]}" | grep -vE "$FINGERPRINTS_REGEX" || {
      # If $ OL_RELEASE_KEY file doesn't contain any keys with unknown fingerprint, import it
      rpm --import "${OL_RELEASE_KEY}"
    }
  fi
fi

Rule   Ensure Software Patches Installed   [ref]

If the system is joined to the ULN or a yum server, run the following command to install updates:
$ sudo yum update
If the system is not configured to use one of these sources, updates (in the form of RPM packages) can be manually downloaded from the ULN and installed using rpm.

NOTE: U.S. Defense systems are required to be patched within 30 days or sooner as local policy dictates.
Warning:  The OVAL feed of Oracle Linux 8 is not a XML file, which may not be understood by all scanners.
Rationale:
Installing software updates is a fundamental mitigation against the exploitation of publicly-known vulnerabilities. If the most recent security patches and updates are not installed, unauthorized users may take advantage of weaknesses in the unpatched software. The lack of prompt attention to patching could result in a system compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_security_patches_up_to_date
Identifiers and References

References:  BP28(R08), 18, 20, 4, 5.10.4.1, APO12.01, APO12.02, APO12.03, APO12.04, BAI03.10, DSS05.01, DSS05.02, CCI-000366, CCI-001227, 4.2.3, 4.2.3.12, 4.2.3.7, 4.2.3.9, A.12.6.1, A.14.2.3, A.16.1.3, A.18.2.2, A.18.2.3, SI-2(5), SI-2(c), CM-6(a), ID.RA-1, PR.IP-12, FMT_MOF_EXT.1, Req-6.2, 6.3.3, SRG-OS-000480-GPOS-00227, OL08-00-010010, SV-248523r779135_rule


Complexity:low
Disruption:high
Reboot:true
Strategy:patch
- name: Security patches are up to date
  package:
    name: '*'
    state: latest
  tags:
  - CJIS-5.10.4.1
  - DISA-STIG-OL08-00-010010
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-2(5)
  - NIST-800-53-SI-2(c)
  - PCI-DSS-Req-6.2
  - PCI-DSSv4-6.3.3
  - high_disruption
  - low_complexity
  - medium_severity
  - patch_strategy
  - reboot_required
  - security_patches_up_to_date
  - skip_ansible_lint

Complexity:low
Disruption:high
Reboot:true
Strategy:patch


yum -y update
Group   Account and Access Control   Group contains 18 groups and 82 rules
[ref]   In traditional Unix security, if an attacker gains shell access to a certain login account, they can perform any action or access any file to which that account has access. Therefore, making it more difficult for unauthorized people to gain shell access to accounts, particularly to privileged accounts, is a necessary part of securing a system. This section introduces mechanisms for restricting access to accounts under Oracle Linux 8.
Group   Warning Banners for System Accesses   Group contains 1 group and 3 rules
[ref]   Each system should expose as little information about itself as possible.

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

Many organizations implement security policies that require a system banner provide notice of the system's ownership, provide warning to unauthorized users, and remind authorized users of their consent to monitoring.
Group   Implement a GUI Warning Banner   Group contains 2 rules

Rule   Enable GNOME3 Login Warning Banner   [ref]

In the default graphical environment, displaying a login warning banner in the GNOME Display Manager's login screen can be enabled on the login screen by setting banner-message-enable to true.

To enable, add or edit banner-message-enable to /etc/dconf/db/local.d/00-security-settings. For example:
[org/gnome/login-screen]
banner-message-enable=true
Once the setting has been added, add a lock to /etc/dconf/db/local.d/locks/00-security-settings-lock to prevent user modification. For example:
/org/gnome/login-screen/banner-message-enable
After the settings have been set, run dconf update. The banner text must also be set.
Rationale:
Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance.

For U.S. Government systems, system use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dconf_gnome_banner_enabled
Identifiers and References

References:  1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, 3.1.9, CCI-000048, CCI-000050, CCI-001384, CCI-001385, CCI-001386, CCI-001387, CCI-001388, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-8(a), AC-8(b), AC-8(c), PR.AC-7, FMT_MOF_EXT.1, SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088, OL08-00-010049, SV-248527r779147_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010049
  - NIST-800-171-3.1.9
  - NIST-800-53-AC-8(a)
  - NIST-800-53-AC-8(b)
  - NIST-800-53-AC-8(c)
  - dconf_gnome_banner_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

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

- name: Dconf Update
  command: dconf update
  when: '"gdm" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010049
  - NIST-800-171-3.1.9
  - NIST-800-53-AC-8(a)
  - NIST-800-53-AC-8(b)
  - NIST-800-53-AC-8(c)
  - dconf_gnome_banner_enabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

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

mkdir -p "${DBDIR}"

# Comment out the configurations in databases different from the target one
if [ "${#SETTINGSFILES[@]}" -ne 0 ]
then
    if grep -q "^\\s*banner-message-enable\\s*=" "${SETTINGSFILES[@]}"
    then
        
        sed -Ei "s/(^\s*)banner-message-enable(\s*=)/#\1banner-message-enable\2/g" "${SETTINGSFILES[@]}"
    fi
fi


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

escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")"
if grep -q "^\\s*banner-message-enable\\s*=" "${DCONFFILE}"
then
        sed -i "s/\\s*banner-message-enable\\s*=\\s*.*/banner-message-enable=${escaped_value}/g" "${DCONFFILE}"
    else
        sed -i "\\|\\[org/gnome/login-screen\\]|a\\banner-message-enable=${escaped_value}" "${DCONFFILE}"
fi

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

mkdir -p "${LOCKSFOLDER}"

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

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

dconf update

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

Rule   Modify the System Login Banner   [ref]

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

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


OR:

I've read & consent to terms in IS user agreem't.
Rationale:
Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance.

System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_banner_etc_issue
Identifiers and References

References:  1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, 3.1.9, CCI-000048, CCI-000050, CCI-001384, CCI-001385, CCI-001386, CCI-001387, CCI-001388, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-8(a), AC-8(c), PR.AC-7, FMT_MOF_EXT.1, SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088, OL08-00-010060, SV-248529r779153_rule


Complexity:low
Disruption:medium
- name: XCCDF Value login_banner_text # promote to variable
  set_fact:
    login_banner_text: !!str ^(You[\s\n]+are[\s\n]+accessing[\s\n]+a[\s\n]+U\.S\.[\s\n]+Government[\s\n]+\(USG\)[\s\n]+Information[\s\n]+System[\s\n]+\(IS\)[\s\n]+that[\s\n]+is[\s\n]+provided[\s\n]+for[\s\n]+USG\-authorized[\s\n]+use[\s\n]+only\.[\s\n]+By[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+\(which[\s\n]+includes[\s\n]+any[\s\n]+device[\s\n]+attached[\s\n]+to[\s\n]+this[\s\n]+IS\),[\s\n]+you[\s\n]+consent[\s\n]+to[\s\n]+the[\s\n]+following[\s\n]+conditions\:(?:[\n]+|(?:\\n)+)\-The[\s\n]+USG[\s\n]+routinely[\s\n]+intercepts[\s\n]+and[\s\n]+monitors[\s\n]+communications[\s\n]+on[\s\n]+this[\s\n]+IS[\s\n]+for[\s\n]+purposes[\s\n]+including,[\s\n]+but[\s\n]+not[\s\n]+limited[\s\n]+to,[\s\n]+penetration[\s\n]+testing,[\s\n]+COMSEC[\s\n]+monitoring,[\s\n]+network[\s\n]+operations[\s\n]+and[\s\n]+defense,[\s\n]+personnel[\s\n]+misconduct[\s\n]+\(PM\),[\s\n]+law[\s\n]+enforcement[\s\n]+\(LE\),[\s\n]+and[\s\n]+counterintelligence[\s\n]+\(CI\)[\s\n]+investigations\.(?:[\n]+|(?:\\n)+)\-At[\s\n]+any[\s\n]+time,[\s\n]+the[\s\n]+USG[\s\n]+may[\s\n]+inspect[\s\n]+and[\s\n]+seize[\s\n]+data[\s\n]+stored[\s\n]+on[\s\n]+this[\s\n]+IS\.(?:[\n]+|(?:\\n)+)\-Communications[\s\n]+using,[\s\n]+or[\s\n]+data[\s\n]+stored[\s\n]+on,[\s\n]+this[\s\n]+IS[\s\n]+are[\s\n]+not[\s\n]+private,[\s\n]+are[\s\n]+subject[\s\n]+to[\s\n]+routine[\s\n]+monitoring,[\s\n]+interception,[\s\n]+and[\s\n]+search,[\s\n]+and[\s\n]+may[\s\n]+be[\s\n]+disclosed[\s\n]+or[\s\n]+used[\s\n]+for[\s\n]+any[\s\n]+USG\-authorized[\s\n]+purpose\.(?:[\n]+|(?:\\n)+)\-This[\s\n]+IS[\s\n]+includes[\s\n]+security[\s\n]+measures[\s\n]+\(e\.g\.,[\s\n]+authentication[\s\n]+and[\s\n]+access[\s\n]+controls\)[\s\n]+to[\s\n]+protect[\s\n]+USG[\s\n]+interests\-\-not[\s\n]+for[\s\n]+your[\s\n]+personal[\s\n]+benefit[\s\n]+or[\s\n]+privacy\.(?:[\n]+|(?:\\n)+)\-Notwithstanding[\s\n]+the[\s\n]+above,[\s\n]+using[\s\n]+this[\s\n]+IS[\s\n]+does[\s\n]+not[\s\n]+constitute[\s\n]+consent[\s\n]+to[\s\n]+PM,[\s\n]+LE[\s\n]+or[\s\n]+CI[\s\n]+investigative[\s\n]+searching[\s\n]+or[\s\n]+monitoring[\s\n]+of[\s\n]+the[\s\n]+content[\s\n]+of[\s\n]+privileged[\s\n]+communications,[\s\n]+or[\s\n]+work[\s\n]+product,[\s\n]+related[\s\n]+to[\s\n]+personal[\s\n]+representation[\s\n]+or[\s\n]+services[\s\n]+by[\s\n]+attorneys,[\s\n]+psychotherapists,[\s\n]+or[\s\n]+clergy,[\s\n]+and[\s\n]+their[\s\n]+assistants\.[\s\n]+Such[\s\n]+communications[\s\n]+and[\s\n]+work[\s\n]+product[\s\n]+are[\s\n]+private[\s\n]+and[\s\n]+confidential\.[\s\n]+See[\s\n]+User[\s\n]+Agreement[\s\n]+for[\s\n]+details\.|I've[\s\n]+read[\s\n]+\&[\s\n]+consent[\s\n]+to[\s\n]+terms[\s\n]+in[\s\n]+IS[\s\n]+user[\s\n]+agreem't\.)$
  tags:
    - always

- name: Modify the System Login Banner - ensure correct banner
  copy:
    dest: /etc/issue
    content: '{{ login_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*\.)\|.*\)$",
      "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)",
      "\n") | regex_replace("\\", "") | wordwrap() }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010060
  - NIST-800-171-3.1.9
  - NIST-800-53-AC-8(a)
  - NIST-800-53-AC-8(c)
  - banner_etc_issue
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy

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

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


# 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
Group   Protect Accounts by Configuring PAM   Group contains 4 groups and 29 rules
[ref]   PAM, or Pluggable Authentication Modules, is a system which implements modular authentication for Linux programs. PAM provides a flexible and configurable architecture for authentication, and it should be configured to minimize exposure to unnecessary risk. This section contains guidance on how to accomplish that.

PAM is implemented as a set of shared objects which are loaded and invoked whenever an application wishes to authenticate a user. Typically, the application must be running as root in order to take advantage of PAM, because PAM's modules often need to be able to access sensitive stores of account information, such as /etc/shadow. Traditional privileged network listeners (e.g. sshd) or SUID programs (e.g. sudo) already meet this requirement. An SUID root application, userhelper, is provided so that programs which are not SUID or privileged themselves can still take advantage of PAM.

PAM looks in the directory /etc/pam.d for application-specific configuration information. For instance, if the program login attempts to authenticate a user, then PAM's libraries follow the instructions in the file /etc/pam.d/login to determine what actions should be taken.

One very important file in /etc/pam.d is /etc/pam.d/system-auth. This file, which is included by many other PAM configuration files, defines 'default' system authentication measures. Modifying this file is a good way to make far-reaching authentication changes, for instance when implementing a centralized authentication service.
Warning:  Be careful when making changes to PAM's configuration files. The syntax for these files is complex, and modifications can have unexpected consequences. The default configurations shipped with applications should be sufficient for most users.
Warning:  Running authconfig or system-config-authentication will re-write the PAM configuration files, destroying any manually made changes and replacing them with a series of system defaults. One reference to the configuration file syntax can be found at https://fossies.org/linux/Linux-PAM-docs/doc/sag/Linux-PAM_SAG.pdf.
Group   Set Lockouts for Failed Password Attempts   Group contains 10 rules
[ref]   The pam_faillock PAM module provides the capability to lock out user accounts after a number of failed login attempts. Its documentation is available in /usr/share/doc/pam-VERSION/txts/README.pam_faillock.

Warning:  Locking out user accounts presents the risk of a denial-of-service attack. The lockout policy must weigh whether the risk of such a denial-of-service attack outweighs the benefits of thwarting password guessing attacks.

Rule   Limit Password Reuse: password-auth   [ref]

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

On systems with newer versions of authselect, the pam_pwhistory PAM module can be enabled via authselect feature:
authselect enable-feature with-pwhistory
Otherwise, it should be enabled using an authselect custom profile.

Newer systems also have the /etc/security/pwhistory.conf file for setting pam_pwhistory module options. This file should be used whenever available. Otherwise, the pam_pwhistory module options can be set in PAM files.

The value for remember option must be equal or greater than 5
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report.
Warning:  Newer versions of authselect contain an authselect feature to easily and properly enable pam_pwhistory.so module. If this feature is not yet available in your system, an authselect custom profile must be used to avoid integrity issues in PAM files. If a custom profile was created and used in the system before this authselect feature was available, the new feature can't be used with this custom profile and the remediation will fail. In this case, the custom profile should be recreated or manually updated.
Rationale:
Preventing re-use of previous passwords helps ensure that a compromised password is not re-used by a user.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth
Identifiers and References

References:  1, 12, 15, 16, 5, 5.6.2.1.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.8, CCI-000200, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(f), IA-5(1)(e), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.5, SRG-OS-000077-GPOS-00045, OL08-00-020220, SV-248698r902821_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020220
  - 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_pwhistory_remember_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
- name: XCCDF Value var_password_pam_remember # promote to variable
  set_fact:
    var_password_pam_remember: !!str 5
  tags:
    - always
- name: XCCDF Value var_password_pam_remember_control_flag # promote to variable
  set_fact:
    var_password_pam_remember_control_flag: !!str required,requisite
  tags:
    - always

- name: 'Limit Password Reuse: password-auth - Check if system relies on authselect
    tool'
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020220
  - 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_pwhistory_remember_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: 'Limit Password Reuse: password-auth - Collect the available authselect features'
  ansible.builtin.command:
    cmd: authselect list-features minimal
  register: result_authselect_available_features
  changed_when: false
  when:
  - '"pam" in ansible_facts.packages'
  - result_authselect_present.stat.exists
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020220
  - 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_pwhistory_remember_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

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

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

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

  - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_enable_feature_cmd is not skipped
    - result_authselect_enable_feature_cmd is success
  when:
  - '"pam" in ansible_facts.packages'
  - result_authselect_present.stat.exists
  - result_authselect_available_features.stdout is search("with-pwhistory")
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020220
  - 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_pwhistory_remember_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present is defined
      - result_authselect_present.stat.exists
      - |-
        (result_pam_module_add is defined and result_pam_module_add.changed)
         or (result_pam_module_edit is defined and result_pam_module_edit.changed)
    when:
    - result_pam_line_present.found is defined
    - result_pam_line_present.found == 0
  when:
  - '"pam" in ansible_facts.packages'
  - |
    (result_authselect_available_features.stdout is defined and result_authselect_available_features.stdout is not search("with-pwhistory")) or result_authselect_available_features is not defined
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020220
  - 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_pwhistory_remember_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: 'Limit Password Reuse: password-auth - Check the presence of /etc/security/pwhistory.conf
    file'
  ansible.builtin.stat:
    path: /etc/security/pwhistory.conf
  register: result_pwhistory_conf_check
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020220
  - 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_pwhistory_remember_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are
          applied'
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - result_pam_option_removal is changed
      when:
      - result_pam_file_present.stat.exists
  when:
  - '"pam" in ansible_facts.packages'
  - result_pwhistory_conf_check.stat.exists
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020220
  - 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_pwhistory_remember_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - (result_pam_remember_add is defined and result_pam_remember_add.changed) or
      (result_pam_remember_edit is defined and result_pam_remember_edit.changed)
  when:
  - '"pam" in ansible_facts.packages'
  - not result_pwhistory_conf_check.stat.exists
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020220
  - 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_pwhistory_remember_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

var_password_pam_remember='5'
var_password_pam_remember_control_flag='required,requisite'


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

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

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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

PWHISTORY_CONF="/etc/security/pwhistory.conf"
if [ -f $PWHISTORY_CONF ]; then
    regex="^\s*remember\s*="
    line="remember = $var_password_pam_remember"
    if ! grep -q $regex $PWHISTORY_CONF; then
        echo $line >> $PWHISTORY_CONF
    else
        sed -i --follow-symlinks 's|^\s*\(remember\s*=\s*\)\(\S\+\)|\1'"$var_password_pam_remember"'|g' $PWHISTORY_CONF
    fi
    if [ -e "/etc/pam.d/password-auth" ] ; then
        PAM_FILE_PATH="/etc/pam.d/password-auth"
        if [ -f /usr/bin/authselect ]; then
            
            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"
                
                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done
                
                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi
        
    if grep -qP '^\s*password\s.*\bpam_pwhistory.so\s.*\bremember\b' "$PAM_FILE_PATH"; then
        sed -i -E --follow-symlinks 's/(.*password.*pam_pwhistory.so.*)\bremember\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
    fi
        if [ -f /usr/bin/authselect ]; then
            
            authselect apply-changes -b
        fi
    else
        echo "/etc/pam.d/password-auth was not found" >&2
    fi
else
    PAM_FILE_PATH="/etc/pam.d/password-auth"
    if [ -f /usr/bin/authselect ]; then
        
        if ! authselect check; then
        echo "
        authselect integrity check failed. Remediation aborted!
        This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
        It is not recommended to manually edit the PAM files when authselect tool is available.
        In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        exit 1
        fi

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

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

Rule   Limit Password Reuse: system-auth   [ref]

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

On systems with newer versions of authselect, the pam_pwhistory PAM module can be enabled via authselect feature:
authselect enable-feature with-pwhistory
Otherwise, it should be enabled using an authselect custom profile.

Newer systems also have the /etc/security/pwhistory.conf file for setting pam_pwhistory module options. This file should be used whenever available. Otherwise, the pam_pwhistory module options can be set in PAM files.

The value for remember option must be equal or greater than 5
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report.
Warning:  Newer versions of authselect contain an authselect feature to easily and properly enable pam_pwhistory.so module. If this feature is not yet available in your system, an authselect custom profile must be used to avoid integrity issues in PAM files.
Rationale:
Preventing re-use of previous passwords helps ensure that a compromised password is not re-used by a user.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth
Identifiers and References

References:  1, 12, 15, 16, 5, 5.6.2.1.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.8, CCI-000200, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(f), IA-5(1)(e), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.5, SRG-OS-000077-GPOS-00045, OL08-00-020221, SV-252661r902824_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020221
  - 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_pwhistory_remember_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
- name: XCCDF Value var_password_pam_remember # promote to variable
  set_fact:
    var_password_pam_remember: !!str 5
  tags:
    - always
- name: XCCDF Value var_password_pam_remember_control_flag # promote to variable
  set_fact:
    var_password_pam_remember_control_flag: !!str required,requisite
  tags:
    - always

- name: 'Limit Password Reuse: system-auth - Check if system relies on authselect
    tool'
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020221
  - 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_pwhistory_remember_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: 'Limit Password Reuse: system-auth - Collect the available authselect features'
  ansible.builtin.command:
    cmd: authselect list-features minimal
  register: result_authselect_available_features
  changed_when: false
  when:
  - '"pam" in ansible_facts.packages'
  - result_authselect_present.stat.exists
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020221
  - 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_pwhistory_remember_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

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

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

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

  - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_enable_feature_cmd is not skipped
    - result_authselect_enable_feature_cmd is success
  when:
  - '"pam" in ansible_facts.packages'
  - result_authselect_present.stat.exists
  - result_authselect_available_features.stdout is search("with-pwhistory")
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020221
  - 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_pwhistory_remember_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present is defined
      - result_authselect_present.stat.exists
      - |-
        (result_pam_module_add is defined and result_pam_module_add.changed)
         or (result_pam_module_edit is defined and result_pam_module_edit.changed)
    when:
    - result_pam_line_present.found is defined
    - result_pam_line_present.found == 0
  when:
  - '"pam" in ansible_facts.packages'
  - |
    (result_authselect_available_features.stdout is defined and result_authselect_available_features.stdout is not search("with-pwhistory")) or result_authselect_available_features is not defined
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020221
  - 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_pwhistory_remember_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: 'Limit Password Reuse: system-auth - Check the presence of /etc/security/pwhistory.conf
    file'
  ansible.builtin.stat:
    path: /etc/security/pwhistory.conf
  register: result_pwhistory_conf_check
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020221
  - 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_pwhistory_remember_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - result_pam_option_removal is changed
      when:
      - result_pam_file_present.stat.exists
  when:
  - '"pam" in ansible_facts.packages'
  - result_pwhistory_conf_check.stat.exists
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020221
  - 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_pwhistory_remember_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - (result_pam_remember_add is defined and result_pam_remember_add.changed) or
      (result_pam_remember_edit is defined and result_pam_remember_edit.changed)
  when:
  - '"pam" in ansible_facts.packages'
  - not result_pwhistory_conf_check.stat.exists
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020221
  - 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_pwhistory_remember_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

var_password_pam_remember='5'
var_password_pam_remember_control_flag='required,requisite'


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

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

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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

PWHISTORY_CONF="/etc/security/pwhistory.conf"
if [ -f $PWHISTORY_CONF ]; then
    regex="^\s*remember\s*="
    line="remember = $var_password_pam_remember"
    if ! grep -q $regex $PWHISTORY_CONF; then
        echo $line >> $PWHISTORY_CONF
    else
        sed -i --follow-symlinks 's|^\s*\(remember\s*=\s*\)\(\S\+\)|\1'"$var_password_pam_remember"'|g' $PWHISTORY_CONF
    fi
    if [ -e "/etc/pam.d/system-auth" ] ; then
        PAM_FILE_PATH="/etc/pam.d/system-auth"
        if [ -f /usr/bin/authselect ]; then
            
            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"
                
                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done
                
                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi
        
    if grep -qP '^\s*password\s.*\bpam_pwhistory.so\s.*\bremember\b' "$PAM_FILE_PATH"; then
        sed -i -E --follow-symlinks 's/(.*password.*pam_pwhistory.so.*)\bremember\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
    fi
        if [ -f /usr/bin/authselect ]; then
            
            authselect apply-changes -b
        fi
    else
        echo "/etc/pam.d/system-auth was not found" >&2
    fi
else
    PAM_FILE_PATH="/etc/pam.d/system-auth"
    if [ -f /usr/bin/authselect ]; then
        
        if ! authselect check; then
        echo "
        authselect integrity check failed. Remediation aborted!
        This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
        It is not recommended to manually edit the PAM files when authselect tool is available.
        In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        exit 1
        fi

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

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

Rule   Account Lockouts Must Be Logged   [ref]

PAM faillock locks an account due to excessive password failures, this event must be logged.
Rationale:
Without auditing of these events it may be harder or impossible to identify what an attacker did after an attack.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_audit
Identifiers and References

References:  CCI-000044, AC-7 (a), SRG-OS-000021-GPOS-00005, OL08-00-020021, SV-248663r853786_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Account Lockouts Must Be Logged - Check if system relies on authselect tool
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  tags:
  - DISA-STIG-OL08-00-020021
  - NIST-800-53-AC-7 (a)
  - accounts_passwords_pam_faillock_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Account Lockouts Must Be Logged - Remediation where authselect tool is present
  block:

  - name: Account Lockouts Must Be Logged - Check integrity of authselect current
      profile
    ansible.builtin.command:
      cmd: authselect check
    register: result_authselect_check_cmd
    changed_when: false
    failed_when: false

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

  - name: Account Lockouts Must Be Logged - Get authselect current features
    ansible.builtin.shell:
      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
    register: result_authselect_features
    changed_when: false
    when:
    - result_authselect_check_cmd is success

  - name: Account Lockouts Must Be Logged - Ensure "with-faillock" feature is enabled
      using authselect tool
    ansible.builtin.command:
      cmd: authselect enable-feature with-faillock
    register: result_authselect_enable_feature_cmd
    when:
    - result_authselect_check_cmd is success
    - result_authselect_features.stdout is not search("with-faillock")

  - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_enable_feature_cmd is not skipped
    - result_authselect_enable_feature_cmd is success
  when: result_authselect_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020021
  - NIST-800-53-AC-7 (a)
  - accounts_passwords_pam_faillock_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Account Lockouts Must Be Logged - Remediation where authselect tool is not
    present
  block:

  - name: Account Lockouts Must Be Logged - Check if pam_faillock.so is already enabled
    ansible.builtin.lineinfile:
      path: /etc/pam.d/system-auth
      regexp: .*auth.*pam_faillock\.so (preauth|authfail)
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_faillock_is_enabled

  - name: Account Lockouts Must Be Logged - Enable pam_faillock.so preauth editing
      PAM files
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      line: auth        required      pam_faillock.so preauth
      insertbefore: ^auth.*sufficient.*pam_unix\.so.*
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_is_enabled.found == 0

  - name: Account Lockouts Must Be Logged - Enable pam_faillock.so authfail editing
      PAM files
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      line: auth        required      pam_faillock.so authfail
      insertbefore: ^auth.*required.*pam_deny\.so.*
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_is_enabled.found == 0

  - name: Account Lockouts Must Be Logged - Enable pam_faillock.so account section
      editing PAM files
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      line: account     required      pam_faillock.so
      insertbefore: ^account.*required.*pam_unix\.so.*
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_is_enabled.found == 0
  when: not result_authselect_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020021
  - NIST-800-53-AC-7 (a)
  - accounts_passwords_pam_faillock_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Account Lockouts Must Be Logged - Check the presence of /etc/security/faillock.conf
    file
  ansible.builtin.stat:
    path: /etc/security/faillock.conf
  register: result_faillock_conf_check
  tags:
  - DISA-STIG-OL08-00-020021
  - NIST-800-53-AC-7 (a)
  - accounts_passwords_pam_faillock_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter
    in /etc/security/faillock.conf
  ansible.builtin.lineinfile:
    path: /etc/security/faillock.conf
    regexp: ^\s*audit
    line: audit
    state: present
  when: result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020021
  - NIST-800-53-AC-7 (a)
  - accounts_passwords_pam_faillock_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter
    not in PAM files
  block:

  - name: Account Lockouts Must Be Logged - Check if /etc/pam.d/system-auth file is
      present
    ansible.builtin.stat:
      path: /etc/pam.d/system-auth
    register: result_pam_file_present

  - name: Account Lockouts Must Be Logged - Check the proper remediation for the system
    block:

    - name: Account Lockouts Must Be Logged - Define the PAM file to be edited as
        a local fact
      ansible.builtin.set_fact:
        pam_file_path: /etc/pam.d/system-auth

    - name: Account Lockouts Must Be Logged - Check if system relies on authselect
        tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present

    - name: Account Lockouts Must Be Logged - Ensure authselect custom profile is
        used if authselect is present
      block:

      - name: Account Lockouts Must Be Logged - Check integrity of authselect current
          profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

      - name: Account Lockouts Must Be Logged - Get authselect current profile
        ansible.builtin.shell:
          cmd: authselect current -r | awk '{ print $1 }'
        register: result_authselect_profile
        changed_when: false
        when:
        - result_authselect_check_cmd is success

      - name: Account Lockouts Must Be Logged - Define the current authselect profile
          as a local fact
        ansible.builtin.set_fact:
          authselect_current_profile: '{{ result_authselect_profile.stdout }}'
          authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
        when:
        - result_authselect_profile is not skipped
        - result_authselect_profile.stdout is match("custom/")

      - name: Account Lockouts Must Be Logged - Define the new authselect custom profile
          as a local fact
        ansible.builtin.set_fact:
          authselect_current_profile: '{{ result_authselect_profile.stdout }}'
          authselect_custom_profile: custom/hardening
        when:
        - result_authselect_profile is not skipped
        - result_authselect_profile.stdout is not match("custom/")

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

      - name: Account Lockouts Must Be Logged - Check if any custom profile with the
          same name was already created
        ansible.builtin.stat:
          path: /etc/authselect/{{ authselect_custom_profile }}
        register: result_authselect_custom_profile_present
        changed_when: false
        when:
        - authselect_current_profile is not match("custom/")

      - name: Account Lockouts Must Be Logged - Create an authselect custom profile
          based on the current profile
        ansible.builtin.command:
          cmd: authselect create-profile hardening -b {{ authselect_current_profile
            }}
        when:
        - result_authselect_check_cmd is success
        - authselect_current_profile is not match("custom/")
        - not result_authselect_custom_profile_present.stat.exists

      - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
        when:
        - result_authselect_check_cmd is success
        - result_authselect_profile is not skipped
        - authselect_current_profile is not match("custom/")
        - authselect_custom_profile is not match(authselect_current_profile)

      - name: Account Lockouts Must Be Logged - Ensure the authselect custom profile
          is selected
        ansible.builtin.command:
          cmd: authselect select {{ authselect_custom_profile }}
        register: result_pam_authselect_select_profile
        when:
        - result_authselect_check_cmd is success
        - result_authselect_profile is not skipped
        - authselect_current_profile is not match("custom/")
        - authselect_custom_profile is not match(authselect_current_profile)

      - name: Account Lockouts Must Be Logged - Restore the authselect features in
          the custom profile
        ansible.builtin.command:
          cmd: authselect enable-feature {{ item }}
        loop: '{{ result_authselect_features.stdout_lines }}'
        register: result_pam_authselect_restore_features
        when:
        - result_authselect_profile is not skipped
        - result_authselect_features is not skipped
        - result_pam_authselect_select_profile is not skipped

      - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
        when:
        - result_authselect_check_cmd is success
        - result_authselect_profile is not skipped
        - result_pam_authselect_restore_features is not skipped

      - name: Account Lockouts Must Be Logged - Change the PAM file to be edited according
          to the custom authselect profile
        ansible.builtin.set_fact:
          pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
            | basename }}
      when:
      - result_authselect_present.stat.exists

    - name: Account Lockouts Must Be Logged - Ensure the "audit" option from "pam_faillock.so"
        is not present in {{ pam_file_path }}
      ansible.builtin.replace:
        dest: '{{ pam_file_path }}'
        regexp: (.*auth.*pam_faillock.so.*)\baudit\b=?[0-9a-zA-Z]*(.*)
        replace: \1\2
      register: result_pam_option_removal

    - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present.stat.exists
      - result_pam_option_removal is changed
    when:
    - result_pam_file_present.stat.exists

  - name: Account Lockouts Must Be Logged - Check if /etc/pam.d/password-auth file
      is present
    ansible.builtin.stat:
      path: /etc/pam.d/password-auth
    register: result_pam_file_present

  - name: Account Lockouts Must Be Logged - Check the proper remediation for the system
    block:

    - name: Account Lockouts Must Be Logged - Define the PAM file to be edited as
        a local fact
      ansible.builtin.set_fact:
        pam_file_path: /etc/pam.d/password-auth

    - name: Account Lockouts Must Be Logged - Check if system relies on authselect
        tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present

    - name: Account Lockouts Must Be Logged - Ensure authselect custom profile is
        used if authselect is present
      block:

      - name: Account Lockouts Must Be Logged - Check integrity of authselect current
          profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

      - name: Account Lockouts Must Be Logged - Get authselect current profile
        ansible.builtin.shell:
          cmd: authselect current -r | awk '{ print $1 }'
        register: result_authselect_profile
        changed_when: false
        when:
        - result_authselect_check_cmd is success

      - name: Account Lockouts Must Be Logged - Define the current authselect profile
          as a local fact
        ansible.builtin.set_fact:
          authselect_current_profile: '{{ result_authselect_profile.stdout }}'
          authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
        when:
        - result_authselect_profile is not skipped
        - result_authselect_profile.stdout is match("custom/")

      - name: Account Lockouts Must Be Logged - Define the new authselect custom profile
          as a local fact
        ansible.builtin.set_fact:
          authselect_current_profile: '{{ result_authselect_profile.stdout }}'
          authselect_custom_profile: custom/hardening
        when:
        - result_authselect_profile is not skipped
        - result_authselect_profile.stdout is not match("custom/")

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

      - name: Account Lockouts Must Be Logged - Check if any custom profile with the
          same name was already created
        ansible.builtin.stat:
          path: /etc/authselect/{{ authselect_custom_profile }}
        register: result_authselect_custom_profile_present
        changed_when: false
        when:
        - authselect_current_profile is not match("custom/")

      - name: Account Lockouts Must Be Logged - Create an authselect custom profile
          based on the current profile
        ansible.builtin.command:
          cmd: authselect create-profile hardening -b {{ authselect_current_profile
            }}
        when:
        - result_authselect_check_cmd is success
        - authselect_current_profile is not match("custom/")
        - not result_authselect_custom_profile_present.stat.exists

      - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
        when:
        - result_authselect_check_cmd is success
        - result_authselect_profile is not skipped
        - authselect_current_profile is not match("custom/")
        - authselect_custom_profile is not match(authselect_current_profile)

      - name: Account Lockouts Must Be Logged - Ensure the authselect custom profile
          is selected
        ansible.builtin.command:
          cmd: authselect select {{ authselect_custom_profile }}
        register: result_pam_authselect_select_profile
        when:
        - result_authselect_check_cmd is success
        - result_authselect_profile is not skipped
        - authselect_current_profile is not match("custom/")
        - authselect_custom_profile is not match(authselect_current_profile)

      - name: Account Lockouts Must Be Logged - Restore the authselect features in
          the custom profile
        ansible.builtin.command:
          cmd: authselect enable-feature {{ item }}
        loop: '{{ result_authselect_features.stdout_lines }}'
        register: result_pam_authselect_restore_features
        when:
        - result_authselect_profile is not skipped
        - result_authselect_features is not skipped
        - result_pam_authselect_select_profile is not skipped

      - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
        when:
        - result_authselect_check_cmd is success
        - result_authselect_profile is not skipped
        - result_pam_authselect_restore_features is not skipped

      - name: Account Lockouts Must Be Logged - Change the PAM file to be edited according
          to the custom authselect profile
        ansible.builtin.set_fact:
          pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
            | basename }}
      when:
      - result_authselect_present.stat.exists

    - name: Account Lockouts Must Be Logged - Ensure the "audit" option from "pam_faillock.so"
        is not present in {{ pam_file_path }}
      ansible.builtin.replace:
        dest: '{{ pam_file_path }}'
        regexp: (.*auth.*pam_faillock.so.*)\baudit\b=?[0-9a-zA-Z]*(.*)
        replace: \1\2
      register: result_pam_option_removal

    - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present.stat.exists
      - result_pam_option_removal is changed
    when:
    - result_pam_file_present.stat.exists
  when: result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020021
  - NIST-800-53-AC-7 (a)
  - accounts_passwords_pam_faillock_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter
    in PAM files
  block:

  - name: Account Lockouts Must Be Logged - Check if pam_faillock.so audit parameter
      is already enabled in pam files
    ansible.builtin.lineinfile:
      path: /etc/pam.d/system-auth
      regexp: .*auth.*pam_faillock\.so (preauth|authfail).*audit
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_faillock_audit_parameter_is_present

  - name: Account Lockouts Must Be Logged - Ensure the inclusion of pam_faillock.so
      preauth audit 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 audit
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_audit_parameter_is_present.found == 0
  when: not result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020021
  - NIST-800-53-AC-7 (a)
  - accounts_passwords_pam_faillock_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy


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

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

fi

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

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    regex="^\s*audit"
    line="audit"
    if ! grep -q $regex $FAILLOCK_CONF; then
        echo $line >> $FAILLOCK_CONF
    fi
    for pam_file in "${AUTH_FILES[@]}"
    do
        if [ -e "$pam_file" ] ; then
            PAM_FILE_PATH="$pam_file"
            if [ -f /usr/bin/authselect ]; then
                
                if ! authselect check; then
                echo "
                authselect integrity check failed. Remediation aborted!
                This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
                It is not recommended to manually edit the PAM files when authselect tool is available.
                In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
                exit 1
                fi

                CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                # If not already in use, a custom profile is created preserving the enabled features.
                if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                    ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                    authselect create-profile hardening -b $CURRENT_PROFILE
                    CURRENT_PROFILE="custom/hardening"
                    
                    authselect apply-changes -b --backup=before-hardening-custom-profile
                    authselect select $CURRENT_PROFILE
                    for feature in $ENABLED_FEATURES; do
                        authselect enable-feature $feature;
                    done
                    
                    authselect apply-changes -b --backup=after-hardening-custom-profile
                fi
                PAM_FILE_NAME=$(basename "$pam_file")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

                authselect apply-changes -b
            fi
            
        if grep -qP '^\s*auth\s.*\bpam_faillock.so\s.*\baudit\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks 's/(.*auth.*pam_faillock.so.*)\baudit\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
        fi
            if [ -f /usr/bin/authselect ]; then
                
                authselect apply-changes -b
            fi
        else
            echo "$pam_file was not found" >&2
        fi
    done
else
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth.*pam_faillock\.so (preauth|authfail).*audit' "$pam_file"; then
            sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ audit/' "$pam_file"
        fi
    done
fi

Rule   Lock Accounts After Failed Password Attempts   [ref]

This rule configures the system to lock out accounts after a number of incorrect login attempts using pam_faillock.so. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
By limiting the number of failed logon attempts, the risk of unauthorized system access via user password guessing, also known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny
Identifiers and References

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


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.3
  - DISA-STIG-OL08-00-020010
  - NIST-800-171-3.1.8
  - NIST-800-53-AC-7(a)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.6
  - PCI-DSSv4-8.3.4
  - accounts_passwords_pam_faillock_deny
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

var_accounts_passwords_pam_faillock_deny='3'


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

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

fi

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

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    regex="^\s*deny\s*="
    line="deny = $var_accounts_passwords_pam_faillock_deny"
    if ! grep -q $regex $FAILLOCK_CONF; then
        echo $line >> $FAILLOCK_CONF
    else
        sed -i --follow-symlinks 's|^\s*\(deny\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_deny"'|g' $FAILLOCK_CONF
    fi
    for pam_file in "${AUTH_FILES[@]}"
    do
        if [ -e "$pam_file" ] ; then
            PAM_FILE_PATH="$pam_file"
            if [ -f /usr/bin/authselect ]; then
                
                if ! authselect check; then
                echo "
                authselect integrity check failed. Remediation aborted!
                This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
                It is not recommended to manually edit the PAM files when authselect tool is available.
                In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
                exit 1
                fi

                CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                # If not already in use, a custom profile is created preserving the enabled features.
                if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                    ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                    authselect create-profile hardening -b $CURRENT_PROFILE
                    CURRENT_PROFILE="custom/hardening"
                    
                    authselect apply-changes -b --backup=before-hardening-custom-profile
                    authselect select $CURRENT_PROFILE
                    for feature in $ENABLED_FEATURES; do
                        authselect enable-feature $feature;
                    done
                    
                    authselect apply-changes -b --backup=after-hardening-custom-profile
                fi
                PAM_FILE_NAME=$(basename "$pam_file")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

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

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

This rule configures the system to lock out the root account after a number of incorrect login attempts using pam_faillock.so. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
By limiting the number of failed logon attempts, the risk of unauthorized system access via user password guessing, also known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root
Identifiers and References

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


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020022
  - NIST-800-53-AC-7(b)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(c)
  - accounts_passwords_pam_faillock_deny_root
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  - name: Configure the root Account for Failed Password Attempts - Ensure the inclusion
      of pam_faillock.so authfail even_deny_root parameter in auth section
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      backrefs: true
      regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
      line: \1required\3 even_deny_root
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_even_deny_root_parameter_is_present.found == 0
  when:
  - '"pam" in ansible_facts.packages'
  - not result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020022
  - 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

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

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

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

fi

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

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    regex="^\s*even_deny_root"
    line="even_deny_root"
    if ! grep -q $regex $FAILLOCK_CONF; then
        echo $line >> $FAILLOCK_CONF
    fi
    for pam_file in "${AUTH_FILES[@]}"
    do
        if [ -e "$pam_file" ] ; then
            PAM_FILE_PATH="$pam_file"
            if [ -f /usr/bin/authselect ]; then
                
                if ! authselect check; then
                echo "
                authselect integrity check failed. Remediation aborted!
                This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
                It is not recommended to manually edit the PAM files when authselect tool is available.
                In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
                exit 1
                fi

                CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                # If not already in use, a custom profile is created preserving the enabled features.
                if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                    ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                    authselect create-profile hardening -b $CURRENT_PROFILE
                    CURRENT_PROFILE="custom/hardening"
                    
                    authselect apply-changes -b --backup=before-hardening-custom-profile
                    authselect select $CURRENT_PROFILE
                    for feature in $ENABLED_FEATURES; do
                        authselect enable-feature $feature;
                    done
                    
                    authselect apply-changes -b --backup=after-hardening-custom-profile
                fi
                PAM_FILE_NAME=$(basename "$pam_file")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

                authselect apply-changes -b
            fi
            
        if grep -qP '^\s*auth\s.*\bpam_faillock.so\s.*\beven_deny_root\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks 's/(.*auth.*pam_faillock.so.*)\beven_deny_root\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
        fi
            if [ -f /usr/bin/authselect ]; then
                
                authselect apply-changes -b
            fi
        else
            echo "$pam_file was not found" >&2
        fi
    done
else
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth.*pam_faillock\.so (preauth|authfail).*even_deny_root' "$pam_file"; then
            sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ even_deny_root/' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ even_deny_root/' "$pam_file"
        fi
    done
fi

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

Rule   Lock Accounts Must Persist   [ref]

This rule ensures that the system lock out accounts using pam_faillock.so persist after system reboot. From "pam_faillock" man pages:
Note that the default directory that "pam_faillock" uses is usually cleared on system
boot so the access will be reenabled after system reboot. If that is undesirable, a different
tally directory must be set with the "dir" option.
pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_dir
Identifiers and References

References:  CCI-000044, CCI-002238, AC-7(b), AC-7(a), AC-7.1(ii), SRG-OS-000021-GPOS-00005, SRG-OS-000329-GPOS-00128, OL08-00-020016, SV-248658r853781_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Check if system relies on authselect tool
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Remediation where authselect tool is present
  block:

  - name: Lock Accounts Must Persist - Check integrity of authselect current profile
    ansible.builtin.command:
      cmd: authselect check
    register: result_authselect_check_cmd
    changed_when: false
    failed_when: false

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

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

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

  - name: Lock Accounts Must Persist - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_enable_feature_cmd is not skipped
    - result_authselect_enable_feature_cmd is success
  when:
  - '"pam" in ansible_facts.packages'
  - result_authselect_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Remediation where authselect tool is not present
  block:

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

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

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

  - name: Lock Accounts Must Persist - Enable pam_faillock.so account section editing
      PAM files
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      line: account     required      pam_faillock.so
      insertbefore: ^account.*required.*pam_unix\.so.*
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_is_enabled.found == 0
  when:
  - '"pam" in ansible_facts.packages'
  - not result_authselect_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
- name: XCCDF Value var_accounts_passwords_pam_faillock_dir # promote to variable
  set_fact:
    var_accounts_passwords_pam_faillock_dir: !!str /var/log/faillock
  tags:
    - always

- name: Lock Accounts Must Persist - Check the presence of /etc/security/faillock.conf
    file
  ansible.builtin.stat:
    path: /etc/security/faillock.conf
  register: result_faillock_conf_check
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Ensure the pam_faillock.so dir parameter in /etc/security/faillock.conf
  ansible.builtin.lineinfile:
    path: /etc/security/faillock.conf
    regexp: ^\s*dir\s*=
    line: dir = {{ var_accounts_passwords_pam_faillock_dir }}
    state: present
  when:
  - '"pam" in ansible_facts.packages'
  - result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Ensure the pam_faillock.so dir parameter not
    in PAM files
  block:

  - name: Lock Accounts Must Persist - Check if /etc/pam.d/system-auth file is present
    ansible.builtin.stat:
      path: /etc/pam.d/system-auth
    register: result_pam_file_present

  - name: Lock Accounts Must Persist - Check the proper remediation for the system
    block:

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

    - name: Lock Accounts Must Persist - Check if system relies on authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present

    - name: Lock Accounts Must Persist - Ensure authselect custom profile is used
        if authselect is present
      block:

      - name: Lock Accounts Must Persist - Check integrity of authselect current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  - name: Lock Accounts Must Persist - Check if /etc/pam.d/password-auth file is present
    ansible.builtin.stat:
      path: /etc/pam.d/password-auth
    register: result_pam_file_present

  - name: Lock Accounts Must Persist - Check the proper remediation for the system
    block:

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

    - name: Lock Accounts Must Persist - Check if system relies on authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present

    - name: Lock Accounts Must Persist - Ensure authselect custom profile is used
        if authselect is present
      block:

      - name: Lock Accounts Must Persist - Check integrity of authselect current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

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

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

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

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

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

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

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

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

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

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

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

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

    - name: Lock Accounts Must Persist - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present.stat.exists
      - result_pam_option_removal is changed
    when:
    - result_pam_file_present.stat.exists
  when:
  - '"pam" in ansible_facts.packages'
  - result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Ensure the pam_faillock.so dir parameter in PAM
    files
  block:

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

  - name: Lock Accounts Must Persist - Ensure the inclusion of pam_faillock.so preauth
      dir 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 dir={{ var_accounts_passwords_pam_faillock_dir }}
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_dir_parameter_is_present.found == 0

  - name: Lock Accounts Must Persist - Ensure the inclusion of pam_faillock.so authfail
      dir 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 dir={{ var_accounts_passwords_pam_faillock_dir }}
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_dir_parameter_is_present.found == 0

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

  - name: Lock Accounts Must Persist - Ensure the desired value for pam_faillock.so
      authfail dir parameter in auth section
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      backrefs: true
      regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(dir)=[0-9]+(.*)
      line: \1required\3\4={{ var_accounts_passwords_pam_faillock_dir }}\5
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_dir_parameter_is_present.found > 0
  when:
  - '"pam" in ansible_facts.packages'
  - not result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Ensure necessary SELinux packages are installed
  ansible.builtin.package:
    name: '{{ item }}'
    state: present
  with_items:
  - python3-libselinux
  - python3-policycoreutils
  - policycoreutils-python-utils
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Create the tally directory if it does not exist
  ansible.builtin.file:
    path: '{{ var_accounts_passwords_pam_faillock_dir }}'
    state: directory
    setype: faillog_t
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Ensure SELinux file context is permanently set
  ansible.builtin.command:
    cmd: semanage fcontext -a -t faillog_t "{{ var_accounts_passwords_pam_faillock_dir
      }}(/.*)?"
  register: result_accounts_passwords_pam_faillock_dir_semanage
  ignore_errors: true
  changed_when:
  - result_accounts_passwords_pam_faillock_dir_semanage.rc == 0
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Lock Accounts Must Persist - Ensure SELinux file context is applied
  ansible.builtin.command:
    cmd: restorecon -R "{{ var_accounts_passwords_pam_faillock_dir }}"
  register: result_accounts_passwords_pam_faillock_dir_restorecon
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020016
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AC-7(b)
  - NIST-800-53-AC-7.1(ii)
  - accounts_passwords_pam_faillock_dir
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_accounts_passwords_pam_faillock_dir='/var/log/faillock'


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

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

fi

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

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    regex="^\s*dir\s*="
    line="dir = $var_accounts_passwords_pam_faillock_dir"
    if ! grep -q $regex $FAILLOCK_CONF; then
        echo $line >> $FAILLOCK_CONF
    else
        sed -i --follow-symlinks 's|^\s*\(dir\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_dir"'|g' $FAILLOCK_CONF
    fi
    for pam_file in "${AUTH_FILES[@]}"
    do
        if [ -e "$pam_file" ] ; then
            PAM_FILE_PATH="$pam_file"
            if [ -f /usr/bin/authselect ]; then
                
                if ! authselect check; then
                echo "
                authselect integrity check failed. Remediation aborted!
                This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
                It is not recommended to manually edit the PAM files when authselect tool is available.
                In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
                exit 1
                fi

                CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                # If not already in use, a custom profile is created preserving the enabled features.
                if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                    ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                    authselect create-profile hardening -b $CURRENT_PROFILE
                    CURRENT_PROFILE="custom/hardening"
                    
                    authselect apply-changes -b --backup=before-hardening-custom-profile
                    authselect select $CURRENT_PROFILE
                    for feature in $ENABLED_FEATURES; do
                        authselect enable-feature $feature;
                    done
                    
                    authselect apply-changes -b --backup=after-hardening-custom-profile
                fi
                PAM_FILE_NAME=$(basename "$pam_file")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

                authselect apply-changes -b
            fi
            
        if grep -qP '^\s*auth\s.*\bpam_faillock.so\s.*\bdir\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks 's/(.*auth.*pam_faillock.so.*)\bdir\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
        fi
            if [ -f /usr/bin/authselect ]; then
                
                authselect apply-changes -b
            fi
        else
            echo "$pam_file was not found" >&2
        fi
    done
else
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth.*pam_faillock\.so (preauth|authfail).*dir' "$pam_file"; then
            sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ dir='"$var_accounts_passwords_pam_faillock_dir"'/' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ dir='"$var_accounts_passwords_pam_faillock_dir"'/' "$pam_file"
        else
            sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*silent.*\)\('"dir"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_dir"'\3/' "$pam_file"
            sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"dir"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_dir"'\3/' "$pam_file"
        fi
    done
fi

if ! rpm -q --quiet "python3-libselinux" ; then
    yum install -y "python3-libselinux"
fi
if ! rpm -q --quiet "python3-policycoreutils" ; then
    yum install -y "python3-policycoreutils"
fi
if ! rpm -q --quiet "policycoreutils-python-utils" ; then
    yum install -y "policycoreutils-python-utils"
fi

mkdir -p "$var_accounts_passwords_pam_faillock_dir"
semanage fcontext -a -t faillog_t "$var_accounts_passwords_pam_faillock_dir(/.*)?"
restorecon -R -v "$var_accounts_passwords_pam_faillock_dir"

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

Rule   Set Interval For Counting Failed Password Attempts   [ref]

Utilizing pam_faillock.so, the fail_interval directive configures the system to lock out an account after a number of incorrect login attempts within a specified time period.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval
Identifiers and References

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


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020012
  - NIST-800-53-AC-7(a)
  - NIST-800-53-CM-6(a)
  - accounts_passwords_pam_faillock_interval
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  - name: Set Interval For Counting Failed Password Attempts - Ensure the desired
      value for pam_faillock.so authfail fail_interval parameter in auth section
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      backrefs: true
      regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(fail_interval)=[0-9]+(.*)
      line: \1required\3\4={{ var_accounts_passwords_pam_faillock_fail_interval }}\5
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_fail_interval_parameter_is_present.found > 0
  when:
  - '"pam" in ansible_facts.packages'
  - not result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020012
  - 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

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

var_accounts_passwords_pam_faillock_fail_interval='900'


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

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

fi

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

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    regex="^\s*fail_interval\s*="
    line="fail_interval = $var_accounts_passwords_pam_faillock_fail_interval"
    if ! grep -q $regex $FAILLOCK_CONF; then
        echo $line >> $FAILLOCK_CONF
    else
        sed -i --follow-symlinks 's|^\s*\(fail_interval\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_fail_interval"'|g' $FAILLOCK_CONF
    fi
    for pam_file in "${AUTH_FILES[@]}"
    do
        if [ -e "$pam_file" ] ; then
            PAM_FILE_PATH="$pam_file"
            if [ -f /usr/bin/authselect ]; then
                
                if ! authselect check; then
                echo "
                authselect integrity check failed. Remediation aborted!
                This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
                It is not recommended to manually edit the PAM files when authselect tool is available.
                In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
                exit 1
                fi

                CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                # If not already in use, a custom profile is created preserving the enabled features.
                if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                    ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                    authselect create-profile hardening -b $CURRENT_PROFILE
                    CURRENT_PROFILE="custom/hardening"
                    
                    authselect apply-changes -b --backup=before-hardening-custom-profile
                    authselect select $CURRENT_PROFILE
                    for feature in $ENABLED_FEATURES; do
                        authselect enable-feature $feature;
                    done
                    
                    authselect apply-changes -b --backup=after-hardening-custom-profile
                fi
                PAM_FILE_NAME=$(basename "$pam_file")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

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

Rule   Do Not Show System Messages When Unsuccessful Logon Attempts Occur   [ref]

This rule ensures the system prevents informative messages from being presented to the user pertaining to logon information after a number of incorrect login attempts using pam_faillock.so. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
The pam_faillock module without the silent option will leak information about the existence or non-existence of a user account in the system because the failures are not recorded for unknown users. The message about the user account being locked is never displayed for non-existing user accounts allowing the adversary to infer that a particular account exists or not on the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_silent
Identifiers and References

References:  CCI-002238, CCI-000044, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, OL08-00-020019, SV-248661r853784_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020019
  - accounts_passwords_pam_faillock_silent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Check
    if system relies on authselect tool
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020019
  - accounts_passwords_pam_faillock_silent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Remediation
    where authselect tool is present
  block:

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Check
      integrity of authselect current profile
    ansible.builtin.command:
      cmd: authselect check
    register: result_authselect_check_cmd
    changed_when: false
    failed_when: false

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

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Get
      authselect current features
    ansible.builtin.shell:
      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
    register: result_authselect_features
    changed_when: false
    when:
    - result_authselect_check_cmd is success

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Ensure
      "with-faillock" feature is enabled using authselect tool
    ansible.builtin.command:
      cmd: authselect enable-feature with-faillock
    register: result_authselect_enable_feature_cmd
    when:
    - result_authselect_check_cmd is success
    - result_authselect_features.stdout is not search("with-faillock")

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Ensure
      authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_enable_feature_cmd is not skipped
    - result_authselect_enable_feature_cmd is success
  when:
  - '"pam" in ansible_facts.packages'
  - result_authselect_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020019
  - accounts_passwords_pam_faillock_silent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Remediation
    where authselect tool is not present
  block:

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Check
      if pam_faillock.so is already enabled
    ansible.builtin.lineinfile:
      path: /etc/pam.d/system-auth
      regexp: .*auth.*pam_faillock\.so (preauth|authfail)
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_faillock_is_enabled

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Enable
      pam_faillock.so preauth editing PAM files
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      line: auth        required      pam_faillock.so preauth
      insertbefore: ^auth.*sufficient.*pam_unix\.so.*
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_is_enabled.found == 0

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Enable
      pam_faillock.so authfail editing PAM files
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      line: auth        required      pam_faillock.so authfail
      insertbefore: ^auth.*required.*pam_deny\.so.*
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_is_enabled.found == 0

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Enable
      pam_faillock.so account section editing PAM files
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      line: account     required      pam_faillock.so
      insertbefore: ^account.*required.*pam_unix\.so.*
      state: present
    loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
    when:
    - result_pam_faillock_is_enabled.found == 0
  when:
  - '"pam" in ansible_facts.packages'
  - not result_authselect_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020019
  - accounts_passwords_pam_faillock_silent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Check
    the presence of /etc/security/faillock.conf file
  ansible.builtin.stat:
    path: /etc/security/faillock.conf
  register: result_faillock_conf_check
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020019
  - accounts_passwords_pam_faillock_silent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Ensure
    the pam_faillock.so silent parameter in /etc/security/faillock.conf
  ansible.builtin.lineinfile:
    path: /etc/security/faillock.conf
    regexp: ^\s*silent
    line: silent
    state: present
  when:
  - '"pam" in ansible_facts.packages'
  - result_faillock_conf_check.stat.exists
  tags:
  - DISA-STIG-OL08-00-020019
  - accounts_passwords_pam_faillock_silent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Ensure
    the pam_faillock.so silent parameter in PAM files
  block:

  - name: Do Not Show System Messages When Unsuccessful Logon Attempts Occur - Ensure
      the inclusion of pam_faillock.so preauth silent parameter in auth section
    ansible.builtin.lineinfile:
      path: '{{ item }}'
      backrefs: true
      regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth(:?(?!silent).)*)
      line: \1required\3 silent
      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:
  - DISA-STIG-OL08-00-020019
  - accounts_passwords_pam_faillock_silent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

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

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

fi

AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    regex="^\s*silent"
    line="silent"
    if ! grep -q $regex $FAILLOCK_CONF; then
        echo $line >> $FAILLOCK_CONF
    fi
else
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth.*pam_faillock\.so\s*preauth.*silent' "$pam_file"; then
            sed -i --follow-symlinks '/^\s*auth.*pam_faillock\.so.*preauth/ s/$/ silent/' "$pam_file"
        fi
    done
fi

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

Rule   Set Lockout Time for Failed Password Attempts   [ref]

This rule configures the system to lock out accounts during a specified time period after a number of incorrect login attempts using pam_faillock.so. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid any errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version. If unlock_time is set to 0, manual intervention by an administrator is required to unlock a user. This should be done using the faillock tool.
Warning:  If the system supports the new /etc/security/faillock.conf file but the pam_faillock.so parameters are defined directly in /etc/pam.d/system-auth and /etc/pam.d/password-auth, the remediation will migrate the unlock_time parameter to /etc/security/faillock.conf to ensure compatibility with authselect tool. The parameters deny and fail_interval, if used, also have to be migrated by their respective remediation.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time
Identifiers and References

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

var_accounts_passwords_pam_faillock_unlock_time='0'


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

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

fi

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

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    regex="^\s*unlock_time\s*="
    line="unlock_time = $var_accounts_passwords_pam_faillock_unlock_time"
    if ! grep -q $regex $FAILLOCK_CONF; then
        echo $line >> $FAILLOCK_CONF
    else
        sed -i --follow-symlinks 's|^\s*\(unlock_time\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_unlock_time"'|g' $FAILLOCK_CONF
    fi
    for pam_file in "${AUTH_FILES[@]}"
    do
        if [ -e "$pam_file" ] ; then
            PAM_FILE_PATH="$pam_file"
            if [ -f /usr/bin/authselect ]; then
                
                if ! authselect check; then
                echo "
                authselect integrity check failed. Remediation aborted!
                This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
                It is not recommended to manually edit the PAM files when authselect tool is available.
                In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
                exit 1
                fi

                CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                # If not already in use, a custom profile is created preserving the enabled features.
                if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                    ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                    authselect create-profile hardening -b $CURRENT_PROFILE
                    CURRENT_PROFILE="custom/hardening"
                    
                    authselect apply-changes -b --backup=before-hardening-custom-profile
                    authselect select $CURRENT_PROFILE
                    for feature in $ENABLED_FEATURES; do
                        authselect enable-feature $feature;
                    done
                    
                    authselect apply-changes -b --backup=after-hardening-custom-profile
                fi
                PAM_FILE_NAME=$(basename "$pam_file")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Set Password Quality Requirements   Group contains 1 group and 13 rules
[ref]   The default pam_pwquality PAM module provides strength checking for passwords. It performs a number of checks, such as making sure passwords are not similar to dictionary words, are of at least a certain length, are not the previous password reversed, and are not simply a change of case from the previous password. It can also require passwords to be in certain character classes. The pam_pwquality module is the preferred way of configuring password requirements.

The man pages pam_pwquality(8) provide information on the capabilities and configuration of each.
Group   Set Password Quality Requirements with pam_pwquality   Group contains 13 rules
[ref]   The pam_pwquality PAM module can be configured to meet requirements for a variety of policies.

For example, to configure pam_pwquality to require at least one uppercase character, lowercase character, digit, and other (special) character, make sure that pam_pwquality exists in /etc/pam.d/system-auth:
password    requisite     pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
If no such line exists, add one as the first line of the password section in /etc/pam.d/system-auth. Next, modify the settings in /etc/security/pwquality.conf to match the following:
difok = 4
minlen = 14
dcredit = -1
ucredit = -1
lcredit = -1
ocredit = -1
maxrepeat = 3
The arguments can be modified to ensure compliance with your organization's security policy. Discussion of each parameter follows.

Rule   Ensure PAM Enforces Password Requirements - Minimum Digit Characters   [ref]

The pam_pwquality module's dcredit parameter controls requirements for usage of digits in a password. When set to a negative number, any password will be required to contain that many digits. When set to a positive number, pam_pwquality will grant +1 additional length credit for each digit. Modify the dcredit setting in /etc/security/pwquality.conf to require the use of a digit in passwords.
Rationale:
Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring digits makes password guessing attacks more difficult by ensuring a larger search space.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit
Identifiers and References

References:  BP28(R18), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000194, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, 8.3.6, 8.3.9, SRG-OS-000071-GPOS-00039, OL08-00-020130, SV-248689r858635_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020130
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_dcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_dcredit # promote to variable
  set_fact:
    var_password_pam_dcredit: !!str -1
  tags:
    - always

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

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

- name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Check
    if /etc/pam.d/system-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020130
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_dcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Check
    the proper remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Define
      the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Check
      if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Ensure
      authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters -
        Check integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters -
        Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters -
        Check if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters -
        Create an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters -
        Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters -
        Change the PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Ensure
      the "dcredit" option from "pam_pwquality.so" is not present in {{ pam_file_path
      }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bdcredit\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Ensure
      authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020130
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_dcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Ensure
    PAM variable dcredit is set accordingly
  ansible.builtin.lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*dcredit
    line: dcredit = {{ var_password_pam_dcredit }}
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020130
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_dcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_dcredit='-1'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bdcredit\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bdcredit\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


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

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

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

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

Rule   Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary Words   [ref]

The pam_pwquality module's dictcheck check if passwords contains dictionary words. When dictcheck is set to 1 passwords will be checked for dictionary words.
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 dictionary words may be more vulnerable to password-guessing attacks.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_dictcheck
Identifiers and References

References:  CCI-000366, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), SRG-OS-000480-GPOS-00225, OL08-00-020300, SV-248711r858649_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020300
  - 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_dictcheck
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_dictcheck # promote to variable
  set_fact:
    var_password_pam_dictcheck: !!str 1
  tags:
    - always

- name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
    Words - Find pwquality.conf.d files
  ansible.builtin.find:
    paths: /etc/security/pwquality.conf.d/
    patterns: '*.conf'
  register: pwquality_conf_d_files
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020300
  - 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_dictcheck
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
    Words - Ensure dictcheck is not set in pwquality.conf.d
  ansible.builtin.lineinfile:
    path: '{{ item.path }}'
    regexp: ^\s*\bdictcheck\b.*
    state: absent
  with_items: '{{ pwquality_conf_d_files.files }}'
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020300
  - 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_dictcheck
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
    Words - Check if /etc/pam.d/system-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020300
  - 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_dictcheck
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
    Words - Check the proper remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
      Words - Define the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
      Words - Check if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
      Words - Ensure authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Check integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Define the current authselect profile as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Define the new authselect custom profile as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: custom/hardening
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is not match("custom/")

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

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Check if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Create an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Ensure the authselect custom profile is selected
      ansible.builtin.command:
        cmd: authselect select {{ authselect_custom_profile }}
      register: result_pam_authselect_select_profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Restore the authselect features in the custom profile
      ansible.builtin.command:
        cmd: authselect enable-feature {{ item }}
      loop: '{{ result_authselect_features.stdout_lines }}'
      register: result_pam_authselect_restore_features
      when:
      - result_authselect_profile is not skipped
      - result_authselect_features is not skipped
      - result_pam_authselect_select_profile is not skipped

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
        Words - Change the PAM file to be edited according to the custom authselect
        profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
      Words - Ensure the "dictcheck" option from "pam_pwquality.so" is not present
      in {{ pam_file_path }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bdictcheck\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
      Words - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020300
  - 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_dictcheck
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Prevent the Use of Dictionary
    Words - Ensure PAM variable dictcheck is set accordingly
  ansible.builtin.lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*dictcheck
    line: dictcheck = {{ var_password_pam_dictcheck }}
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020300
  - 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_dictcheck
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_dictcheck='1'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bdictcheck\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bdictcheck\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


# 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' <<< "^dictcheck")

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

# 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 "^dictcheck\\>" "/etc/security/pwquality.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^dictcheck\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
else
    if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

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

Rule   Ensure PAM Enforces Password Requirements - Minimum Different Characters   [ref]

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.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_difok
Identifiers and References

References:  1, 12, 15, 16, 5, 5.6.2.1.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000195, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(b), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, SRG-OS-000072-GPOS-00040, OL08-00-020170, SV-248693r858643_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020170
  - 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 Enforces Password Requirements - Minimum Different Characters -
    Find pwquality.conf.d files
  ansible.builtin.find:
    paths: /etc/security/pwquality.conf.d/
    patterns: '*.conf'
  register: pwquality_conf_d_files
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020170
  - 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: Ensure PAM Enforces Password Requirements - Minimum Different Characters -
    Ensure difok is not set in pwquality.conf.d
  ansible.builtin.lineinfile:
    path: '{{ item.path }}'
    regexp: ^\s*\bdifok\b.*
    state: absent
  with_items: '{{ pwquality_conf_d_files.files }}'
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020170
  - 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: Ensure PAM Enforces Password Requirements - Minimum Different Characters -
    Check if /etc/pam.d/system-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020170
  - 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: Ensure PAM Enforces Password Requirements - Minimum Different Characters -
    Check the proper remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
      - Define the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
      - Check if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
      - Ensure authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
        - Check integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
        - Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
        - Check if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
        - Create an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
        - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
        - Change the PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
      - Ensure the "difok" option from "pam_pwquality.so" is not present in {{ pam_file_path
      }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bdifok\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Characters
      - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020170
  - 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: Ensure PAM Enforces Password Requirements - Minimum Different Characters -
    Ensure PAM variable difok is set accordingly
  ansible.builtin.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
  - DISA-STIG-OL08-00-020170
  - 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

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_difok='8'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bdifok\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bdifok\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


# 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
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^difok\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
else
    if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

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

Rule   Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters   [ref]

The pam_pwquality module's lcredit parameter controls requirements for usage of lowercase letters in a password. When set to a negative number, any password will be required to contain that many lowercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each lowercase character. Modify the lcredit setting in /etc/security/pwquality.conf to require the use of a lowercase character in passwords.
Rationale:
Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.
Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of 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.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit
Identifiers and References

References:  BP28(R18), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000193, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, 8.3.6, 8.3.9, SRG-OS-000070-GPOS-00038, OL08-00-020120, SV-248688r858633_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020120
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_lcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_lcredit # promote to variable
  set_fact:
    var_password_pam_lcredit: !!str -1
  tags:
    - always

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

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

- name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters -
    Check if /etc/pam.d/system-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020120
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_lcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters -
    Check the proper remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
      - Define the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
      - Check if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
      - Ensure authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
        - Check integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
        - Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
        - Check if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
        - Create an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
        - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
        - Change the PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
      - Ensure the "lcredit" option from "pam_pwquality.so" is not present in {{ pam_file_path
      }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\blcredit\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters
      - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020120
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_lcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters -
    Ensure PAM variable lcredit is set accordingly
  ansible.builtin.lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*lcredit
    line: lcredit = {{ var_password_pam_lcredit }}
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020120
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_lcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_lcredit='-1'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\blcredit\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\blcredit\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


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

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

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

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

Rule   Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating Characters from Same Character Class   [ref]

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 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 a password, the greater the number of possible combinations that need to be tested before the password is compromised.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat
Identifiers and References

References:  1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000195, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, SRG-OS-000072-GPOS-00040, OL08-00-020140, SV-248690r858637_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020140
  - 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 Enforces Password Requirements - Maximum Consecutive Repeating
    Characters from Same Character Class - Find pwquality.conf.d files
  ansible.builtin.find:
    paths: /etc/security/pwquality.conf.d/
    patterns: '*.conf'
  register: pwquality_conf_d_files
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020140
  - 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: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
    Characters from Same Character Class - Ensure maxclassrepeat is not set in pwquality.conf.d
  ansible.builtin.lineinfile:
    path: '{{ item.path }}'
    regexp: ^\s*\bmaxclassrepeat\b.*
    state: absent
  with_items: '{{ pwquality_conf_d_files.files }}'
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020140
  - 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: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
    Characters from Same Character Class - Check if /etc/pam.d/system-auth file is
    present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020140
  - 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: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
    Characters from Same Character Class - Check the proper remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
      Characters from Same Character Class - Define the PAM file to be edited as a
      local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
      Characters from Same Character Class - Check if system relies on authselect
      tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
      Characters from Same Character Class - Ensure authselect custom profile is used
      if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Check integrity of authselect current
        profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Define the current authselect profile
        as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Define the new authselect custom profile
        as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: custom/hardening
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Get authselect current features to
        also enable them in the custom profile
      ansible.builtin.shell:
        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
      register: result_authselect_features
      changed_when: false
      when:
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Check if any custom profile with the
        same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Create an authselect custom profile
        based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Ensure the authselect custom profile
        is selected
      ansible.builtin.command:
        cmd: authselect select {{ authselect_custom_profile }}
      register: result_pam_authselect_select_profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Restore the authselect features in
        the custom profile
      ansible.builtin.command:
        cmd: authselect enable-feature {{ item }}
      loop: '{{ result_authselect_features.stdout_lines }}'
      register: result_pam_authselect_restore_features
      when:
      - result_authselect_profile is not skipped
      - result_authselect_features is not skipped
      - result_pam_authselect_select_profile is not skipped

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
        Characters from Same Character Class - Change the PAM file to be edited according
        to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
      Characters from Same Character Class - Ensure the "maxclassrepeat" option from
      "pam_pwquality.so" is not present in {{ pam_file_path }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bmaxclassrepeat\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
      Characters from Same Character Class - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020140
  - 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: Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating
    Characters from Same Character Class - Ensure PAM variable maxclassrepeat is set
    accordingly
  ansible.builtin.lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*maxclassrepeat
    line: maxclassrepeat = {{ var_password_pam_maxclassrepeat }}
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020140
  - 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

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_maxclassrepeat='4'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bmaxclassrepeat\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bmaxclassrepeat\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


# 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
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^maxclassrepeat\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
else
    if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

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

Rule   Set Password Maximum Consecutive Repeating Characters   [ref]

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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat
Identifiers and References

References:  1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000195, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, SRG-OS-000072-GPOS-00040, OL08-00-020150, SV-248691r858639_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020150
  - 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: Set Password Maximum Consecutive Repeating Characters - Find pwquality.conf.d
    files
  ansible.builtin.find:
    paths: /etc/security/pwquality.conf.d/
    patterns: '*.conf'
  register: pwquality_conf_d_files
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020150
  - 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: Set Password Maximum Consecutive Repeating Characters - Ensure maxrepeat is
    not set in pwquality.conf.d
  ansible.builtin.lineinfile:
    path: '{{ item.path }}'
    regexp: ^\s*\bmaxrepeat\b.*
    state: absent
  with_items: '{{ pwquality_conf_d_files.files }}'
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020150
  - 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: Set Password Maximum Consecutive Repeating Characters - Check if /etc/pam.d/system-auth
    file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020150
  - 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: Set Password Maximum Consecutive Repeating Characters - Check the proper remediation
    for the system
  block:

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

  - name: Set Password Maximum Consecutive Repeating Characters - Check if system
      relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Set Password Maximum Consecutive Repeating Characters - Ensure authselect
      custom profile is used if authselect is present
    block:

    - name: Set Password Maximum Consecutive Repeating Characters - Check integrity
        of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Set Password Maximum Consecutive Repeating Characters - Get authselect
        current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

    - name: Set Password Maximum Consecutive Repeating Characters - Define the current
        authselect profile as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is match("custom/")

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

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

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

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

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

    - name: Set Password Maximum Consecutive Repeating Characters - Ensure the authselect
        custom profile is selected
      ansible.builtin.command:
        cmd: authselect select {{ authselect_custom_profile }}
      register: result_pam_authselect_select_profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Set Password Maximum Consecutive Repeating Characters - Restore the authselect
        features in the custom profile
      ansible.builtin.command:
        cmd: authselect enable-feature {{ item }}
      loop: '{{ result_authselect_features.stdout_lines }}'
      register: result_pam_authselect_restore_features
      when:
      - result_authselect_profile is not skipped
      - result_authselect_features is not skipped
      - result_pam_authselect_select_profile is not skipped

    - name: Set Password Maximum Consecutive Repeating Characters - Ensure authselect
        changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Set Password Maximum Consecutive Repeating Characters - Change the PAM
        file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Set Password Maximum Consecutive Repeating Characters - Ensure the "maxrepeat"
      option from "pam_pwquality.so" is not present in {{ pam_file_path }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bmaxrepeat\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Set Password Maximum Consecutive Repeating Characters - Ensure authselect
      changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020150
  - 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: Set Password Maximum Consecutive Repeating Characters - Ensure PAM variable
    maxrepeat is set accordingly
  ansible.builtin.lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*maxrepeat
    line: maxrepeat = {{ var_password_pam_maxrepeat }}
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020150
  - 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

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_maxrepeat='3'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bmaxrepeat\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bmaxrepeat\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


# 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
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^maxrepeat\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
else
    if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

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

Rule   Ensure PAM Enforces Password Requirements - Minimum Different Categories   [ref]

The pam_pwquality module's minclass parameter controls requirements for usage of different character classes, or types, of character that must exist in a password before it is considered valid. For example, setting this value to three (3) requires that any password must have characters from at least three different categories in order to be approved. The default value is zero (0), meaning there are no required classes. There are four categories available:
* Upper-case characters
* Lower-case characters
* Digits
* Special characters (for example, punctuation)
Modify the minclass setting in /etc/security/pwquality.conf entry to require 4 differing categories of 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 character categories makes password guessing attacks more difficult by ensuring a larger search space.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_minclass
Identifiers and References

References:  1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000195, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, SRG-OS-000072-GPOS-00040, OL08-00-020160, SV-248692r858641_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020160
  - 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_minclass
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_minclass # promote to variable
  set_fact:
    var_password_pam_minclass: !!str 4
  tags:
    - always

- name: Ensure PAM Enforces Password Requirements - Minimum Different Categories -
    Find pwquality.conf.d files
  ansible.builtin.find:
    paths: /etc/security/pwquality.conf.d/
    patterns: '*.conf'
  register: pwquality_conf_d_files
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020160
  - 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_minclass
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Different Categories -
    Ensure minclass is not set in pwquality.conf.d
  ansible.builtin.lineinfile:
    path: '{{ item.path }}'
    regexp: ^\s*\bminclass\b.*
    state: absent
  with_items: '{{ pwquality_conf_d_files.files }}'
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020160
  - 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_minclass
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Different Categories -
    Check if /etc/pam.d/system-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020160
  - 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_minclass
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Different Categories -
    Check the proper remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
      - Define the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
      - Check if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
      - Ensure authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
        - Check integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
        - Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
        - Check if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
        - Create an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
        - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
        - Change the PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
      - Ensure the "minclass" option from "pam_pwquality.so" is not present in {{
      pam_file_path }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bminclass\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Minimum Different Categories
      - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020160
  - 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_minclass
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Different Categories -
    Ensure PAM variable minclass is set accordingly
  ansible.builtin.lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*minclass
    line: minclass = {{ var_password_pam_minclass }}
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020160
  - 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_minclass
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_minclass='4'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bminclass\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bminclass\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


# 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' <<< "^minclass")

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

# 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 "^minclass\\>" "/etc/security/pwquality.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^minclass\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
else
    if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

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

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

The pam_pwquality module's minlen parameter controls requirements for minimum characters required in a password. Add minlen=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 compromise the password.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen
Identifiers and References

References:  BP28(R18), 1, 12, 15, 16, 5, 5.6.2.1.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000205, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, 8.3.6, 8.3.9, SRG-OS-000078-GPOS-00046, OL08-00-020230, SV-248699r858645_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020230
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - 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 Enforces Password Requirements - Minimum Length - Find pwquality.conf.d
    files
  ansible.builtin.find:
    paths: /etc/security/pwquality.conf.d/
    patterns: '*.conf'
  register: pwquality_conf_d_files
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020230
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_minlen
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

- name: Ensure PAM Enforces Password Requirements - Minimum Length - Check if /etc/pam.d/system-auth
    file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020230
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_minlen
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Length - Check the proper
    remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Minimum Length - Define the
      PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Minimum Length - Check if system
      relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Minimum Length - Ensure authselect
      custom profile is used if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Minimum Length - Check integrity
        of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

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

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

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Length - Create an
        authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Length - Change the
        PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Minimum Length - Ensure the
      "minlen" option from "pam_pwquality.so" is not present in {{ pam_file_path }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bminlen\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Minimum Length - Ensure authselect
      changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - CJIS-5.6.2.1.1
  - DISA-STIG-OL08-00-020230
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_minlen
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_minlen='15'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bminlen\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bminlen\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


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

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

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

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

Rule   Ensure PAM Enforces Password Requirements - Minimum Special Characters   [ref]

The pam_pwquality module's ocredit= parameter controls requirements for usage of special (or "other") characters in a password. When set to a negative number, any password will be required to contain that many special characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each special character. Modify the ocredit setting in /etc/security/pwquality.conf to equal -1 to require use of a special character in passwords.
Rationale:
Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring a minimum number of special characters makes password guessing attacks more difficult by ensuring a larger search space.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit
Identifiers and References

References:  BP28(R18), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-001619, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, SRG-OS-000266-GPOS-00101, OL08-00-020280, SV-248709r858647_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020280
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - accounts_password_pam_ocredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_ocredit # promote to variable
  set_fact:
    var_password_pam_ocredit: !!str -1
  tags:
    - always

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

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

- name: Ensure PAM Enforces Password Requirements - Minimum Special Characters - Check
    if /etc/pam.d/system-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020280
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - accounts_password_pam_ocredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Special Characters - Check
    the proper remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters -
      Define the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters -
      Check if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters -
      Ensure authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters
        - Check integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters
        - Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters
        - Check if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters
        - Create an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters
        - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters
        - Change the PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters -
      Ensure the "ocredit" option from "pam_pwquality.so" is not present in {{ pam_file_path
      }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bocredit\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters -
      Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020280
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - accounts_password_pam_ocredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Special Characters - Ensure
    PAM variable ocredit is set accordingly
  ansible.builtin.lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*ocredit
    line: ocredit = {{ var_password_pam_ocredit }}
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020280
  - 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

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_ocredit='-1'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bocredit\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bocredit\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


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

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

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

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

Rule   Ensure PAM password complexity module is enabled in password-auth   [ref]

To enable PAM password complexity in password-auth file: Edit the password section in /etc/pam.d/password-auth to show password requisite pam_pwquality.so.
Rationale:
Enabling PAM password complexity permits to enforce strong passwords and consequently makes the system less prone to dictionary attacks.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_pwquality_password_auth
Identifiers and References

References:  CCI-000366, SRG-OS-000069-GPOS-00037, SRG-OS-000070-GPOS-00038, SRG-OS-000480-GPOS-00227, OL08-00-020100, SV-248686r902809_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020100
  - accounts_password_pam_pwquality_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure PAM password complexity module is enabled in password-auth - Check
    if /etc/pam.d/password-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/password-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020100
  - accounts_password_pam_pwquality_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure PAM password complexity module is enabled in password-auth - Check
    the proper remediation for the system
  block:

  - name: Ensure PAM password complexity module is enabled in password-auth - Define
      the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/password-auth

  - name: Ensure PAM password complexity module is enabled in password-auth - Check
      if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM password complexity module is enabled in password-auth - Ensure
      authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM password complexity module is enabled in password-auth - Check
        integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM password complexity module is enabled in password-auth - Get
        authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

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

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

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

    - name: Ensure PAM password complexity module is enabled in password-auth - Check
        if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM password complexity module is enabled in password-auth - Create
        an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

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

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

    - name: Ensure PAM password complexity module is enabled in password-auth - Change
        the PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM password complexity module is enabled in password-auth - Check
      if expected PAM module line is present in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      regexp: ^\s*password\s+requisite\s+pam_pwquality.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

  - name: Ensure PAM password complexity module is enabled in password-auth - Include
      or update the PAM module line in {{ pam_file_path }}
    block:

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

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

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

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

  - name: Ensure PAM password complexity module is enabled in password-auth - Ensure
      authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - |-
      (result_pam__add is defined and result_pam__add.changed)
       or (result_pam__edit is defined and result_pam__edit.changed)
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020100
  - accounts_password_pam_pwquality_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

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

Rule   Ensure PAM password complexity module is enabled in system-auth   [ref]

To enable PAM password complexity in system-auth file: Edit the password section in /etc/pam.d/system-auth to show password requisite pam_pwquality.so.
Rationale:
Enabling PAM password complexity permits to enforce strong passwords and consequently makes the system less prone to dictionary attacks.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_pwquality_system_auth
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-020101, SV-252657r902812_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020101
  - accounts_password_pam_pwquality_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure PAM password complexity module is enabled in system-auth - Check if
    /etc/pam.d/system-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020101
  - accounts_password_pam_pwquality_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure PAM password complexity module is enabled in system-auth - Check the
    proper remediation for the system
  block:

  - name: Ensure PAM password complexity module is enabled in system-auth - Define
      the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM password complexity module is enabled in system-auth - Check
      if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM password complexity module is enabled in system-auth - Ensure
      authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM password complexity module is enabled in system-auth - Check
        integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM password complexity module is enabled in system-auth - Get
        authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

    - name: Ensure PAM password complexity module is enabled in system-auth - Define
        the current authselect profile as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is match("custom/")

    - name: Ensure PAM password complexity module is enabled in system-auth - Define
        the new authselect custom profile as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: custom/hardening
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is not match("custom/")

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

    - name: Ensure PAM password complexity module is enabled in system-auth - Check
        if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM password complexity module is enabled in system-auth - Create
        an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

    - name: Ensure PAM password complexity module is enabled in system-auth - Restore
        the authselect features in the custom profile
      ansible.builtin.command:
        cmd: authselect enable-feature {{ item }}
      loop: '{{ result_authselect_features.stdout_lines }}'
      register: result_pam_authselect_restore_features
      when:
      - result_authselect_profile is not skipped
      - result_authselect_features is not skipped
      - result_pam_authselect_select_profile is not skipped

    - name: Ensure PAM password complexity module is enabled in system-auth - Ensure
        authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM password complexity module is enabled in system-auth - Change
        the PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM password complexity module is enabled in system-auth - Check
      if expected PAM module line is present in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      regexp: ^\s*password\s+requisite\s+pam_pwquality.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

  - name: Ensure PAM password complexity module is enabled in system-auth - Include
      or update the PAM module line in {{ pam_file_path }}
    block:

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

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

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

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

  - name: Ensure PAM password complexity module is enabled in system-auth - Ensure
      authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - |-
      (result_pam__add is defined and result_pam__add.changed)
       or (result_pam__edit is defined and result_pam__edit.changed)
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020101
  - accounts_password_pam_pwquality_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

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

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

Rule   Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session   [ref]

To configure the number of retry prompts that are permitted per-session: Edit the /etc/security/pwquality.conf to include retry=3, or a lower value if site policy is more restrictive. The DoD requirement is a maximum of 3 prompts per session.
Rationale:
Setting the password retry prompts that are permitted on a per-session basis to a low value requires some software, such as SSH, to re-connect. This can slow down and draw additional attention to some types of password-guessing attacks. Note that this is different from account lockout, which is provided by the pam_faillock module.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_retry
Identifiers and References

References:  1, 11, 12, 15, 16, 3, 5, 9, 5.5.3, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000192, CCI-000366, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, PR.IP-1, FMT_MOF_EXT.1, SRG-OS-000069-GPOS-00037, SRG-OS-000480-GPOS-00227, OL08-00-020104, SV-252660r858600_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.3
  - DISA-STIG-OL08-00-020104
  - NIST-800-53-AC-7(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(4)
  - accounts_password_pam_retry
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
- name: XCCDF Value var_password_pam_retry # promote to variable
  set_fact:
    var_password_pam_retry: !!str 3
  tags:
    - always

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

var_password_pam_retry='3'


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

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

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^retry\\>" "/etc/security/pwquality.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^retry\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
else
    if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi
	
		if [ -e "/etc/pam.d/password-auth" ] ; then
    PAM_FILE_PATH="/etc/pam.d/password-auth"
    if [ -f /usr/bin/authselect ]; then
        
        if ! authselect check; then
        echo "
        authselect integrity check failed. Remediation aborted!
        This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
        It is not recommended to manually edit the PAM files when authselect tool is available.
        In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        exit 1
        fi

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s+'".*"'\s+pam_pwquality.so\s.*\bretry\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*'".*"'.*pam_pwquality.so.*)\sretry=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/password-auth was not found" >&2
fi
	
		if [ -e "/etc/pam.d/system-auth" ] ; then
    PAM_FILE_PATH="/etc/pam.d/system-auth"
    if [ -f /usr/bin/authselect ]; then
        
        if ! authselect check; then
        echo "
        authselect integrity check failed. Remediation aborted!
        This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
        It is not recommended to manually edit the PAM files when authselect tool is available.
        In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        exit 1
        fi

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s+'".*"'\s+pam_pwquality.so\s.*\bretry\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*'".*"'.*pam_pwquality.so.*)\sretry=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi

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

Rule   Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters   [ref]

The pam_pwquality module's ucredit= parameter controls requirements for usage of uppercase letters in a password. When set to a negative number, any password will be required to contain that many uppercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each uppercase character. Modify the ucredit setting in /etc/security/pwquality.conf to require the use of an uppercase character in passwords.
Rationale:
Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit
Identifiers and References

References:  BP28(R18), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000192, CCI-000193, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, 8.3.6, 8.3.9, SRG-OS-000069-GPOS-00037, SRG-OS-000070-GPOS-00038, OL08-00-020110, SV-248687r858631_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020110
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_ucredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_ucredit # promote to variable
  set_fact:
    var_password_pam_ucredit: !!str -1
  tags:
    - always

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

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

- name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters -
    Check if /etc/pam.d/system-auth file is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020110
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_ucredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters -
    Check the proper remediation for the system
  block:

  - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
      - Define the PAM file to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
      - Check if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
      - Ensure authselect custom profile is used if authselect is present
    block:

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
        - Check integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
        - Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
        - Check if any custom profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
        - Create an authselect custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

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

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

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

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
        - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
        - Change the PAM file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
      - Ensure the "ucredit" option from "pam_pwquality.so" is not present in {{ pam_file_path
      }}
    ansible.builtin.replace:
      dest: '{{ pam_file_path }}'
      regexp: (.*password.*pam_pwquality.so.*)\bucredit\b=?[0-9a-zA-Z]*(.*)
      replace: \1\2
    register: result_pam_option_removal

  - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters
      - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - result_pam_option_removal is changed
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020110
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_ucredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters -
    Ensure PAM variable ucredit is set accordingly
  ansible.builtin.lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*ucredit
    line: ucredit = {{ var_password_pam_ucredit }}
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020110
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - accounts_password_pam_ucredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_ucredit='-1'



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



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

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    
if grep -qP '^\s*password\s.*\bpam_pwquality.so\s.*\bucredit\b' "$PAM_FILE_PATH"; then
    sed -i -E --follow-symlinks 's/(.*password.*pam_pwquality.so.*)\bucredit\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi


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

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

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

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Set Password Hashing Algorithm   Group contains 4 rules
[ref]   The system's default algorithm for storing password hashes in /etc/shadow is SHA-512. This can be configured in several locations.

Rule   Set Password Hashing Algorithm in /etc/login.defs   [ref]

In /etc/login.defs, add or correct the following line to ensure the system will use SHA512 as the hashing algorithm:
ENCRYPT_METHOD SHA512
Rationale:
Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text.

Using a stronger hashing algorithm makes password cracking attacks more difficult.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_set_password_hashing_algorithm_logindefs
Identifiers and References

References:  BP28(R32), 1, 12, 15, 16, 5, 5.6.2.2, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.13.11, CCI-000196, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0418, 1055, 1402, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(c), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.1, 8.3.2, SRG-OS-000073-GPOS-00041, OL08-00-010110, SV-248533r877397_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.2
  - DISA-STIG-OL08-00-010110
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - PCI-DSSv4-8.3.2
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - set_password_hashing_algorithm_logindefs
- name: XCCDF Value var_password_hashing_algorithm # promote to variable
  set_fact:
    var_password_hashing_algorithm: !!str SHA512
  tags:
    - always

- name: Set Password Hashing Algorithm in /etc/login.defs
  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
  - DISA-STIG-OL08-00-010110
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - PCI-DSSv4-8.3.2
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - set_password_hashing_algorithm_logindefs

# 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

Rule   Set PAM''s Password Hashing Algorithm - password-auth   [ref]

The PAM system service can be configured to only store encrypted representations of passwords. In /etc/pam.d/password-auth, the password section of the file controls which PAM modules 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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_set_password_hashing_algorithm_passwordauth
Identifiers and References

References:  BP28(R32), 1, 12, 15, 16, 5, 5.6.2.2, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.13.11, CCI-000196, CCI-000803, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0418, 1055, 1402, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(c), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.1, SRG-OS-000073-GPOS-00041, SRG-OS-000120-GPOS-00061, OL08-00-010160, SV-248544r818611_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.2
  - DISA-STIG-OL08-00-010160
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - set_password_hashing_algorithm_passwordauth

- name: Set PAM's Password Hashing Algorithm - password-auth - Check if /etc/pam.d/password-auth
    file is present
  ansible.builtin.stat:
    path: /etc/pam.d/password-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.2
  - DISA-STIG-OL08-00-010160
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - set_password_hashing_algorithm_passwordauth

- name: Set PAM's Password Hashing Algorithm - password-auth - Check the proper remediation
    for the system
  block:

  - name: Set PAM's Password Hashing Algorithm - password-auth - Define the PAM file
      to be edited as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/password-auth

  - name: Set PAM's Password Hashing Algorithm - password-auth - Check if system relies
      on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect
      custom profile is used if authselect is present
    block:

    - name: Set PAM's Password Hashing Algorithm - password-auth - Check integrity
        of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

    - name: Set PAM's Password Hashing Algorithm - password-auth - Informative message
        based on the authselect integrity check result
      ansible.builtin.assert:
        that:
        - result_authselect_check_cmd.rc == 0
        fail_msg:
        - authselect integrity check failed. Remediation aborted!
        - This remediation could not be applied because an authselect profile was
          not selected or the selected profile is not intact.
        - It is not recommended to manually edit the PAM files when authselect tool
          is available.
        - In cases where the default authselect profile does not cover a specific
          demand, a custom authselect profile is recommended.
        success_msg:
        - authselect integrity check passed

    - name: Set PAM's Password Hashing Algorithm - password-auth - Get authselect
        current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

    - name: Set PAM's Password Hashing Algorithm - password-auth - Define the current
        authselect profile as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is match("custom/")

    - name: Set PAM's Password Hashing Algorithm - password-auth - Define the new
        authselect custom profile as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: custom/hardening
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is not match("custom/")

    - name: Set PAM's Password Hashing Algorithm - password-auth - Get authselect
        current features to also enable them in the custom profile
      ansible.builtin.shell:
        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
      register: result_authselect_features
      changed_when: false
      when:
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")

    - name: Set PAM's Password Hashing Algorithm - password-auth - Check if any custom
        profile with the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Set PAM's Password Hashing Algorithm - password-auth - Create an authselect
        custom profile based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

    - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect
        changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the authselect
        custom profile is selected
      ansible.builtin.command:
        cmd: authselect select {{ authselect_custom_profile }}
      register: result_pam_authselect_select_profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Set PAM's Password Hashing Algorithm - password-auth - Restore the authselect
        features in the custom profile
      ansible.builtin.command:
        cmd: authselect enable-feature {{ item }}
      loop: '{{ result_authselect_features.stdout_lines }}'
      register: result_pam_authselect_restore_features
      when:
      - result_authselect_profile is not skipped
      - result_authselect_features is not skipped
      - result_pam_authselect_select_profile is not skipped

    - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect
        changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Set PAM's Password Hashing Algorithm - password-auth - Change the PAM
        file to be edited according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Set PAM's Password Hashing Algorithm - password-auth - Check if expected
      PAM module line is present in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      regexp: ^\s*password\s+sufficient\s+pam_unix.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

  - name: Set PAM's Password Hashing Algorithm - password-auth - Include or update
      the PAM module line in {{ pam_file_path }}
    block:

    - name: Set PAM's Password Hashing Algorithm - password-auth - Check if required
        PAM module line is present in {{ pam_file_path }} with different control
      ansible.builtin.lineinfile:
        path: '{{ pam_file_path }}'
        regexp: ^\s*password\s+.*\s+pam_unix.so\s*
        state: absent
      check_mode: true
      changed_when: false
      register: result_pam_line_other_control_present

    - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the correct
        control for the required PAM module line in {{ pam_file_path }}
      ansible.builtin.replace:
        dest: '{{ pam_file_path }}'
        regexp: ^(\s*password\s+).*(\bpam_unix.so.*)
        replace: \1sufficient \2
      register: result_pam_module_edit
      when:
      - result_pam_line_other_control_present.found == 1

    - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the required
        PAM module line is included in {{ pam_file_path }}
      ansible.builtin.lineinfile:
        dest: '{{ pam_file_path }}'
        line: password    sufficient    pam_unix.so
      register: result_pam_module_add
      when:
      - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
        > 1

    - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect
        changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present is defined
      - result_authselect_present.stat.exists
      - |-
        (result_pam_module_add is defined and result_pam_module_add.changed)
         or (result_pam_module_edit is defined and result_pam_module_edit.changed)
    when:
    - result_pam_line_present.found is defined
    - result_pam_line_present.found == 0

  - name: Set PAM's Password Hashing Algorithm - password-auth - Check if the required
      PAM module option is present in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      regexp: ^\s*password\s+sufficient\s+pam_unix.so\s*.*\ssha512\b
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_module_sha512_option_present

  - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure the "sha512"
      PAM option for "pam_unix.so" is included in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      backrefs: true
      regexp: ^(\s*password\s+sufficient\s+pam_unix.so.*)
      line: \1 sha512
      state: present
    register: result_pam_sha512_add
    when:
    - result_pam_module_sha512_option_present.found == 0

  - name: Set PAM's Password Hashing Algorithm - password-auth - Ensure authselect
      changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - |-
      (result_pam_sha512_add is defined and result_pam_sha512_add.changed)
       or (result_pam_sha512_edit is defined and result_pam_sha512_edit.changed)
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - CJIS-5.6.2.2
  - DISA-STIG-OL08-00-010160
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - set_password_hashing_algorithm_passwordauth

# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

if [ -e "/etc/pam.d/password-auth" ] ; then
    PAM_FILE_PATH="/etc/pam.d/password-auth"
    if [ -f /usr/bin/authselect ]; then
        
        if ! authselect check; then
        echo "
        authselect integrity check failed. Remediation aborted!
        This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
        It is not recommended to manually edit the PAM files when authselect tool is available.
        In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        exit 1
        fi

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*' "$PAM_FILE_PATH"; then
            # Line matching group + control + module was not found. Check group + module.
            if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                # The control is updated only if one single line matches.
                sed -i -E --follow-symlinks 's/^(\s*password\s+).*(\bpam_unix.so.*)/\1'"sufficient"' \2/' "$PAM_FILE_PATH"
            else
                echo 'password    '"sufficient"'    pam_unix.so' >> "$PAM_FILE_PATH"
            fi
        fi
        # Check the option
        if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*\ssha512\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks '/\s*password\s+'"sufficient"'\s+pam_unix.so.*/ s/$/ sha512/' "$PAM_FILE_PATH"
        fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/password-auth was not found" >&2
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Set PAM''s Password Hashing Algorithm   [ref]

The PAM system service can be configured to only store encrypted representations of passwords. In "/etc/pam.d/system-auth", the password section of the file controls which PAM modules 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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_set_password_hashing_algorithm_systemauth
Identifiers and References

References:  BP28(R32), 1, 12, 15, 16, 5, 5.6.2.2, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.13.11, CCI-000196, CCI-000803, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0418, 1055, 1402, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(c), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.1, 8.3.2, SRG-OS-000073-GPOS-00041, SRG-OS-000120-GPOS-00061, OL08-00-010159, SV-248543r818608_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.2
  - DISA-STIG-OL08-00-010159
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - PCI-DSSv4-8.3.2
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - set_password_hashing_algorithm_systemauth

- name: Set PAM's Password Hashing Algorithm - Check if /etc/pam.d/system-auth file
    is present
  ansible.builtin.stat:
    path: /etc/pam.d/system-auth
  register: result_pam_file_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.6.2.2
  - DISA-STIG-OL08-00-010159
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - PCI-DSSv4-8.3.2
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - set_password_hashing_algorithm_systemauth

- name: Set PAM's Password Hashing Algorithm - Check the proper remediation for the
    system
  block:

  - name: Set PAM's Password Hashing Algorithm - Define the PAM file to be edited
      as a local fact
    ansible.builtin.set_fact:
      pam_file_path: /etc/pam.d/system-auth

  - name: Set PAM's Password Hashing Algorithm - Check if system relies on authselect
      tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Set PAM's Password Hashing Algorithm - Ensure authselect custom profile
      is used if authselect is present
    block:

    - name: Set PAM's Password Hashing Algorithm - Check integrity of authselect current
        profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      failed_when: false

    - name: Set PAM's Password Hashing Algorithm - Informative message based on the
        authselect integrity check result
      ansible.builtin.assert:
        that:
        - result_authselect_check_cmd.rc == 0
        fail_msg:
        - authselect integrity check failed. Remediation aborted!
        - This remediation could not be applied because an authselect profile was
          not selected or the selected profile is not intact.
        - It is not recommended to manually edit the PAM files when authselect tool
          is available.
        - In cases where the default authselect profile does not cover a specific
          demand, a custom authselect profile is recommended.
        success_msg:
        - authselect integrity check passed

    - name: Set PAM's Password Hashing Algorithm - Get authselect current profile
      ansible.builtin.shell:
        cmd: authselect current -r | awk '{ print $1 }'
      register: result_authselect_profile
      changed_when: false
      when:
      - result_authselect_check_cmd is success

    - name: Set PAM's Password Hashing Algorithm - Define the current authselect profile
        as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is match("custom/")

    - name: Set PAM's Password Hashing Algorithm - Define the new authselect custom
        profile as a local fact
      ansible.builtin.set_fact:
        authselect_current_profile: '{{ result_authselect_profile.stdout }}'
        authselect_custom_profile: custom/hardening
      when:
      - result_authselect_profile is not skipped
      - result_authselect_profile.stdout is not match("custom/")

    - name: Set PAM's Password Hashing Algorithm - Get authselect current features
        to also enable them in the custom profile
      ansible.builtin.shell:
        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
      register: result_authselect_features
      changed_when: false
      when:
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")

    - name: Set PAM's Password Hashing Algorithm - Check if any custom profile with
        the same name was already created
      ansible.builtin.stat:
        path: /etc/authselect/{{ authselect_custom_profile }}
      register: result_authselect_custom_profile_present
      changed_when: false
      when:
      - authselect_current_profile is not match("custom/")

    - name: Set PAM's Password Hashing Algorithm - Create an authselect custom profile
        based on the current profile
      ansible.builtin.command:
        cmd: authselect create-profile hardening -b {{ authselect_current_profile
          }}
      when:
      - result_authselect_check_cmd is success
      - authselect_current_profile is not match("custom/")
      - not result_authselect_custom_profile_present.stat.exists

    - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Set PAM's Password Hashing Algorithm - Ensure the authselect custom profile
        is selected
      ansible.builtin.command:
        cmd: authselect select {{ authselect_custom_profile }}
      register: result_pam_authselect_select_profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - authselect_current_profile is not match("custom/")
      - authselect_custom_profile is not match(authselect_current_profile)

    - name: Set PAM's Password Hashing Algorithm - Restore the authselect features
        in the custom profile
      ansible.builtin.command:
        cmd: authselect enable-feature {{ item }}
      loop: '{{ result_authselect_features.stdout_lines }}'
      register: result_pam_authselect_restore_features
      when:
      - result_authselect_profile is not skipped
      - result_authselect_features is not skipped
      - result_pam_authselect_select_profile is not skipped

    - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
      when:
      - result_authselect_check_cmd is success
      - result_authselect_profile is not skipped
      - result_pam_authselect_restore_features is not skipped

    - name: Set PAM's Password Hashing Algorithm - Change the PAM file to be edited
        according to the custom authselect profile
      ansible.builtin.set_fact:
        pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
          | basename }}
    when:
    - result_authselect_present.stat.exists

  - name: Set PAM's Password Hashing Algorithm - Check if expected PAM module line
      is present in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      regexp: ^\s*password\s+sufficient\s+pam_unix.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

  - name: Set PAM's Password Hashing Algorithm - Include or update the PAM module
      line in {{ pam_file_path }}
    block:

    - name: Set PAM's Password Hashing Algorithm - Check if required PAM module line
        is present in {{ pam_file_path }} with different control
      ansible.builtin.lineinfile:
        path: '{{ pam_file_path }}'
        regexp: ^\s*password\s+.*\s+pam_unix.so\s*
        state: absent
      check_mode: true
      changed_when: false
      register: result_pam_line_other_control_present

    - name: Set PAM's Password Hashing Algorithm - Ensure the correct control for
        the required PAM module line in {{ pam_file_path }}
      ansible.builtin.replace:
        dest: '{{ pam_file_path }}'
        regexp: ^(\s*password\s+).*(\bpam_unix.so.*)
        replace: \1sufficient \2
      register: result_pam_module_edit
      when:
      - result_pam_line_other_control_present.found == 1

    - name: Set PAM's Password Hashing Algorithm - Ensure the required PAM module
        line is included in {{ pam_file_path }}
      ansible.builtin.lineinfile:
        dest: '{{ pam_file_path }}'
        line: password    sufficient    pam_unix.so
      register: result_pam_module_add
      when:
      - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
        > 1

    - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present is defined
      - result_authselect_present.stat.exists
      - |-
        (result_pam_module_add is defined and result_pam_module_add.changed)
         or (result_pam_module_edit is defined and result_pam_module_edit.changed)
    when:
    - result_pam_line_present.found is defined
    - result_pam_line_present.found == 0

  - name: Set PAM's Password Hashing Algorithm - Check if the required PAM module
      option is present in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      regexp: ^\s*password\s+sufficient\s+pam_unix.so\s*.*\ssha512\b
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_module_sha512_option_present

  - name: Set PAM's Password Hashing Algorithm - Ensure the "sha512" PAM option for
      "pam_unix.so" is included in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      backrefs: true
      regexp: ^(\s*password\s+sufficient\s+pam_unix.so.*)
      line: \1 sha512
      state: present
    register: result_pam_sha512_add
    when:
    - result_pam_module_sha512_option_present.found == 0

  - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - |-
      (result_pam_sha512_add is defined and result_pam_sha512_add.changed)
       or (result_pam_sha512_edit is defined and result_pam_sha512_edit.changed)
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - CJIS-5.6.2.2
  - DISA-STIG-OL08-00-010159
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - PCI-DSSv4-8.3.2
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - set_password_hashing_algorithm_systemauth

# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

if [ -e "/etc/pam.d/system-auth" ] ; then
    PAM_FILE_PATH="/etc/pam.d/system-auth"
    if [ -f /usr/bin/authselect ]; then
        
        if ! authselect check; then
        echo "
        authselect integrity check failed. Remediation aborted!
        This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
        It is not recommended to manually edit the PAM files when authselect tool is available.
        In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        exit 1
        fi

        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # If not already in use, a custom profile is created preserving the enabled features.
        if [[ ! $CURRENT_PROFILE == custom/* ]]; then
            ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
            authselect create-profile hardening -b $CURRENT_PROFILE
            CURRENT_PROFILE="custom/hardening"
            
            authselect apply-changes -b --backup=before-hardening-custom-profile
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
            
            authselect apply-changes -b --backup=after-hardening-custom-profile
        fi
        PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
        PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

        authselect apply-changes -b
    fi
    if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*' "$PAM_FILE_PATH"; then
            # Line matching group + control + module was not found. Check group + module.
            if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                # The control is updated only if one single line matches.
                sed -i -E --follow-symlinks 's/^(\s*password\s+).*(\bpam_unix.so.*)/\1'"sufficient"' \2/' "$PAM_FILE_PATH"
            else
                echo 'password    '"sufficient"'    pam_unix.so' >> "$PAM_FILE_PATH"
            fi
        fi
        # Check the option
        if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*\ssha512\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks '/\s*password\s+'"sufficient"'\s+pam_unix.so.*/ s/$/ sha512/' "$PAM_FILE_PATH"
        fi
    if [ -f /usr/bin/authselect ]; then
        
        authselect apply-changes -b
    fi
else
    echo "/etc/pam.d/system-auth was not found" >&2
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Set Password Hashing Rounds in /etc/login.defs   [ref]

In /etc/login.defs, ensure SHA_CRYPT_MIN_ROUNDS and SHA_CRYPT_MAX_ROUNDS has the minimum value of 5000. For example:
SHA_CRYPT_MIN_ROUNDS 5000
SHA_CRYPT_MAX_ROUNDS 5000
Notice that if neither are set, they already have the default value of 5000. If either is set, they must have the minimum value of 5000.
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 more hashing rounds makes password cracking attacks more difficult.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_set_password_hashing_min_rounds_logindefs
Identifiers and References

References:  CCI-000196, CCI-000803, SRG-OS-000073-GPOS-00041, SRG-OS-000120-GPOS-00061, OL08-00-010130, SV-248535r880546_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Ensure SHA_CRYPT_MIN_ROUNDS has minimum value of 5000
  replace:
    path: /etc/login.defs
    regexp: (^\s*SHA_CRYPT_MIN_ROUNDS\s+)(?!(?:[5-9]\d{3,}|\d{5,}))\S*(\s*$)
    replace: \g<1>5000\g<2>
    backup: false
  tags:
  - DISA-STIG-OL08-00-010130
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - set_password_hashing_min_rounds_logindefs

- name: Check to see if SHA_CRYPT_MIN_ROUNDS is explicitly configured
  shell: |
    set -o pipefail
    grep -e '^\s*SHA_CRYPT_MIN_ROUNDS\s\+' /etc/login.defs || true
  register: check_sha_crypt_min_rounds_result
  tags:
  - DISA-STIG-OL08-00-010130
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - set_password_hashing_min_rounds_logindefs

- name: Ensure SHA_CRYPT_MAX_ROUNDS has minimum value of 5000
  replace:
    path: /etc/login.defs
    regexp: (^\s*SHA_CRYPT_MAX_ROUNDS\s+)(?!(?:[5-9]\d{3,}|\d{5,}))\S*(\s*$)
    replace: \g<1>5000\g<2>
    backup: false
  when: '"SHA_CRYPT_MIN_ROUNDS" not in check_sha_crypt_min_rounds_result.stdout'
  tags:
  - DISA-STIG-OL08-00-010130
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - set_password_hashing_min_rounds_logindefs


if [ -e "/etc/login.defs" ] ; then
    
    LC_ALL=C sed -i "/^\s*SHA_CRYPT_MIN_ROUNDS\s*/Id" "/etc/login.defs"
else
    printf '%s\n' "Path '/etc/login.defs' wasn't found on this system. Refusing to continue." >&2
    return 1
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/login.defs"

cp "/etc/login.defs" "/etc/login.defs.bak"
# Insert at the end of the file
printf '%s\n' "SHA_CRYPT_MIN_ROUNDS 5000" >> "/etc/login.defs"
# Clean up after ourselves.
rm "/etc/login.defs.bak"

Rule   Disallow Configuration to Bypass Password Requirements for Privilege Escalation   [ref]

Verify the operating system is not configured to bypass password requirements for privilege escalation. Check the configuration of the "/etc/pam.d/sudo" file with the following command:
$ sudo grep pam_succeed_if /etc/pam.d/sudo
If any occurrences of "pam_succeed_if" is returned from the command, this is a finding.
Rationale:
Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical the user re-authenticate.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_disallow_bypass_password_sudo
Identifiers and References

References:  CCI-002038, IA-11, SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158, OL08-00-010385, SV-252656r854184_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010385
  - NIST-800-53-IA-11
  - disallow_bypass_password_sudo
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Check for pam_succeed_if entry
  ansible.builtin.lineinfile:
    path: /etc/pam.d/sudo
    create: false
    regexp: pam_succeed_if
    state: absent
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010385
  - NIST-800-53-IA-11
  - disallow_bypass_password_sudo
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

sed -i '/pam_succeed_if/d' /etc/pam.d/sudo

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Protect Physical Console Access   Group contains 3 groups and 14 rules
[ref]   It is impossible to fully protect a system from an attacker with physical access, so securing the space in which the system is located should be considered a necessary step. However, there are some steps which, if taken, make it more difficult for an attacker to quickly or undetectably modify a system from its console.
Group   Configure Screen Locking   Group contains 2 groups and 9 rules
[ref]   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.
Group   Configure Console Screen Locking   Group contains 7 rules
[ref]   A console screen locking mechanism is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operation system session prior to vacating the vicinity, operating systems need to be able to identify when a user's session has idled and take action to initiate the session lock.

Rule   Install the tmux Package   [ref]

To enable console screen locking, install the tmux package. A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to log out because of the temporary nature of the absence. The session lock is implemented at the point where session activity can be determined. Rather than be forced to wait for a period of time to expire before the user session can be locked, Oracle Linux 8 needs to provide users with the ability to manually invoke a session lock so users can secure their session if it is necessary to temporarily vacate the immediate physical vicinity. Instruct users to begin new terminal sessions with the following command:
$ tmux
The console can now be locked with the following key combination:
ctrl+b :lock-session
Rationale:
A session time-out lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not logout because of the temporary nature of the absence. Rather than relying on the user to manually lock their operation system session prior to vacating the vicinity, operating systems need to be able to identify when a user's session has idled and take action to initiate the session lock.

The tmux package allows for a session lock to be implemented and configured.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_tmux_installed
Identifiers and References

References:  1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, 3.1.10, CCI-000058, CCI-000056, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), PR.AC-7, FMT_SMF_EXT.1, FMT_MOF_EXT.1, FTA_SSL.1, SRG-OS-000030-GPOS-00011, SRG-OS-000028-GPOS-00009, OL08-00-020039, SV-248674r779588_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=tmux


[[packages]]
name = "tmux"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_tmux

class install_tmux {
  package { 'tmux':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure tmux is installed
  package:
    name: tmux
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020039
  - NIST-800-171-3.1.10
  - NIST-800-53-CM-6(a)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_tmux_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "tmux" ; then
    yum install -y "tmux"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Support session locking with tmux (not enforcing)   [ref]

The tmux terminal multiplexer is used to implement automatic session locking. It should be started from /etc/bashrc or drop-in files within /etc/profile.d/.
Warning:  This rule configures Tmux to be executed in a way that exiting Tmux drops the user into a regular shell instead of logging them out, therefore the session locking mechanism is not enforced on the user.
Rationale:
Unlike bash itself, the tmux terminal multiplexer provides a mechanism to lock sessions after period of inactivity. A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to log out because of the temporary nature of the absence.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configure_bashrc_tmux
Identifiers and References

References:  CCI-000056, CCI-000058, SRG-OS-000031-GPOS-00012, SRG-OS-000028-GPOS-00009, SRG-OS-000030-GPOS-00011, OL08-00-020041, SV-248676r880538_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020041
  - configure_bashrc_tmux
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: 'Support session locking with tmux (not enforcing): Determine if the Tmux
    launch script is present in /etc/bashrc'
  ansible.builtin.find:
    paths: /etc
    patterns: bashrc
    contains: .*case "$name" in sshd|login) tmux ;; esac.*
  register: tmux_in_bashrc
  when: '"tmux" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020041
  - configure_bashrc_tmux
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: 'Support session locking with tmux (not enforcing): Determine if the Tmux
    launch script is present in /etc/profile.d/*.sh'
  ansible.builtin.find:
    paths: /etc/profile.d
    patterns: '*.sh'
    contains: .*case "$name" in sshd|login) tmux ;; esac.*
  register: tmux_in_profile_d
  when: '"tmux" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020041
  - configure_bashrc_tmux
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: 'Support session locking with tmux (not enforcing): Insert the correct script
    into /etc/profile.d/tmux.sh'
  ansible.builtin.blockinfile:
    path: /etc/profile.d/tmux.sh
    block: |
      if [ "$PS1" ]; then
        parent=$(ps -o ppid= -p $$)
        name=$(ps -o comm= -p $parent)
        case "$name" in sshd|login) tmux ;; esac
      fi
    create: true
  when:
  - '"tmux" in ansible_facts.packages'
  - tmux_in_bashrc is defined and tmux_in_bashrc.matched == 0
  - tmux_in_profile_d is defined and tmux_in_profile_d.matched == 0
  tags:
  - DISA-STIG-OL08-00-020041
  - configure_bashrc_tmux
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:true
Strategy:enable
# Remediation is applicable only in certain platforms
if rpm --quiet -q tmux; then

if ! grep -x '  case "$name" in sshd|login) tmux ;; esac' /etc/bashrc /etc/profile.d/*.sh; then
    cat >> /etc/profile.d/tmux.sh <<'EOF'
if [ "$PS1" ]; then
  parent=$(ps -o ppid= -p $$)
  name=$(ps -o comm= -p $parent)
  case "$name" in sshd|login) tmux ;; esac
fi
EOF
    chmod 0644 /etc/profile.d/tmux.sh
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure tmux to lock session after inactivity   [ref]

To enable console screen locking in tmux terminal multiplexer after a period of inactivity, the lock-after-time option has to be set to a value greater than 0 and less than or equal to 900 in /etc/tmux.conf.
Rationale:
Locking the session after a period of inactivity limits the potential exposure if the session is left unattended.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configure_tmux_lock_after_time
Identifiers and References

References:  CCI-000057, CCI-000060, FMT_SMF_EXT.1, FMT_MOF_EXT.1, FTA_SSL.1, SRG-OS-000029-GPOS-00010, SRG-OS-000031-GPOS-00012, OL08-00-020070, SV-248681r779609_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Configure tmux to lock session after inactivity
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/tmux.conf
      create: false
      regexp: ^\s*set -g lock-after-time\s+
      mode: '0644'
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/tmux.conf
    lineinfile:
      path: /etc/tmux.conf
      create: false
      regexp: ^\s*set -g lock-after-time\s+
      mode: '0644'
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/tmux.conf
    lineinfile:
      path: /etc/tmux.conf
      create: true
      regexp: ^\s*set -g lock-after-time\s+
      mode: '0644'
      line: set -g lock-after-time 900
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020070
  - configure_tmux_lock_after_time
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

tmux_conf="/etc/tmux.conf"

if grep -qP '^\s*set\s+-g\s+lock-after-time' "$tmux_conf" ; then
    sed -i 's/^\s*set\s\+-g\s\+lock-after-time.*$/set -g lock-after-time 900/' "$tmux_conf"
else
    echo "set -g lock-after-time 900" >> "$tmux_conf"
fi
chmod 0644 "$tmux_conf"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure the tmux Lock Command   [ref]

To enable console screen locking in tmux terminal multiplexer, the vlock command must be configured to be used as a locking mechanism. Add the following line to /etc/tmux.conf:
set -g lock-command vlock
. The console can now be locked with the following key combination:
ctrl+b :lock-session
Rationale:
The tmux package allows for a session lock to be implemented and configured. However, the session lock is implemented by an external command. The tmux default configuration does not contain an effective session lock.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configure_tmux_lock_command
Identifiers and References

References:  CCI-000056, CCI-000058, AC-11(a), AC-11(b), CM-6(a), FMT_SMF_EXT.1, FMT_MOF_EXT.1, FTA_SSL.1, SRG-OS-000028-GPOS-00009, SRG-OS-000030-GPOS-00011, OL08-00-020040, SV-248675r902786_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Configure the tmux Lock Command
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/tmux.conf
      create: false
      regexp: ^\s*set -g lock-command\s+
      mode: '0644'
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/tmux.conf
    lineinfile:
      path: /etc/tmux.conf
      create: false
      regexp: ^\s*set -g lock-command\s+
      mode: '0644'
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/tmux.conf
    lineinfile:
      path: /etc/tmux.conf
      create: true
      regexp: ^\s*set -g lock-command\s+
      mode: '0644'
      line: set -g lock-command vlock
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020040
  - NIST-800-53-AC-11(a)
  - NIST-800-53-AC-11(b)
  - NIST-800-53-CM-6(a)
  - configure_tmux_lock_command
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

tmux_conf="/etc/tmux.conf"

if grep -qP '^\s*set\s+-g\s+lock-command' "$tmux_conf" ; then
    sed -i 's/^\s*set\s\+-g\s\+lock-command.*$/set -g lock-command vlock/' "$tmux_conf"
else
    echo "set -g lock-command vlock" >> "$tmux_conf"
fi
chmod 0644 "$tmux_conf"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure the tmux lock session key binding   [ref]

To set a key binding for the screen locking in tmux terminal multiplexer, the session-lock command must be bound to a key. Add the following line to /etc/tmux.conf:
bind X lock-session
. The console can now be locked with the following key combination:
Ctrl+b Shift+x
Rationale:
The tmux package allows for a session lock to be implemented and configured. However, the session lock is implemented by an external command. The tmux default configuration does not contain an effective session lock.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_configure_tmux_lock_keybinding
Identifiers and References

References:  CCI-000056, SRG-OS-000028-GPOS-00009, SRG-OS-000030-GPOS-00011, OL08-00-020040, SV-248675r902786_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Check for duplicate values
  lineinfile:
    path: /etc/tmux.conf
    create: false
    regexp: \s*bind\s+\w\s+lock-session.*$
    mode: '0644'
    state: absent
  check_mode: true
  changed_when: false
  register: dupes
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020040
  - configure_strategy
  - configure_tmux_lock_keybinding
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed

- name: Deduplicate values from /etc/tmux.conf
  lineinfile:
    path: /etc/tmux.conf
    create: false
    regexp: \s*bind\s+\w\s+lock-session.*$
    mode: '0644'
    state: absent
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - dupes.found is defined and dupes.found > 1
  tags:
  - DISA-STIG-OL08-00-020040
  - configure_strategy
  - configure_tmux_lock_keybinding
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed

- name: Insert correct line into /etc/tmux.conf
  lineinfile:
    path: /etc/tmux.conf
    create: null
    regexp: \s*bind\s+\w\s+lock-session.*$
    mode: '0644'
    line: bind X lock-session
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020040
  - configure_strategy
  - configure_tmux_lock_keybinding
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

tmux_conf="/etc/tmux.conf"

if ! grep -qP '^\s*bind\s+\w\s+lock-session' "$tmux_conf" ; then
    echo "bind X lock-session" >> "$tmux_conf"
fi
chmod 0644 "$tmux_conf"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Prevent user from disabling the screen lock   [ref]

The tmux terminal multiplexer is used to implement automatic session locking. It should not be listed in /etc/shells.
Rationale:
Not listing tmux among permitted shells prevents malicious program running as user from lowering security by disabling the screen lock.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_no_tmux_in_shells
Identifiers and References

References:  CCI-000056, CCI-000058, CM-6, FMT_SMF_EXT.1, FMT_MOF_EXT.1, FTA_SSL.1, SRG-OS-000324-GPOS-00125, SRG-OS-000028-GPOS-00009, SRG-OS-000030-GPOS-00011, OL08-00-020042, SV-248677r779597_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if grep -q 'tmux\s*$' /etc/shells ; then
	sed -i '/tmux\s*$/d' /etc/shells
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Check that vlock is installed to allow session locking   [ref]

The Oracle Linux 8 operating system must have vlock installed to allow for session locking. The kbd package can be installed with the following command:
$ sudo yum install kbd
Rationale:
A session lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not want to log out because of the temporary nature of the absence. The session lock is implemented at the point where session activity can be determined. Regardless of where the session lock is determined and implemented, once invoked, the session lock must remain in place until the user reauthenticates. No other activity aside from reauthentication must unlock the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_vlock_installed
Identifiers and References

References:  CCI-000056, CCI-000058, CCI-000060, SRG-OS-000028-GPOS-00009, SRG-OS-000030-GPOS-00011, OL08-00-020043, SV-248678r779600_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=kbd


[[packages]]
name = "kbd"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_kbd

class install_kbd {
  package { 'kbd':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure kbd is installed
  package:
    name: kbd
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020043
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - vlock_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "kbd" ; then
    yum install -y "kbd"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Hardware Tokens for Authentication   Group contains 2 rules

Rule   Install the opensc Package For Multifactor Authentication   [ref]

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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_opensc_installed
Identifiers and References

References:  CCI-001954, CCI-001953, 1382, 1384, 1386, CM-6(a), SRG-OS-000375-GPOS-00160, SRG-OS-000376-GPOS-00161, OL08-00-010410, SV-248588r853769_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=opensc


[[packages]]
name = "opensc"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_opensc

class install_opensc {
  package { 'opensc':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure opensc is installed
  package:
    name: opensc
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010410
  - NIST-800-53-CM-6(a)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_opensc_installed

Complexity:low
Disruption:low
Strategy:enable
# 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

Rule   Install Smart Card Packages For Multifactor Authentication   [ref]

Configure the operating system to implement multifactor authentication by installing the required package with the following command: The openssl-pkcs11 package can be installed with the following command:
$ sudo yum install openssl-pkcs11
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.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_install_smartcard_packages
Identifiers and References

References:  CCI-000765, CCI-001948, CCI-001953, CCI-001954, CM-6(a), Req-8.3, 8.4, SRG-OS-000105-GPOS-00052, SRG-OS-000375-GPOS-00160, SRG-OS-000375-GPOS-00161, SRG-OS-000377-GPOS-00162, OL08-00-010390, SV-248586r853767_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=openssl-pkcs11


[[packages]]
name = "openssl-pkcs11"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_openssl-pkcs11

class install_openssl-pkcs11 {
  package { 'openssl-pkcs11':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure openssl-pkcs11 is installed
  package:
    name: openssl-pkcs11
    state: present
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture != "s390x"
  tags:
  - DISA-STIG-OL08-00-010390
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.3
  - PCI-DSSv4-8.4
  - enable_strategy
  - install_smartcard_packages
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { ! grep -q s390x /proc/sys/kernel/osrelease; }; then

if ! rpm -q --quiet "openssl-pkcs11" ; then
    yum install -y "openssl-pkcs11"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable debug-shell SystemD Service   [ref]

SystemD's debug-shell service is intended to diagnose SystemD related boot issues with various systemctl commands. Once enabled and following a system reboot, the root shell will be available on tty9 which is access by pressing CTRL-ALT-F9. The debug-shell service should only be used for SystemD related issues and should otherwise be disabled.

By default, the debug-shell SystemD service is already disabled. The debug-shell service can be disabled with the following command:
$ sudo systemctl mask --now debug-shell.service
Rationale:
This prevents attackers with physical access from trivially bypassing security on the machine through valid troubleshooting configurations and gaining root access when the system is rebooted.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_service_debug-shell_disabled
Identifiers and References

References:  3.4.5, CCI-000366, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), CM-6, FIA_UAU.1, SRG-OS-000324-GPOS-00125, SRG-OS-000480-GPOS-00227, OL08-00-040180, SV-248872r780182_rule



[customizations.services]
disabled = ["debug-shell"]

Complexity:low
Disruption:low
Strategy:enable
include disable_debug-shell

class disable_debug-shell {
  service {'debug-shell':
    enable => false,
    ensure => 'stopped',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Block Disable service debug-shell
  block:

  - name: Disable service debug-shell
    block:

    - name: Disable service debug-shell
      systemd:
        name: debug-shell.service
        enabled: 'no'
        state: stopped
        masked: 'yes'
    rescue:

    - name: Intentionally ignored previous 'Disable service debug-shell' failure,
        service was already disabled
      meta: noop
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040180
  - NIST-800-171-3.4.5
  - NIST-800-53-CM-6
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_debug-shell_disabled

- name: Unit Socket Exists - debug-shell.socket
  command: systemctl list-unit-files debug-shell.socket
  register: socket_file_exists
  changed_when: false
  failed_when: socket_file_exists.rc not in [0, 1]
  check_mode: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040180
  - NIST-800-171-3.4.5
  - NIST-800-53-CM-6
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_debug-shell_disabled

- name: Disable socket debug-shell
  systemd:
    name: debug-shell.socket
    enabled: 'no'
    state: stopped
    masked: 'yes'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"debug-shell.socket" in socket_file_exists.stdout_lines[1]'
  tags:
  - DISA-STIG-OL08-00-040180
  - NIST-800-171-3.4.5
  - NIST-800-53-CM-6
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_debug-shell_disabled

Complexity:low
Disruption:low
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" stop 'debug-shell.service'
"$SYSTEMCTL_EXEC" disable 'debug-shell.service'
"$SYSTEMCTL_EXEC" mask 'debug-shell.service'
# Disable socket activation if we have a unit file for it
if "$SYSTEMCTL_EXEC" -q list-unit-files debug-shell.socket; then
    "$SYSTEMCTL_EXEC" stop 'debug-shell.socket'
    "$SYSTEMCTL_EXEC" mask 'debug-shell.socket'
fi
# The service may not be running because it has been started and failed,
# so let's reset the state so OVAL checks pass.
# Service should be 'inactive', not 'failed' after reboot though.
"$SYSTEMCTL_EXEC" reset-failed 'debug-shell.service' || true

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Ctrl-Alt-Del Burst Action   [ref]

By default, SystemD will reboot the system if the Ctrl-Alt-Del key sequence is pressed Ctrl-Alt-Delete more than 7 times in 2 seconds.

To configure the system to ignore the CtrlAltDelBurstAction setting, add or modify the following to /etc/systemd/system.conf:
CtrlAltDelBurstAction=none
Warning:  Disabling the Ctrl-Alt-Del key sequence in /etc/init/control-alt-delete.conf DOES NOT disable the Ctrl-Alt-Del key sequence if running in runlevel 6 (e.g. in GNOME, KDE, etc.)! The Ctrl-Alt-Del key sequence will only be disabled if running in the non-graphical runlevel 3.
Rationale:
A locally logged-in user who presses Ctrl-Alt-Del, when at the console, can reboot the system. If accidentally pressed, as could happen in the case of mixed OS environment, this can create the risk of short-term loss of availability of systems due to unintentional reboot.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.4.5, CCI-000366, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-6(a), AC-6(1), CM-6(a), PR.AC-4, PR.DS-5, FAU_GEN.1.2, SRG-OS-000324-GPOS-00125, SRG-OS-000480-GPOS-00227, OL08-00-040172, SV-248871r780179_rule


Complexity:low
Disruption:low
Strategy:disable
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-040172
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(a)
  - disable_ctrlaltdel_burstaction
  - disable_strategy
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed

- name: Disable Ctrl-Alt-Del Burst Action
  lineinfile:
    dest: /etc/systemd/system.conf
    state: present
    regexp: ^CtrlAltDelBurstAction
    line: CtrlAltDelBurstAction=none
    create: true
  when: '"systemd" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-040172
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(a)
  - disable_ctrlaltdel_burstaction
  - disable_strategy
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if rpm --quiet -q systemd; then

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^CtrlAltDelBurstAction=")

# shellcheck disable=SC2059
printf -v formatted_output "%s=%s" "$stripped_key" "none"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^CtrlAltDelBurstAction=\\>" "/etc/systemd/system.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^CtrlAltDelBurstAction=\\>.*/$escaped_formatted_output/gi" "/etc/systemd/system.conf"
else
    if [[ -s "/etc/systemd/system.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/systemd/system.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/systemd/system.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/systemd/system.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Ctrl-Alt-Del Reboot Activation   [ref]

By default, SystemD will reboot the system if the Ctrl-Alt-Del key sequence is pressed.

To configure the system to ignore the Ctrl-Alt-Del key sequence from the command line instead of rebooting the system, do either of the following:
ln -sf /dev/null /etc/systemd/system/ctrl-alt-del.target
or
systemctl mask ctrl-alt-del.target


Do not simply delete the /usr/lib/systemd/system/ctrl-alt-del.service file, as this file may be restored during future system updates.
Rationale:
A locally logged-in user who presses Ctrl-Alt-Del, when at the console, can reboot the system. If accidentally pressed, as could happen in the case of mixed OS environment, this can create the risk of short-term loss of availability of systems due to unintentional reboot.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_reboot
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.4.5, CCI-000366, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, FAU_GEN.1.2, SRG-OS-000324-GPOS-00125, SRG-OS-000480-GPOS-00227, OL08-00-040170, SV-248869r833246_rule


Complexity:low
Disruption:low
Strategy:disable
- name: Disable Ctrl-Alt-Del Reboot Activation
  systemd:
    name: ctrl-alt-del.target
    force: true
    masked: true
    state: stopped
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040170
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - disable_ctrlaltdel_reboot
  - disable_strategy
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

systemctl disable --now ctrl-alt-del.target
systemctl mask --now ctrl-alt-del.target

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Require Authentication for Emergency Systemd Target   [ref]

Emergency mode is intended as a system recovery method, providing a single user root access to the system during a failed boot sequence.

By default, Emergency mode is protected by requiring a password and is set in /usr/lib/systemd/system/emergency.service.
Rationale:
This prevents attackers with physical access from trivially bypassing security on the machine and gaining root access. Such accesses are further prevented by configuring the bootloader password.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_require_emergency_target_auth
Identifiers and References

References:  1, 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.1.1, 3.4.5, CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, IA-2, AC-3, CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048, OL08-00-010152, SV-248542r779192_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Require emergency mode password
  lineinfile:
    create: true
    dest: /usr/lib/systemd/system/emergency.service
    regexp: ^#?ExecStart=
    line: ExecStart=-/usr/lib/systemd/systemd-sulogin-shell emergency
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010152
  - NIST-800-171-3.1.1
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-3
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-2
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - require_emergency_target_auth
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

service_file="/usr/lib/systemd/system/emergency.service"


sulogin="/usr/lib/systemd/systemd-sulogin-shell emergency"


if grep "^ExecStart=.*" "$service_file" ; then
    sed -i "s%^ExecStart=.*%ExecStart=-$sulogin%" "$service_file"
else
    echo "ExecStart=-$sulogin" >> "$service_file"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Require Authentication for Single User Mode   [ref]

Single-user mode is intended as a system recovery method, providing a single user root access to the system by providing a boot option at startup.

By default, single-user mode is protected by requiring a password and is set in /usr/lib/systemd/system/rescue.service.
Rationale:
This prevents attackers with physical access from trivially bypassing security on the machine and gaining root access. Such accesses are further prevented by configuring the bootloader password.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_require_singleuser_auth
Identifiers and References

References:  1, 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.1.1, 3.4.5, CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, IA-2, AC-3, CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048, OL08-00-010151, SV-248541r779189_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Require single user mode password
  lineinfile:
    create: true
    dest: /usr/lib/systemd/system/rescue.service
    regexp: ^#?ExecStart=
    line: ExecStart=-/usr/lib/systemd/systemd-sulogin-shell rescue
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010151
  - NIST-800-171-3.1.1
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-3
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-2
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - require_singleuser_auth
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

service_file="/usr/lib/systemd/system/rescue.service"

sulogin="/usr/lib/systemd/systemd-sulogin-shell rescue"

if grep "^ExecStart=.*" "$service_file" ; then
    sed -i "s%^ExecStart=.*%ExecStart=-$sulogin%" "$service_file"
else
    echo "ExecStart=-$sulogin" >> "$service_file"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Protect Accounts by Restricting Password-Based Login   Group contains 4 groups and 16 rules
[ref]   Conventionally, Unix shell accounts are accessed by providing a username and password to a login program, which tests these values for correctness using the /etc/passwd and /etc/shadow files. Password-based login is vulnerable to guessing of weak passwords, and to sniffing and man-in-the-middle attacks against passwords entered over a network or at an insecure console. Therefore, mechanisms for accessing accounts by entering usernames and passwords should be restricted to those which are operationally necessary.
Group   Set Account Expiration Parameters   Group contains 5 rules
Group   Set Password Expiration Parameters   Group contains 5 rules
[ref]   The file /etc/login.defs controls several password-related settings. Programs such as passwd, su, and login consult /etc/login.defs to determine behavior with regard to password aging, expiration warnings, and length. See the man page login.defs(5) for more information.

Users should be forced to change their passwords, in order to decrease the utility of compromised passwords. However, the need to change passwords often should be balanced against the risk that users will reuse or write down passwords if forced to change them too often. Forcing password changes every 90-360 days, depending on the environment, is recommended. Set the appropriate value as PASS_MAX_DAYS and apply it to existing accounts with the -M flag.

The PASS_MIN_DAYS (-m) setting prevents password changes for 7 days after the first change, to discourage password cycling. If you use this setting, train users to contact an administrator for an emergency password change in case a new password becomes compromised. The PASS_WARN_AGE (-W) setting gives users 7 days of warnings at login time that their passwords are about to expire.

For example, for each existing human user USER, expiration parameters could be adjusted to a 180 day maximum password age, 7 day minimum password age, and 7 day warning period with the following command:
$ sudo chage -M 180 -m 7 -W 7 USER

Rule   Set Existing Passwords Maximum Age   [ref]

Configure non-compliant accounts to enforce a 60-day maximum password lifetime restriction by running the following command:
$ sudo chage -M 60 USER
Rationale:
Any password, no matter how complex, can eventually be cracked. Therefore, passwords need to be changed periodically. If the operating system does not limit the lifetime of passwords and force users to change their passwords, there is the risk that the operating system passwords could be compromised.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_set_max_life_existing
Identifiers and References

References:  CCI-000199, IA-5(f), IA-5(1)(d), CM-6(a), SRG-OS-000076-GPOS-00044, OL08-00-020210, SV-248697r779657_rule


Complexity:low
Disruption:low
Strategy:restrict

var_accounts_maximum_age_login_defs='60'


while IFS= read -r i; do
    
    chage -M $var_accounts_maximum_age_login_defs $i

done <   <(awk -v var="$var_accounts_maximum_age_login_defs" -F: '(/^[^:]+:[^!*]/ && ($5 > var || $5 == "")) {print $1}' /etc/shadow)

Rule   Set Existing Passwords Minimum Age   [ref]

Configure non-compliant accounts to enforce a 24 hours/1 day minimum password lifetime by running the following command:
$ sudo chage -m 1 USER
Rationale:
Enforcing a minimum password lifetime helps to prevent repeated password changes to defeat the password reuse or history enforcement requirement. If users are allowed to immediately and continually change their password, the password could be repeatedly changed in a short period of time to defeat the organization's policy regarding password reuse.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_set_min_life_existing
Identifiers and References

References:  CCI-000198, IA-5(f), IA-5(1)(d), CM-6(a), SRG-OS-000075-GPOS-00043, OL08-00-020180, SV-248694r779648_rule


Complexity:low
Disruption:low
Strategy:restrict

var_accounts_minimum_age_login_defs='1'


while IFS= read -r i; do
    
    chage -m $var_accounts_minimum_age_login_defs $i

done <   <(awk -v var="$var_accounts_minimum_age_login_defs" -F: '(/^[^:]+:[^!*]/ && ($4 < var || $4 == "")) {print $1}' /etc/shadow)
Group   Verify Proper Storage and Existence of Password Hashes   Group contains 3 rules
[ref]   By default, password hashes for local accounts are stored in the second field (colon-separated) in /etc/shadow. This file should be readable only by processes running with root credentials, preventing users from casually accessing others' password hashes and attempting to crack them. However, it remains possible to misconfigure the system and store password hashes in world-readable files such as /etc/passwd, or to even store passwords themselves in plaintext on the system. Using system-provided tools for password change/creation should allow administrators to avoid such misconfiguration.

Rule   Verify All Account Password Hashes are Shadowed with SHA512   [ref]

Verify the operating system requires the shadow password suite configuration be set to encrypt interactive user passwords using a strong cryptographic hash. Check that the interactive user account passwords are using a strong password hash with the following command:
$ sudo cut -d: -f2 /etc/shadow
$6$kcOnRq/5$NUEYPuyL.wghQwWssXRcLRFiiru7f5JPV6GaJhNC2aK5F3PZpE/BCCtwrxRc/AInKMNX3CdMw11m9STiql12f/
Password hashes ! or * indicate inactive accounts not available for logon and are not evaluated. If any interactive user password hash does not begin with $6, this is a finding.
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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_all_shadowed_sha512
Identifiers and References

References:  CCI-000196, CCI-000803, IA-5(1)(c), IA-5(1).1(v), IA-7, IA-7.1, SRG-OS-000073-GPOS-00041, SRG-OS-000120-GPOS-00061, OL08-00-010120, SV-248534r877397_rule

Rule   Prevent Login to Accounts With Empty Password   [ref]

If an account is configured for password authentication but does not have an assigned password, it may be possible to log into the account without authentication. Remove any instances of the nullok in /etc/pam.d/system-auth and /etc/pam.d/password-auth to prevent logins with empty passwords.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. Note that this rule is not applicable for systems running within a container. Having user with empty password within a container is not considered a risk, because it should not be possible to directly login into a container anyway.
Rationale:
If an account has an empty password, anyone could log in and run commands with the privileges of that account. Accounts with empty passwords should never be used in operational environments.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_no_empty_passwords
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 18, 3, 5, 5.5.2, APO01.06, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.02, DSS06.03, DSS06.10, 3.1.1, 3.1.5, CCI-000366, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.18.1.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, IA-5(1)(a), IA-5(c), CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.DS-5, FIA_UAU.1, Req-8.2.3, 8.3.6, 8.3.9, SRG-OS-000480-GPOS-00227, OL08-00-020331, SV-248715r779711_rule


---
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

Complexity:low
Disruption:medium
Strategy:configure
- name: Prevent Login to Accounts With Empty Password - Check if system relies on
    authselect
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2
  - DISA-STIG-OL08-00-020331
  - NIST-800-171-3.1.1
  - NIST-800-171-3.1.5
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - configure_strategy
  - high_severity
  - low_complexity
  - medium_disruption
  - no_empty_passwords
  - no_reboot_needed

- name: Prevent Login to Accounts With Empty Password - Remediate using authselect
  block:

  - name: Prevent Login to Accounts With Empty Password - Check integrity of authselect
      current profile
    ansible.builtin.command:
      cmd: authselect check
    register: result_authselect_check_cmd
    changed_when: false
    failed_when: false

  - name: Prevent Login to Accounts With Empty Password - Informative message based
      on the authselect integrity check result
    ansible.builtin.assert:
      that:
      - result_authselect_check_cmd.rc == 0
      fail_msg:
      - authselect integrity check failed. Remediation aborted!
      - This remediation could not be applied because an authselect profile was not
        selected or the selected profile is not intact.
      - It is not recommended to manually edit the PAM files when authselect tool
        is available.
      - In cases where the default authselect profile does not cover a specific demand,
        a custom authselect profile is recommended.
      success_msg:
      - authselect integrity check passed

  - name: Prevent Login to Accounts With Empty Password - Get authselect current features
    ansible.builtin.shell:
      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
    register: result_authselect_features
    changed_when: false
    when:
    - result_authselect_check_cmd is success

  - name: Prevent Login to Accounts With Empty Password - Ensure "without-nullok"
      feature is enabled using authselect tool
    ansible.builtin.command:
      cmd: authselect enable-feature without-nullok
    register: result_authselect_enable_feature_cmd
    when:
    - result_authselect_check_cmd is success
    - result_authselect_features.stdout is not search("without-nullok")

  - name: Prevent Login to Accounts With Empty Password - Ensure authselect changes
      are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_enable_feature_cmd is not skipped
    - result_authselect_enable_feature_cmd is success
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - result_authselect_present.stat.exists
  tags:
  - CJIS-5.5.2
  - DISA-STIG-OL08-00-020331
  - NIST-800-171-3.1.1
  - NIST-800-171-3.1.5
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - configure_strategy
  - high_severity
  - low_complexity
  - medium_disruption
  - no_empty_passwords
  - no_reboot_needed

- name: Prevent Login to Accounts With Empty Password - Remediate directly editing
    PAM files
  ansible.builtin.replace:
    dest: '{{ item }}'
    regexp: nullok
  loop:
  - /etc/pam.d/system-auth
  - /etc/pam.d/password-auth
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - not result_authselect_present.stat.exists
  tags:
  - CJIS-5.5.2
  - DISA-STIG-OL08-00-020331
  - NIST-800-171-3.1.1
  - NIST-800-171-3.1.5
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - PCI-DSSv4-8.3.9
  - configure_strategy
  - high_severity
  - low_complexity
  - medium_disruption
  - no_empty_passwords
  - no_reboot_needed

Complexity:low
Disruption:medium
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if [ -f /usr/bin/authselect ]; then
    if ! authselect check; then
echo "
authselect integrity check failed. Remediation aborted!
This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
It is not recommended to manually edit the PAM files when authselect tool is available.
In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
exit 1
fi
authselect enable-feature without-nullok

authselect apply-changes -b
else
    
if grep -qP '^\s*auth\s+'"sufficient"'\s+pam_unix.so\s.*\bnullok\b' "/etc/pam.d/system-auth"; then
    sed -i -E --follow-symlinks 's/(.*auth.*'"sufficient"'.*pam_unix.so.*)\snullok=?[[:alnum:]]*(.*)/\1\2/g' "/etc/pam.d/system-auth"
fi
    
if grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s.*\bnullok\b' "/etc/pam.d/system-auth"; then
    sed -i -E --follow-symlinks 's/(.*password.*'"sufficient"'.*pam_unix.so.*)\snullok=?[[:alnum:]]*(.*)/\1\2/g' "/etc/pam.d/system-auth"
fi
    
if grep -qP '^\s*auth\s+'"sufficient"'\s+pam_unix.so\s.*\bnullok\b' "/etc/pam.d/password-auth"; then
    sed -i -E --follow-symlinks 's/(.*auth.*'"sufficient"'.*pam_unix.so.*)\snullok=?[[:alnum:]]*(.*)/\1\2/g' "/etc/pam.d/password-auth"
fi
    
if grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s.*\bnullok\b' "/etc/pam.d/password-auth"; then
    sed -i -E --follow-symlinks 's/(.*password.*'"sufficient"'.*pam_unix.so.*)\snullok=?[[:alnum:]]*(.*)/\1\2/g' "/etc/pam.d/password-auth"
fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure There Are No Accounts With Blank or Null Passwords   [ref]

Check the "/etc/shadow" file for blank passwords with the following command:
$ sudo awk -F: '!$2 {print $1}' /etc/shadow
If the command returns any results, this is a finding. Configure all accounts on the system to have a password or lock the account with the following commands: Perform a password reset:
$ sudo passwd [username]
Lock an account:
$ sudo passwd -l [username]
Warning:  Note that this rule is not applicable for systems running within a container. Having user with empty password within a container is not considered a risk, because it should not be possible to directly login into a container anyway.
Rationale:
If an account has an empty password, anyone could log in and run commands with the privileges of that account. Accounts with empty passwords should never be used in operational environments.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_no_empty_passwords_etc_shadow
Identifiers and References

References:  CCI-000366, CM-6(b), CM-6.1(iv), SRG-OS-000480-GPOS-00227, OL08-00-010121, SV-252650r818746_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Collect users with no password
  command: |
    awk -F: '!$2 {print $1}' /etc/shadow
  register: users_nopasswd
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010121
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - high_severity
  - low_complexity
  - low_disruption
  - no_empty_passwords_etc_shadow
  - no_reboot_needed
  - restrict_strategy

- name: Lock users with no password
  command: |
    passwd -l {{ item }}
  with_items: '{{ users_nopasswd.stdout_lines }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - users_nopasswd.stdout_lines | length > 0
  tags:
  - DISA-STIG-OL08-00-010121
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - high_severity
  - low_complexity
  - low_disruption
  - no_empty_passwords_etc_shadow
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

readarray -t users_with_empty_pass < <(sudo awk -F: '!$2 {print $1}' /etc/shadow)

for user_with_empty_pass in "${users_with_empty_pass[@]}"
do
    passwd -l $user_with_empty_pass
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Restrict Root Logins   Group contains 1 rule
[ref]   Direct root logins should be allowed only for emergency use. In normal situations, the administrator should access the system via a unique unprivileged account, and then use su or sudo to execute privileged commands. Discouraging administrators from accessing the root account directly ensures an audit trail in organizations with multiple administrators. Locking down the channels through which root can connect directly also reduces opportunities for password-guessing against the root account. The login program uses the file /etc/securetty to determine which interfaces should allow root logins. The virtual devices /dev/console and /dev/tty* represent the system consoles (accessible via the Ctrl-Alt-F1 through Ctrl-Alt-F6 keyboard sequences on a default installation). The default securetty file also contains /dev/vc/*. These are likely to be deprecated in most environments, but may be retained for compatibility. Root should also be prohibited from connecting via network protocols. Other sections of this document include guidance describing how to prevent root from logging in via SSH.

Rule   Verify Only Root Has UID 0   [ref]

If any account other than root has a UID of 0, this misconfiguration should be investigated and the accounts other than root should be removed or have their UID changed.
If the account is associated with system commands or applications the UID should be changed to one greater than "0" but less than "1000." Otherwise assign a UID greater than "1000" that has not already been assigned.
Rationale:
An account has root authority if it has a UID of 0. Multiple accounts with a UID of 0 afford more opportunity for potential intruders to guess a password for a privileged account. Proper configuration of sudo is recommended to afford multiple system administrators access to root privileges in an accountable manner.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_accounts_no_uid_except_zero
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.02, DSS06.03, DSS06.10, 3.1.1, 3.1.5, CCI-000366, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.18.1.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, IA-2, AC-6(5), IA-4(b), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.DS-5, Req-8.5, 8.2.2, 8.2.3, SRG-OS-000480-GPOS-00227, OL08-00-040200, SV-248874r780188_rule


awk -F: '$3 == 0 && $1 != "root" { print $1 }' /etc/passwd | xargs --no-run-if-empty --max-lines=1 passwd -l

Rule   Only Authorized Local User Accounts Exist on Operating System   [ref]

Enterprise Application tends to use the server or virtual machine exclusively. Besides the default operating system user, there should be only authorized local users required by the installed software groups and applications that exist on the operating system. The authorized user list can be customized in the refine value variable var_accounts_authorized_local_users_regex. OVAL regular expression is used for the user list. Configure the system so all accounts on the system are assigned to an active system, application, or user account. Remove accounts that do not support approved system activities or that allow for a normal user to perform administrative-level actions. To remove unauthorized system accounts, use the following command:
$ sudo userdel unauthorized_user
Warning:  Automatic remediation of this control is not available due to the unique requirements of each system.
Rationale:
Accounts providing no operational purpose provide additional opportunities for system compromise. Unnecessary accounts include user accounts for individuals not requiring access to the system and application accounts for applications not installed on the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_authorized_local_users
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-020320, SV-248713r779705_rule

Group   Secure Session Configuration Files for Login Accounts   Group contains 1 group and 20 rules
[ref]   When a user logs into a Unix account, the system configures the user's session by reading a number of files. Many of these files are located in the user's home directory, and may have weak permissions as a result of user error or misconfiguration. If an attacker can modify or even read certain types of account configuration information, they can often gain full access to the affected user's account. Therefore, it is important to test and correct configuration file permissions for interactive accounts, particularly those of privileged users such as root or system administrators.
Group   Ensure that Users Have Sensible Umask Values   Group contains 5 rules
[ref]   The umask setting controls the default permissions for the creation of new files. With a default umask setting of 077, files and directories created by users will not be readable by any other user on the system. Users who wish to make specific files group- or world-readable can accomplish this by using the chmod command. Additionally, users can make all their files readable to their group by default by setting a umask of 027 in their shell configuration files. If default per-user groups exist (that is, if every user has a default group whose name is the same as that user's username and whose only member is the user), then it may even be safe for users to select a umask of 007, making it very easy to intentionally share files with groups of which the user is a member.

Rule   Ensure the Default Bash Umask is Set Correctly   [ref]

To ensure the default umask for users of the Bash shell is set properly, add or correct the umask setting in /etc/bashrc to read as follows:
umask 077
Rationale:
The umask value influences the permissions assigned to files when they are created. A misconfigured umask value could result in files with excessive permissions that can be read or written to by unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_umask_etc_bashrc
Identifiers and References

References:  BP28(R35), 18, APO13.01, BAI03.01, BAI03.02, BAI03.03, CCI-000366, 4.3.4.3.3, A.14.1.1, A.14.2.1, A.14.2.5, A.6.1.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, AC-6(1), CM-6(a), PR.IP-2, 8.6.1, SRG-OS-000480-GPOS-00228, SRG-OS-000480-GPOS-00227, OL08-00-020353, SV-248721r818666_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: XCCDF Value var_accounts_user_umask # promote to variable
  set_fact:
    var_accounts_user_umask: !!str 077
  tags:
    - always

- name: Check if umask in /etc/bashrc is already set
  ansible.builtin.lineinfile:
    path: /etc/bashrc
    regexp: ^(\s*)umask\s+.*
    state: absent
  check_mode: true
  changed_when: false
  register: umask_replace
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-8.6.1
  - accounts_umask_etc_bashrc
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Replace user umask in /etc/bashrc
  ansible.builtin.replace:
    path: /etc/bashrc
    regexp: ^(\s*)umask(\s+).*
    replace: \g<1>umask\g<2>{{ var_accounts_user_umask }}
  when: umask_replace.found > 0
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-8.6.1
  - accounts_umask_etc_bashrc
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure the Default umask is Appended Correctly
  ansible.builtin.lineinfile:
    create: true
    path: /etc/bashrc
    line: umask {{ var_accounts_user_umask }}
  when: umask_replace.found == 0
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-8.6.1
  - accounts_umask_etc_bashrc
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy


var_accounts_user_umask='077'






grep -q "^\s*umask" /etc/bashrc && \
  sed -i -E -e "s/^(\s*umask).*/\1 $var_accounts_user_umask/g" /etc/bashrc
if ! [ $? -eq 0 ]; then
    echo "umask $var_accounts_user_umask" >> /etc/bashrc
fi

Rule   Ensure the Default C Shell Umask is Set Correctly   [ref]

To ensure the default umask for users of the C shell is set properly, add or correct the umask setting in /etc/csh.cshrc to read as follows:
umask 077
Rationale:
The umask value influences the permissions assigned to files when they are created. A misconfigured umask value could result in files with excessive permissions that can be read or written to by unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_umask_etc_csh_cshrc
Identifiers and References

References:  18, APO13.01, BAI03.01, BAI03.02, BAI03.03, CCI-000366, 4.3.4.3.3, A.14.1.1, A.14.2.1, A.14.2.5, A.6.1.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, AC-6(1), CM-6(a), PR.IP-2, SRG-OS-000480-GPOS-00228, SRG-OS-000480-GPOS-00227, OL08-00-020353, SV-248721r818666_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: XCCDF Value var_accounts_user_umask # promote to variable
  set_fact:
    var_accounts_user_umask: !!str 077
  tags:
    - always

- name: Check if umask in /etc/csh.cshrc is already set
  ansible.builtin.lineinfile:
    path: /etc/csh.cshrc
    regexp: ^(\s*)umask\s+.*
    state: absent
  check_mode: true
  changed_when: false
  register: umask_replace
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - accounts_umask_etc_csh_cshrc
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Replace user umask in /etc/csh.cshrc
  ansible.builtin.replace:
    path: /etc/csh.cshrc
    regexp: ^(\s*)umask(\s+).*
    replace: \g<1>umask\g<2>{{ var_accounts_user_umask }}
  when: umask_replace.found > 0
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - accounts_umask_etc_csh_cshrc
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure the Default umask is Appended Correctly
  ansible.builtin.lineinfile:
    create: true
    path: /etc/csh.cshrc
    line: umask {{ var_accounts_user_umask }}
  when: umask_replace.found == 0
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - accounts_umask_etc_csh_cshrc
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy


var_accounts_user_umask='077'


grep -q "^\s*umask" /etc/csh.cshrc && \
  sed -i -E -e "s/^(\s*umask).*/\1 $var_accounts_user_umask/g" /etc/csh.cshrc
if ! [ $? -eq 0 ]; then
    echo "umask $var_accounts_user_umask" >> /etc/csh.cshrc
fi

Rule   Ensure the Default Umask is Set Correctly in /etc/profile   [ref]

To ensure the default umask controlled by /etc/profile is set properly, add or correct the umask setting in /etc/profile to read as follows:
umask 077
Note that /etc/profile also reads scrips within /etc/profile.d directory. These scripts are also valid files to set umask value. Therefore, they should also be considered during the check and properly remediated, if necessary.
Rationale:
The umask value influences the permissions assigned to files when they are created. A misconfigured umask value could result in files with excessive permissions that can be read or written to by unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile
Identifiers and References

References:  BP28(R35), 18, APO13.01, BAI03.01, BAI03.02, BAI03.03, CCI-000366, 4.3.4.3.3, A.14.1.1, A.14.2.1, A.14.2.5, A.6.1.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, AC-6(1), CM-6(a), PR.IP-2, 8.6.1, SRG-OS-000480-GPOS-00228, SRG-OS-000480-GPOS-00227, OL08-00-020353, SV-248721r818666_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: XCCDF Value var_accounts_user_umask # promote to variable
  set_fact:
    var_accounts_user_umask: !!str 077
  tags:
    - always

- name: Ensure the Default Umask is Set Correctly in /etc/profile - Locate Profile
    Configuration Files Where umask Is Defined
  ansible.builtin.find:
    paths:
    - /etc/profile.d
    patterns:
    - sh.local
    - '*.sh'
    contains: ^[\s]*umask\s+\d+
  register: result_profile_d_files
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-8.6.1
  - accounts_umask_etc_profile
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure the Default Umask is Set Correctly in /etc/profile - Replace Existing
    umask Value in Files From /etc/profile.d
  ansible.builtin.replace:
    path: '{{ item.path }}'
    regexp: ^(\s*)umask\s+\d+
    replace: \1umask {{ var_accounts_user_umask }}
  loop: '{{ result_profile_d_files.files }}'
  register: result_umask_replaced_profile_d
  when: result_profile_d_files.matched
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-8.6.1
  - accounts_umask_etc_profile
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure the Default Umask is Set Correctly in /etc/profile - Ensure umask Is
    Set in /etc/profile if Not Already Set Elsewhere
  ansible.builtin.lineinfile:
    create: true
    mode: 420
    path: /etc/profile
    line: umask {{ var_accounts_user_umask }}
  when: not result_profile_d_files.matched
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-8.6.1
  - accounts_umask_etc_profile
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure the Default Umask is Set Correctly in /etc/profile - Ensure umask Value
    For All Existing umask Definition in /etc/profile
  ansible.builtin.replace:
    path: /etc/profile
    regexp: ^(\s*)umask\s+\d+
    replace: \1umask {{ var_accounts_user_umask }}
  register: result_umask_replaced_profile
  tags:
  - DISA-STIG-OL08-00-020353
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-8.6.1
  - accounts_umask_etc_profile
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict

var_accounts_user_umask='077'


readarray -t profile_files < <(find /etc/profile.d/ -type f -name '*.sh' -or -name 'sh.local')

for file in "${profile_files[@]}" /etc/profile; do
  grep -qE '^[^#]*umask' "$file" && sed -i "s/umask.*/umask $var_accounts_user_umask/g" "$file"
done

if ! grep -qrE '^[^#]*umask' /etc/profile*; then
  echo "umask $var_accounts_user_umask" >> /etc/profile
fi

Rule   Ensure the Default Umask is Set Correctly For Interactive Users   [ref]

Remove the UMASK environment variable from all interactive users initialization files.
Rationale:
The umask controls the default access mode assigned to newly created files. A umask of 077 limits new files to mode 700 or less permissive. Although umask can be represented as a four-digit number, the first digit representing special access modes is typically ignored or required to be 0. This requirement applies to the globally configured system defaults and the local interactive user defaults for each account on the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_umask_interactive_users
Identifiers and References

References:  CCI-000366, CCI-001814, SRG-OS-000480-GPOS-00227, SRG-OS-000480-GPOS-00228, OL08-00-020352, SV-248720r858597_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Ensure interactive local users are the owners of their respective initialization
    files
  ansible.builtin.shell:
    cmd: |-
      for dir in $(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $6}' /etc/passwd); do
        for file in $(find $dir -maxdepth 1 -type f -name ".*"); do
          if [ "$(basename $file)" != ".bash_history" ]; then
            sed -i 's/^\([\s]*umask\s*\)/#\1/g' $file
          fi
        done
      done
  tags:
  - DISA-STIG-OL08-00-020352
  - accounts_umask_interactive_users
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict

while IFS= read -r dir; do
    while IFS= read -r -d '' file; do
        if [ "$(basename $file)" != ".bash_history" ]; then
            sed -i 's/^\([\s]*umask\s*\)/#\1/g' "$file"
        fi
    done <   <(find $dir -maxdepth 1 -type f -name ".*" -print0)
done <   <(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $6}' /etc/passwd)

Rule   Ensure the Logon Failure Delay is Set Correctly in login.defs   [ref]

To ensure the logon failure delay controlled by /etc/login.defs is set properly, add or correct the FAIL_DELAY setting in /etc/login.defs to read as follows:
FAIL_DELAY 4
Rationale:
Increasing the time between a failed authentication attempt and re-prompting to enter credentials helps to slow a single-threaded brute force attack.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_logon_fail_delay
Identifiers and References

References:  11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, CCI-000366, 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-7(b), CM-6(a), PR.IP-1, SRG-OS-000480-GPOS-00226, OL08-00-020310, SV-248712r779702_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020310
  - NIST-800-53-AC-7(b)
  - NIST-800-53-CM-6(a)
  - accounts_logon_fail_delay
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy
- name: XCCDF Value var_accounts_fail_delay # promote to variable
  set_fact:
    var_accounts_fail_delay: !!str 4
  tags:
    - always

- name: Set accounts logon fail delay
  lineinfile:
    dest: /etc/login.defs
    regexp: ^FAIL_DELAY
    line: FAIL_DELAY {{ var_accounts_fail_delay }}
    create: true
  when: '"shadow-utils" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-020310
  - NIST-800-53-AC-7(b)
  - NIST-800-53-CM-6(a)
  - accounts_logon_fail_delay
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if rpm --quiet -q shadow-utils; then

var_accounts_fail_delay='4'


# 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' <<< "^FAIL_DELAY")

# shellcheck disable=SC2059
printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_fail_delay"

# 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 "^FAIL_DELAY\\>" "/etc/login.defs"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^FAIL_DELAY\\>.*/$escaped_formatted_output/gi" "/etc/login.defs"
else
    if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/login.defs"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   User Initialization Files Must Not Run World-Writable Programs   [ref]

Set the mode on files being executed by the user initialization files with the following command:
$ sudo chmod o-w FILE
Rationale:
If user start-up files execute world-writable programs, especially in unprotected directories, they could be maliciously modified to destroy user files or otherwise compromise the system at the user level. If the system is compromised at the user level, it is easier to elevate privileges to eventually compromise the system at the root and network level.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_user_dot_no_world_writable_programs
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010660, SV-248627r779447_rule


Complexity:low
Disruption:low
Strategy:restrict

readarray -t world_writable_files < <(find / -xdev -type f -perm -0002 2> /dev/null)
readarray -t interactive_home_dirs < <(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $6 }' /etc/passwd)

for world_writable in "${world_writable_files[@]}"; do
    for homedir in "${interactive_home_dirs[@]}"; do
        if grep -q -d skip "$world_writable" "$homedir"/.*; then
            chmod o-w $world_writable
            break
        fi
    done
done

Rule   Ensure that Users Path Contains Only Local Directories   [ref]

Ensure that all interactive user initialization files executable search path statements do not contain statements that will reference a working directory other than the users home directory.
Rationale:
The executable search path (typically the PATH environment variable) contains a list of directories for the shell to search to find executables. If this path includes the current working directory (other than the users home directory), executables in these directories may be executed instead of system commands. This variable is formatted as a colon-separated list of directories. If there is an empty entry, such as a leading or trailing colon or two consecutive colons, this is interpreted as the current working directory. If deviations from the default system search path for the local interactive user are required, they must be documented with the Information System Security Officer (ISSO).
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_user_home_paths_only
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010690, SV-248635r779471_rule

Rule   All Interactive Users Must Have A Home Directory Defined   [ref]

Assign home directories to all interactive users that currently do not have a home directory assigned. This rule checks if the home directory is properly defined in a folder which has at least one parent folder, like "user" in "/home/user" or "/remote/users/user". Therefore, this rule will report a finding for home directories like /users, /tmp or /.
Rationale:
If local interactive users are not assigned a valid home directory, there is no place for the storage and control of files they should own.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_user_interactive_home_directory_defined
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010720, SV-248638r779480_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - DISA-STIG-OL08-00-010720
  - accounts_user_interactive_home_directory_defined
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Create local_users variable from the getent output
  ansible.builtin.set_fact:
    local_users: '{{ ansible_facts.getent_passwd|dict2items }}'
  tags:
  - DISA-STIG-OL08-00-010720
  - accounts_user_interactive_home_directory_defined
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure interactive users have an exclusive home directory defined
  ansible.builtin.user:
    name: '{{ item.key }}'
    home: /home/{{ item.key }}
    create_home: false
  loop: '{{ local_users }}'
  when:
  - item.value[2]|int >= 1000
  - item.value[2]|int != 65534
  - not item.value[4] | regex_search('^\/\w*\/\w{1,}')
  tags:
  - DISA-STIG-OL08-00-010720
  - accounts_user_interactive_home_directory_defined
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict

for user in $(awk -F':' '{ if ($4 >= 1000 && $4 != 65534) print $1 }' /etc/passwd); do
    # This follows the same logic of evaluation of home directories as used in OVAL.
    if ! grep -q $user /etc/passwd | cut -d: -f6 | grep '^\/\w*\/\w\{1,\}'; then
        sed -i "s/\($user:x:[0-9]*:[0-9]*:.*:\).*\(:.*\)$/\1\/home\/$user\2/g" /etc/passwd;
    fi
done

Rule   All Interactive Users Home Directories Must Exist   [ref]

Create home directories to all interactive users that currently do not have a home directory assigned. Use the following commands to create the user home directory assigned in /etc/passwd:
$ sudo mkdir /home/USER
Rationale:
If a local interactive user has a home directory defined that does not exist, the user may be given access to the / directory as the current working directory upon logon. This could create a Denial of Service because the user would not be able to access their logon configuration files, and it may give them visibility to system files they normally would not be able to access.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_user_interactive_home_directory_exists
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010750, SV-248643r779495_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - DISA-STIG-OL08-00-010750
  - accounts_user_interactive_home_directory_exists
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Create local_users variable from the getent output
  ansible.builtin.set_fact:
    local_users: '{{ ansible_facts.getent_passwd|dict2items }}'
  tags:
  - DISA-STIG-OL08-00-010750
  - accounts_user_interactive_home_directory_exists
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure interactive users have a home directory exists
  ansible.builtin.user:
    name: '{{ item.key }}'
    create_home: true
  loop: '{{ local_users }}'
  when:
  - item.value[2]|int >= 1000
  - item.value[2]|int != 65534
  tags:
  - DISA-STIG-OL08-00-010750
  - accounts_user_interactive_home_directory_exists
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict

for user in $(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $1}' /etc/passwd); do
    mkhomedir_helper $user 0077;
done

Rule   All User Files and Directories In The Home Directory Must Be Group-Owned By The Primary Group   [ref]

Change the group of a local interactive users files and directories to a group that the interactive user is a member of. To change the group owner of a local interactive users files and directories, use the following command:
$ sudo chgrp USER_GROUP /home/USER/FILE_DIR
This rule ensures every file or directory under the home directory related to an interactive user is group-owned by an interactive user.
Warning:  Due to OVAL limitation, this rule can report a false negative in a specific situation where two interactive users swap the group-ownership of folders or files in their respective home directories.
Rationale:
If a local interactive users files are group-owned by a group of which the user is not a member, unintended users may be able to access them.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_users_home_files_groupownership
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010741, SV-248642r779492_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - DISA-STIG-OL08-00-010741
  - accounts_users_home_files_groupownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Create local_users variable from the getent output
  ansible.builtin.set_fact:
    local_users: '{{ ansible_facts.getent_passwd|dict2items }}'
  tags:
  - DISA-STIG-OL08-00-010741
  - accounts_users_home_files_groupownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Test for existence of home directories to avoid creating them, but only fixing
    ownership
  ansible.builtin.stat:
    path: '{{ item.value[4] }}'
  register: path_exists
  loop: '{{ local_users }}'
  when:
  - item.value[1]|int >= 1000
  - item.value[1]|int != 65534
  tags:
  - DISA-STIG-OL08-00-010741
  - accounts_users_home_files_groupownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure interactive local users are the owners of their respective home directories
  ansible.builtin.file:
    path: '{{ item.0.value[4] }}'
    group: '{{ item.0.value[2] }}'
    recurse: true
  loop: '{{ local_users|zip(path_exists.results)|list }}'
  when: item.1.stat is defined and item.1.stat.exists
  tags:
  - DISA-STIG-OL08-00-010741
  - accounts_users_home_files_groupownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict

for user in $(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $1 }' /etc/passwd); do
    home_dir=$(getent passwd $user | cut -d: -f6)
    group=$(getent passwd $user | cut -d: -f4)
    # Only update the group-ownership when necessary. This will avoid changing the inode timestamp
    # when the group is already defined as expected, therefore not impacting in possible integrity
    # check systems that also check inodes timestamps.
    find $home_dir -not -group $group -exec chgrp -f $group {} \;
done

Rule   All User Files and Directories In The Home Directory Must Have Mode 0750 Or Less Permissive   [ref]

Set the mode on files and directories in the local interactive user home directory with the following command:
$ sudo chmod 0750 /home/USER/FILE_DIR
Files that begin with a "." are excluded from this requirement.
Rationale:
If a local interactive user files have excessive permissions, unintended users may be able to access or modify them.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_users_home_files_permissions
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010731, SV-248640r779486_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - DISA-STIG-OL08-00-010731
  - accounts_users_home_files_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Create local_users variable from the getent output
  ansible.builtin.set_fact:
    local_users: '{{ ansible_facts.getent_passwd|dict2items }}'
  tags:
  - DISA-STIG-OL08-00-010731
  - accounts_users_home_files_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Test for existence home directories to avoid creating them.
  ansible.builtin.stat:
    path: '{{ item.value[4] }}'
  register: path_exists
  loop: '{{ local_users }}'
  when:
  - item.value[1]|int >= 1000
  - item.value[1]|int != 65534
  tags:
  - DISA-STIG-OL08-00-010731
  - accounts_users_home_files_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure interactive local users have proper permissions on their respective
    home directories
  ansible.builtin.file:
    path: '{{ item.0.value[4] }}'
    mode: u-s,g-w-s,o=-
    follow: false
    recurse: true
  loop: '{{ local_users|zip(path_exists.results)|list }}'
  when: item.1.stat is defined and item.1.stat.exists
  tags:
  - DISA-STIG-OL08-00-010731
  - accounts_users_home_files_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict

for home_dir in $(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $6 }' /etc/passwd); do
    # Only update the permissions when necessary. This will avoid changing the inode timestamp when
    # the permission is already defined as expected, therefore not impacting in possible integrity
    # check systems that also check inodes timestamps.
    find "$home_dir" -perm /7027 -exec chmod u-s,g-w-s,o=- {} \;
done

Rule   All Interactive User Home Directories Must Be Group-Owned By The Primary Group   [ref]

Change the group owner of interactive users home directory to the group found in /etc/passwd. To change the group owner of interactive users home directory, use the following command:
$ sudo chgrp USER_GROUP /home/USER
This rule ensures every home directory related to an interactive user is group-owned by an interactive user. It also ensures that interactive users are group-owners of one and only one home directory.
Warning:  Due to OVAL limitation, this rule can report a false negative in a specific situation where two interactive users swap the group-ownership of their respective home directories.
Rationale:
If the Group Identifier (GID) of a local interactive users home directory is not the same as the primary GID of the user, this would allow unauthorized access to the users files, and users that share the same group may not be able to access files that they legitimately should.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupownership_home_directories
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010740, SV-248641r880567_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - DISA-STIG-OL08-00-010740
  - file_groupownership_home_directories
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Create local_users variable from the getent output
  ansible.builtin.set_fact:
    local_users: '{{ ansible_facts.getent_passwd|dict2items }}'
  tags:
  - DISA-STIG-OL08-00-010740
  - file_groupownership_home_directories
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Test for existence of home directories to avoid creating them, but only fixing
    group ownership
  ansible.builtin.stat:
    path: '{{ item.value[4] }}'
  register: path_exists
  loop: '{{ local_users }}'
  when:
  - item.value[1]|int >= 1000
  - item.value[1]|int != 65534
  tags:
  - DISA-STIG-OL08-00-010740
  - file_groupownership_home_directories
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure interactive local users are the group-owners of their respective home
    directories
  ansible.builtin.file:
    path: '{{ item.0.value[4] }}'
    group: '{{ item.0.value[2] }}'
  loop: '{{ local_users|zip(path_exists.results)|list }}'
  when: item.1.stat is defined and item.1.stat.exists
  tags:
  - DISA-STIG-OL08-00-010740
  - file_groupownership_home_directories
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict

awk -F':' '{ if ($3 >= 1000 && $3 != 65534) system("chgrp -f " $4" "$6) }' /etc/passwd

Rule   Verify Group Who Owns lastlog Command   [ref]

To properly set the group owner of /var/log/lastlog, run the command:
$ sudo chgrp root /var/log/lastlog
Rationale:
Unauthorized disclosure of the contents of the /var/log/lastlog file can reveal system data to attackers, thus compromising its confidentiality.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupownership_lastlog
Identifiers and References

References:  CCI-001314, SI-11(b), SI-11(c), SI-11.1(iv), SRG-OS-000206-GPOS-00084, OL08-00-020264, SV-248707r779687_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /usr/bin/lastlog
  stat:
    path: /usr/bin/lastlog
  register: file_exists
  tags:
  - DISA-STIG-OL08-00-020264
  - NIST-800-53-SI-11(b)
  - NIST-800-53-SI-11(c)
  - NIST-800-53-SI-11.1(iv)
  - configure_strategy
  - file_groupownership_lastlog
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /usr/bin/lastlog
  file:
    path: /usr/bin/lastlog
    group: '0'
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-020264
  - NIST-800-53-SI-11(b)
  - NIST-800-53-SI-11(c)
  - NIST-800-53-SI-11.1(iv)
  - configure_strategy
  - file_groupownership_lastlog
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
chgrp 0 /usr/bin/lastlog

Rule   Verify Owner on lastlog Command   [ref]

To properly set the owner of /usr/bin/lastlog, run the command:
$ sudo chown root /usr/bin/lastlog 
Rationale:
Unauthorized disclosure of the contents of the /var/log/lastlog file can reveal system data to attackers, thus compromising its confidentiality.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_ownership_lastlog
Identifiers and References

References:  CCI-001314, SI-11(b), SI-11(c), SI-11.1(iv), SRG-OS-000206-GPOS-00084, OL08-00-020263, SV-248706r779684_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /usr/bin/lastlog
  stat:
    path: /usr/bin/lastlog
  register: file_exists
  tags:
  - DISA-STIG-OL08-00-020263
  - NIST-800-53-SI-11(b)
  - NIST-800-53-SI-11(c)
  - NIST-800-53-SI-11.1(iv)
  - configure_strategy
  - file_ownership_lastlog
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /usr/bin/lastlog
  file:
    path: /usr/bin/lastlog
    owner: '0'
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-020263
  - NIST-800-53-SI-11(b)
  - NIST-800-53-SI-11(c)
  - NIST-800-53-SI-11.1(iv)
  - configure_strategy
  - file_ownership_lastlog
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
chown 0 /usr/bin/lastlog

Rule   Ensure All User Initialization Files Have Mode 0740 Or Less Permissive   [ref]

Set the mode of the user initialization files to 0740 with the following command:
$ sudo chmod 0740 /home/USER/.INIT_FILE
Rationale:
Local initialization files are used to configure the user's shell environment upon logon. Malicious modification of these files could compromise accounts upon logon.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permission_user_init_files
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010770, SV-248645r779501_rule

Rule   All Interactive User Home Directories Must Have mode 0750 Or Less Permissive   [ref]

Change the mode of interactive users home directories to 0750. To change the mode of interactive users home directory, use the following command:
$ sudo chmod 0750 /home/USER
Rationale:
Excessive permissions on local interactive user home directories may allow unauthorized access to user files by other users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_home_directories
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010730, SV-248639r779483_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - DISA-STIG-OL08-00-010730
  - file_permissions_home_directories
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Create local_users variable from the getent output
  ansible.builtin.set_fact:
    local_users: '{{ ansible_facts.getent_passwd|dict2items }}'
  tags:
  - DISA-STIG-OL08-00-010730
  - file_permissions_home_directories
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Test for existence home directories to avoid creating them.
  ansible.builtin.stat:
    path: '{{ item.value[4] }}'
  register: path_exists
  loop: '{{ local_users }}'
  when:
  - item.value[1]|int >= 1000
  - item.value[1]|int != 65534
  tags:
  - DISA-STIG-OL08-00-010730
  - file_permissions_home_directories
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure interactive local users have proper permissions on their respective
    home directories
  ansible.builtin.file:
    path: '{{ item.0.value[4] }}'
    mode: u-s,g-w-s,o=-
    follow: false
    recurse: false
  loop: '{{ local_users|zip(path_exists.results)|list }}'
  when: item.1.stat is defined and item.1.stat.exists
  tags:
  - DISA-STIG-OL08-00-010730
  - file_permissions_home_directories
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict

for home_dir in $(awk -F':' '{ if ($3 >= 1000 && $3 != 65534) print $6 }' /etc/passwd); do
    # Only update the permissions when necessary. This will avoid changing the inode timestamp when
    # the permission is already defined as expected, therefore not impacting in possible integrity
    # check systems that also check inodes timestamps.
    find "$home_dir" -maxdepth 0 -perm /7027 -exec chmod u-s,g-w-s,o=- {} \;
done

Rule   Verify Permissions on lastlog Command   [ref]

To properly set the permissions of /usr/bin/lastlog, run the command:
$ sudo chmod 0750 /usr/bin/lastlog
Rationale:
Unauthorized disclosure of the contents of the /var/log/lastlog file can reveal system data to attackers, thus compromising its confidentiality.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_lastlog
Identifiers and References

References:  CCI-001314, SI-11(b), SI-11(c), SI-11.1(iv), SRG-OS-000206-GPOS-00084, OL08-00-020262, SV-248705r779681_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /usr/bin/lastlog
  stat:
    path: /usr/bin/lastlog
  register: file_exists
  tags:
  - DISA-STIG-OL08-00-020262
  - NIST-800-53-SI-11(b)
  - NIST-800-53-SI-11(c)
  - NIST-800-53-SI-11.1(iv)
  - configure_strategy
  - file_permissions_lastlog
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-ws,o-xwrt on /usr/bin/lastlog
  file:
    path: /usr/bin/lastlog
    mode: u-s,g-ws,o-xwrt
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-020262
  - NIST-800-53-SI-11(b)
  - NIST-800-53-SI-11(c)
  - NIST-800-53-SI-11.1(iv)
  - configure_strategy
  - file_permissions_lastlog
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure





chmod u-s,g-ws,o-xwrt /usr/bin/lastlog
Group   System Accounting with auditd   Group contains 11 groups and 86 rules
[ref]   The audit service provides substantial capabilities for recording system activities. By default, the service audits about SELinux AVC denials and certain types of security-relevant events such as system logins, account modifications, and authentication events performed by programs such as sudo. Under its default configuration, auditd has modest disk space requirements, and should not noticeably impact system performance.

NOTE: The Linux Audit daemon auditd can be configured to use the augenrules program to read audit rules files (*.rules) located in /etc/audit/rules.d location and compile them to create the resulting form of the /etc/audit/audit.rules configuration file during the daemon startup (default configuration). Alternatively, the auditd daemon can use the auditctl utility to read audit rules from the /etc/audit/audit.rules configuration file during daemon startup, and load them into the kernel. The expected behavior is configured via the appropriate ExecStartPost directive setting in the /usr/lib/systemd/system/auditd.service configuration file. To instruct the auditd daemon to use the augenrules program to read audit rules (default configuration), use the following setting:
ExecStartPost=-/sbin/augenrules --load
in the /usr/lib/systemd/system/auditd.service configuration file. In order to instruct the auditd daemon to use the auditctl utility to read audit rules, use the following setting:
ExecStartPost=-/sbin/auditctl -R /etc/audit/audit.rules
in the /usr/lib/systemd/system/auditd.service configuration file. Refer to [Service] section of the /usr/lib/systemd/system/auditd.service configuration file for further details.

Government networks often have substantial auditing requirements and auditd can be configured to meet these requirements. Examining some example audit records demonstrates how the Linux audit system satisfies common requirements. The following example from Red Hat Enterprise Linux 7 Documentation available at https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html-single/selinux_users_and_administrators_guide/index#sect-Security-Enhanced_Linux-Fixing_Problems-Raw_Audit_Messages shows the substantial amount of information captured in a two typical "raw" audit messages, followed by a breakdown of the most important fields. In this example the message is SELinux-related and reports an AVC denial (and the associated system call) that occurred when the Apache HTTP Server attempted to access the /var/www/html/file1 file (labeled with the samba_share_t type):
type=AVC msg=audit(1226874073.147:96): avc:  denied  { getattr } for pid=2465 comm="httpd"
path="/var/www/html/file1" dev=dm-0 ino=284133 scontext=unconfined_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:samba_share_t:s0 tclass=file

type=SYSCALL msg=audit(1226874073.147:96): arch=40000003 syscall=196 success=no exit=-13
a0=b98df198 a1=bfec85dc a2=54dff4 a3=2008171 items=0 ppid=2463 pid=2465 auid=502 uid=48
gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=6 comm="httpd"
exe="/usr/sbin/httpd" subj=unconfined_u:system_r:httpd_t:s0 key=(null)
  • msg=audit(1226874073.147:96)
    • The number in parentheses is the unformatted time stamp (Epoch time) for the event, which can be converted to standard time by using the date command.
  • { getattr }
    • The item in braces indicates the permission that was denied. getattr indicates the source process was trying to read the target file's status information. This occurs before reading files. This action is denied due to the file being accessed having the wrong label. Commonly seen permissions include getattr, read, and write.
  • comm="httpd"
    • The executable that launched the process. The full path of the executable is found in the exe= section of the system call (SYSCALL) message, which in this case, is exe="/usr/sbin/httpd".
  • path="/var/www/html/file1"
    • The path to the object (target) the process attempted to access.
  • scontext="unconfined_u:system_r:httpd_t:s0"
    • The SELinux context of the process that attempted the denied action. In this case, it is the SELinux context of the Apache HTTP Server, which is running in the httpd_t domain.
  • tcontext="unconfined_u:object_r:samba_share_t:s0"
    • The SELinux context of the object (target) the process attempted to access. In this case, it is the SELinux context of file1. Note: the samba_share_t type is not accessible to processes running in the httpd_t domain.
  • From the system call (SYSCALL) message, two items are of interest:
    • success=no: indicates whether the denial (AVC) was enforced or not. success=no indicates the system call was not successful (SELinux denied access). success=yes indicates the system call was successful - this can be seen for permissive domains or unconfined domains, such as initrc_t and kernel_t.
    • exe="/usr/sbin/httpd": the full path to the executable that launched the process, which in this case, is exe="/usr/sbin/httpd".
Group   Configure auditd Rules for Comprehensive Auditing   Group contains 8 groups and 71 rules
[ref]   The auditd program can perform comprehensive monitoring of system activity. This section describes recommended configuration settings for comprehensive auditing, but a full description of the auditing system's capabilities is beyond the scope of this guide. The mailing list linux-audit@redhat.com exists to facilitate community discussion of the auditing system.

The audit subsystem supports extensive collection of events, including:
  • Tracing of arbitrary system calls (identified by name or number) on entry or exit.
  • Filtering by PID, UID, call success, system call argument (with some limitations), etc.
  • Monitoring of specific files for modifications to the file's contents or metadata.

Auditing rules at startup are controlled by the file /etc/audit/audit.rules. Add rules to it to meet the auditing requirements for your organization. Each line in /etc/audit/audit.rules represents a series of arguments that can be passed to auditctl and can be individually tested during runtime. See documentation in /usr/share/doc/audit-VERSION and in the related man pages for more details.

If copying any example audit rulesets from /usr/share/doc/audit-VERSION, be sure to comment out the lines containing arch= which are not appropriate for your system's architecture. Then review and understand the following rules, ensuring rules are activated as needed for the appropriate architecture.

After reviewing all the rules, reading the following sections, and editing as needed, the new rules can be activated as follows:
$ sudo service auditd restart
Group   Record Events that Modify the System's Discretionary Access Controls   Group contains 13 rules
[ref]   At a minimum, the audit system should collect file permission changes for all users and root. Note that the "-F arch=b32" lines should be present even on a 64 bit system. These commands identify system calls for auditing. Even if the system is 64 bit it can still execute 32 bit system calls. Additionally, these rules can be configured in a number of ways while still achieving the desired effect. An example of this is that the "-S" calls could be split up and placed on separate lines, however, this is less efficient. Add the following to /etc/audit/audit.rules:
-a always,exit -F arch=b32 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If your system is 64 bit then these lines should be duplicated and the arch=b32 replaced with arch=b64 as follows:
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod

Rule   Record Events that Modify the System's Discretionary Access Controls - chmod   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_chmod
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, OL08-00-030490, SV-248791r853829_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_chmod
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit chmod tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_chmod
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for chmod for 32bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - chmod
      syscall_grouping:
      - chmod
      - fchmod
      - fchmodat

  - name: Check existence of chmod in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - chmod
      syscall_grouping:
      - chmod
      - fchmod
      - fchmodat

  - name: Check existence of chmod in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_chmod
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for chmod for 64bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - chmod
      syscall_grouping:
      - chmod
      - fchmod
      - fchmodat

  - name: Check existence of chmod in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - chmod
      syscall_grouping:
      - chmod
      - fchmod
      - fchmodat

  - name: Check existence of chmod in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_chmod
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="chmod"
	KEY="perm_mod"
	SYSCALL_GROUPING="chmod fchmod fchmodat"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - chown   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_chown
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219, OL08-00-030480, SV-248790r853828_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_chown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit chown tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_chown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for chown for 32bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - chown
      syscall_grouping:
      - chown
      - fchown
      - fchownat
      - lchown

  - name: Check existence of chown in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - chown
      syscall_grouping:
      - chown
      - fchown
      - fchownat
      - lchown

  - name: Check existence of chown in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_chown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for chown for 64bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - chown
      syscall_grouping:
      - chown
      - fchown
      - fchownat
      - lchown

  - name: Check existence of chown in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - chown
      syscall_grouping:
      - chown
      - fchown
      - fchownat
      - lchown

  - name: Check existence of chown in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_chown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="chown"
	KEY="perm_mod"
	SYSCALL_GROUPING="chown fchown fchownat lchown"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - fchmod   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fchmod
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, OL08-00-030490, SV-248791r853829_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchmod
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit fchmod tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchmod
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fchmod for 32bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchmod
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fchmod for 64bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchmod
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fchmod"
	KEY="perm_mod"
	SYSCALL_GROUPING="chmod fchmod fchmodat"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - fchmodat   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fchmodat
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, OL08-00-030490, SV-248791r853829_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchmodat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit fchmodat tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchmodat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fchmodat for 32bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchmodat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fchmodat for 64bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030490
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchmodat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fchmodat"
	KEY="perm_mod"
	SYSCALL_GROUPING="chmod fchmod fchmodat"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - fchown   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fchown
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219, OL08-00-030480, SV-248790r853828_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit fchown tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fchown for 32bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fchown for 64bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fchown"
	KEY="perm_mod"
	SYSCALL_GROUPING="chown fchown fchownat lchown"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - fchownat   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fchownat
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219, OL08-00-030480, SV-248790r853828_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchownat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit fchownat tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchownat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fchownat for 32bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchownat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fchownat for 64bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fchownat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fchownat"
	KEY="perm_mod"
	SYSCALL_GROUPING="chown fchown fchownat lchown"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - fremovexattr   [ref]

At a minimum, the audit system should collect file permission changes for all users and root.

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S fremovexattr -F auid=0 -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S fremovexattr -F auid=0 -F key=perm_mod


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S fremovexattr -F auid=0 -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S fremovexattr -F auid=0 -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fremovexattr
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000466-GPOS-00210, SRG-OS-000468-GPOS-00212, SRG-OS-000064-GPOS-00033, OL08-00-030200, SV-248748r853804_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fremovexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit fremovexattr tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fremovexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fremovexattr for 32bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fremovexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fremovexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fremovexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fremovexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fremovexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fremovexattr for 64bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fremovexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fremovexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fremovexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fremovexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fremovexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fremovexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done



for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid=0"
	SYSCALL="fremovexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - fsetxattr   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S fsetxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S fsetxattr -F auid=0 -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S fsetxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S fsetxattr -F auid=0 -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fsetxattr
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000466-GPOS-00210, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000064-GPOS-00033, OL08-00-030200, SV-248748r853804_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fsetxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit fsetxattr tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fsetxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fsetxattr for 32bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fsetxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fsetxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fsetxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fsetxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fsetxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for fsetxattr for 64bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fsetxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fsetxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fsetxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - fsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of fsetxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_fsetxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fsetxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done



for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid=0"
	SYSCALL="fsetxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - lchown   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_lchown
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219, OL08-00-030480, SV-248790r853828_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lchown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit lchown tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lchown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for lchown for 32bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lchown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for lchown for 64bit platform
  block:

  - name: Declare list of syscalls
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030480
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lchown
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="lchown"
	KEY="perm_mod"
	SYSCALL_GROUPING="chown fchown fchownat lchown"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - lremovexattr   [ref]

At a minimum, the audit system should collect file permission changes for all users and root.

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S lremovexattr -F auid=0 -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S lremovexattr -F auid=0 -F key=perm_mod


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S lremovexattr -F auid=0 -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S lremovexattr -F auid=0 -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_lremovexattr
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000466-GPOS-00210, SRG-OS-000064-GPOS-00033, OL08-00-030200, SV-248748r853804_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lremovexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit lremovexattr tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lremovexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for lremovexattr for 32bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lremovexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lremovexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lremovexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lremovexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lremovexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for lremovexattr for 64bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lremovexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lremovexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lremovexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lremovexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lremovexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lremovexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="lremovexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done



for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid=0"
	SYSCALL="lremovexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - lsetxattr   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S lsetxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S lsetxattr -F auid=0 -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S lsetxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S lsetxattr -F auid=0 -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_lsetxattr
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000466-GPOS-00210, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000064-GPOS-00033, OL08-00-030200, SV-248748r853804_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lsetxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit lsetxattr tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lsetxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for lsetxattr for 32bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lsetxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lsetxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lsetxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lsetxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lsetxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for lsetxattr for 64bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lsetxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lsetxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lsetxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - lsetxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of lsetxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_lsetxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="lsetxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done



for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid=0"
	SYSCALL="lsetxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - removexattr   [ref]

At a minimum, the audit system should collect file permission changes for all users and root.

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S removexattr -F auid=0 -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S removexattr -F auid=0 -F key=perm_mod


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S removexattr -F auid=0 -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S removexattr -F auid=0 -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_removexattr
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000466-GPOS-00210, SRG-OS-000064-GPOS-00033, OL08-00-030200, SV-248748r853804_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_removexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit removexattr tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_removexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for removexattr for 32bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - removexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of removexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - removexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of removexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - removexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of removexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - removexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of removexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_removexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for removexattr for 64bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - removexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of removexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - removexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of removexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - removexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of removexattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - removexattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of removexattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_removexattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="removexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done



for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid=0"
	SYSCALL="removexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify the System's Discretionary Access Controls - setxattr   [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S setxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S setxattr -F auid=0 -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b32 -S setxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
-a always,exit -F arch=b64 -S setxattr -F auid=0 -F key=perm_mod
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_setxattr
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.5, 10.3.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000466-GPOS-00210, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000458-GPOS-00203, OL08-00-030200, SV-248748r853804_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_setxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Set architecture for audit setxattr tasks
  set_fact:
    audit_arch: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_setxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for setxattr for 32bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - setxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of setxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - setxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of setxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - setxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of setxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - setxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of setxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_setxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for setxattr for 64bit platform
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - setxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of setxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - setxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of setxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
        key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
        -F auid!=unset -F key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - setxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of setxattr in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
    set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls:
      - setxattr
      syscall_grouping:
      - fremovexattr
      - lremovexattr
      - removexattr
      - fsetxattr
      - lsetxattr
      - setxattr

  - name: Check existence of setxattr in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
        |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
        join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
        key=perm_mod
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030200
  - NIST-800-171-3.1.7
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.5
  - PCI-DSSv4-10.3.4
  - audit_rules_dac_modification_setxattr
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="setxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done



for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid=0"
	SYSCALL="setxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Record Execution Attempts to Run ACL Privileged Commands   Group contains 2 rules
[ref]   At a minimum, the audit system should collect the execution of ACL privileged commands for all users and root.

Rule   Record Any Attempts to Run chacl   [ref]

At a minimum, the audit system should collect any execution attempt of the chacl 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/chacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Without generating audit records that are specific to the security and mission needs of the organization, it would be difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. Audit records can be generated from various components within the information system (e.g., module or policy filter).
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_execution_chacl
Identifiers and References

References:  CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, OL08-00-030570, SV-248799r853832_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030570
  - audit_rules_execution_chacl
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/chacl
  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/chacl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chacl -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chacl -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chacl -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030570
  - audit_rules_execution_chacl
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/chacl -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Any Attempts to Run setfacl   [ref]

At a minimum, the audit system should collect any execution attempt of the setfacl 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/setfacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Without generating audit records that are specific to the security and mission needs of the organization, it would be difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. Audit records can be generated from various components within the information system (e.g., module or policy filter).
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_execution_setfacl
Identifiers and References

References:  CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030330, SV-248770r853818_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030330
  - audit_rules_execution_setfacl
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/setfacl
  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/setfacl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/setfacl -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/setfacl -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/setfacl -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030330
  - audit_rules_execution_setfacl
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/setfacl -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Record Execution Attempts to Run SELinux Privileged Commands   Group contains 4 rules
[ref]   At a minimum, the audit system should collect the execution of SELinux privileged commands for all users and root.

Rule   Record Any Attempts to Run chcon   [ref]

At a minimum, the audit system should collect any execution attempt of the chcon command for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_execution_chcon
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, OL08-00-030260, SV-248754r853806_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030260
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_execution_chcon
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/chcon
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chcon -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chcon -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chcon -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030260
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_execution_chcon
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/chcon -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Any Attempts to Run semanage   [ref]

At a minimum, the audit system should collect any execution attempt of the semanage command for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_execution_semanage
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, OL08-00-030313, SV-248764r779858_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030313
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_execution_semanage
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/semanage
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/semanage -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/semanage -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/semanage -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030313
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_execution_semanage
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/semanage -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Any Attempts to Run setfiles   [ref]

At a minimum, the audit system should collect any execution attempt of the setfiles command for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_execution_setfiles
Identifiers and References

References:  CCI-000169, CCI-000172, CCI-002884, AU-2(d), AU-12(c), AC-6(9), CM-6(a), SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, OL08-00-030314, SV-248765r779861_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030314
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_execution_setfiles
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/setfiles
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/setfiles -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setfiles -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setfiles -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030314
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_execution_setfiles
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/setfiles -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Any Attempts to Run setsebool   [ref]

At a minimum, the audit system should collect any execution attempt of the setsebool command for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_execution_setsebool
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, OL08-00-030316, SV-248767r853815_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030316
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_execution_setsebool
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/setsebool
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/setsebool -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setsebool -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setsebool -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030316
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_execution_setsebool
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/setsebool -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Record File Deletion Events by User   Group contains 5 rules
[ref]   At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=unset -F key=delete

Rule   Ensure auditd Collects File Deletion Events by User - rename   [ref]

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rename -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rename -F auid>=1000 -F auid!=unset -F key=delete
Rationale:
Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_rename
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.4, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.1.1, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 10.2.1.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, OL08-00-030361, SV-248774r853822_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_file_deletion_events_rename
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="rename"
	KEY="delete"
	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects File Deletion Events by User - renameat   [ref]

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S renameat -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S renameat -F auid>=1000 -F auid!=unset -F key=delete
Rationale:
Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_renameat
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.4, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.1.1, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 10.2.1.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, OL08-00-030361, SV-248774r853822_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_file_deletion_events_renameat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="renameat"
	KEY="delete"
	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects File Deletion Events by User - rmdir   [ref]

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rmdir -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rmdir -F auid>=1000 -F auid!=unset -F key=delete
Rationale:
Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_rmdir
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.4, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.1.1, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 10.2.1.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, OL08-00-030361, SV-248774r853822_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_file_deletion_events_rmdir
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="rmdir"
	KEY="delete"
	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects File Deletion Events by User - unlinkat   [ref]

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S unlinkat -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S unlinkat -F auid>=1000 -F auid!=unset -F key=delete
Rationale:
Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_unlinkat
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.4, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.1.1, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 10.2.1.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, OL08-00-030361, SV-248774r853822_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030361
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_file_deletion_events_unlinkat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="unlinkat"
	KEY="delete"
	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Record Unauthorized Access Attempts Events to Files (unsuccessful)   Group contains 6 rules
[ref]   At a minimum, the audit system should collect unauthorized file accesses for all users and root. Note that the "-F arch=b32" lines should be present even on a 64 bit system. These commands identify system calls for auditing. Even if the system is 64 bit it can still execute 32 bit system calls. Additionally, these rules can be configured in a number of ways while still achieving the desired effect. An example of this is that the "-S" calls could be split up and placed on separate lines, however, this is less efficient. Add the following to /etc/audit/audit.rules:
-a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If your system is 64 bit then these lines should be duplicated and the arch=b32 replaced with arch=b64 as follows:
-a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access

Rule   Record Unsuccessful Access Attempts to Files - creat   [ref]

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_creat
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 10.2.1.1, 10.2.1.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, OL08-00-030420, SV-248784r853827_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.4
  - audit_rules_unsuccessful_file_modification_creat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="creat"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Unsuccessful Access Attempts to Files - ftruncate   [ref]

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S ftruncate -F 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
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_ftruncate
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 10.2.1.1, 10.2.1.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, OL08-00-030420, SV-248784r853827_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.4
  - audit_rules_unsuccessful_file_modification_ftruncate
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="ftruncate"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Unsuccessful Access Attempts to Files - open   [ref]

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_open
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 10.2.1.1, 10.2.1.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, OL08-00-030420, SV-248784r853827_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.4
  - audit_rules_unsuccessful_file_modification_open
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="open"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Unsuccessful Access Attempts to Files - open_by_handle_at   [ref]

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_open_by_handle_at
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 10.2.1.1, 10.2.1.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, OL08-00-030420, SV-248784r853827_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit
    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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.4
  - audit_rules_unsuccessful_file_modification_open_by_handle_at
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="open_by_handle_at"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Unsuccessful Access Attempts to Files - openat   [ref]

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_openat
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 10.2.1.1, 10.2.1.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, OL08-00-030420, SV-248784r853827_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.4
  - audit_rules_unsuccessful_file_modification_openat
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="openat"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Unsuccessful Access Attempts to Files - truncate   [ref]

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F arch=b32 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:
Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_truncate
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 10.2.1.1, 10.2.1.4, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, OL08-00-030420, SV-248784r853827_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.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 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030420
  - NIST-800-171-3.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
  - PCI-DSSv4-10.2.1.1
  - PCI-DSSv4-10.2.1.4
  - audit_rules_unsuccessful_file_modification_truncate
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="truncate"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Record Information on Kernel Modules Loading and Unloading   Group contains 3 rules
[ref]   To capture kernel module loading and unloading events, use following lines, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit:
-a always,exit -F arch=ARCH -S init_module,delete_module -F key=modules
Place to add the lines depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the lines to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the lines to file /etc/audit/audit.rules.

Rule   Ensure auditd Collects Information on Kernel Module Unloading - delete_module   [ref]

To capture kernel module unloading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit:
-a always,exit -F arch=ARCH -S delete_module -F auid>=1000 -F auid!=unset -F key=modules
Place to add the line depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the line to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the line to file /etc/audit/audit.rules.
Rationale:
The removal of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_kernel_module_loading_delete
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 10.2.1.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000471-GPOS-00216, SRG-OS-000477-GPOS-00222, OL08-00-030390, SV-248781r853824_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030390
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030390
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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 32bit 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+)* -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/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+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 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=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+)* -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=module-change
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030390
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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 64bit 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+)* -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/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+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 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=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+)* -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=module-change
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030390
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_kernel_module_loading_delete
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
# Note: 32-bit and 64-bit kernel syscall numbers not always line up =>
#       it's required on a 64-bit system to check also for the presence
#       of 32-bit's equivalent of the corresponding rule.
#       (See `man 7 audit.rules` for details )
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	
	SYSCALL="delete_module"
	KEY="modules"
	SYSCALL_GROUPING="delete_module"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on Kernel Module Loading and Unloading - finit_module   [ref]

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d 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 auid>=1000 -F auid!=unset -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 auid>=1000 -F auid!=unset -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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_kernel_module_loading_finit
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 10.2.1.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000471-GPOS-00216, SRG-OS-000477-GPOS-00222, OL08-00-030360, SV-248773r853821_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030360
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030360
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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+)* -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/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+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 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=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+)* -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=module-change
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030360
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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+)* -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/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+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 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=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+)* -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=module-change
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030360
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_kernel_module_loading_finit
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
# Note: 32-bit and 64-bit kernel syscall numbers not always line up =>
#       it's required on a 64-bit system to check also for the presence
#       of 32-bit's equivalent of the corresponding rule.
#       (See `man 7 audit.rules` for details )
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	
	SYSCALL="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" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on Kernel Module Loading - init_module   [ref]

To capture kernel module loading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit:
-a always,exit -F arch=ARCH -S init_module -F auid>=1000 -F auid!=unset -F key=modules
Place to add the line depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the line to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the line to file /etc/audit/audit.rules.
Rationale:
The addition of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_kernel_module_loading_init
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 10.2.1.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000471-GPOS-00216, SRG-OS-000477-GPOS-00222, OL08-00-030360, SV-248773r853821_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030360
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - DISA-STIG-OL08-00-030360
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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 32bit 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+)* -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/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+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 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=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+)* -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=module-change
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030360
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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 64bit 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+)* -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/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+)+)( -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 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=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+)* -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=module-change
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - DISA-STIG-OL08-00-030360
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_kernel_module_loading_init
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
# Note: 32-bit and 64-bit kernel syscall numbers not always line up =>
#       it's required on a 64-bit system to check also for the presence
#       of 32-bit's equivalent of the corresponding rule.
#       (See `man 7 audit.rules` for details )
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	
	SYSCALL="init_module"
	KEY="modules"
	SYSCALL_GROUPING="init_module finit_module"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Record Attempts to Alter Logon and Logout Events   Group contains 2 rules
Group   Record Information on the Use of Privileged Commands   Group contains 20 rules
[ref]   At a minimum, the audit system should collect the execution of privileged commands for all users and root.

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - chage   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_chage
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, OL08-00-030250, SV-248753r853805_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030250
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_chage
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/chage
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chage -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chage -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chage -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030250
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/chage -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - chsh   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_chsh
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030410, SV-248783r853826_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030410
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_chsh
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/chsh
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chsh -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chsh -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chsh -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030410
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/chsh -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - crontab   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_crontab
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030400, SV-248782r853825_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030400
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_crontab
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/crontab
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/crontab -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/crontab -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/crontab -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030400
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/crontab -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - gpasswd   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_gpasswd
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030370, SV-248779r853823_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030370
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_gpasswd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/gpasswd
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/gpasswd -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/gpasswd -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/gpasswd -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030370
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/gpasswd -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - kmod   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-w /usr/bin/kmod -p x -k modules
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:
-w /usr/bin/kmod -p x -k modules
Rationale:
Without generating audit records that are specific to the security and mission needs of the organization, it would be difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. Audit records can be generated from various components within the information system (e.g., module or policy filter).
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_kmod
Identifiers and References

References:  BP28(R73), CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, AU-3, AU-3.1, AU-12(a), AU-12.1(ii), AU-12.1(iv)AU-12(c), MA-4(1)(a), SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000471-GPOS-00216, SRG-OS-000477-GPOS-00222, OL08-00-030580, SV-248800r853833_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030580
  - NIST-800-53-AU-12(a)
  - NIST-800-53-AU-12.1(ii)
  - NIST-800-53-AU-12.1(iv)AU-12(c)
  - NIST-800-53-AU-3
  - NIST-800-53-AU-3.1
  - NIST-800-53-MA-4(1)(a)
  - audit_rules_privileged_commands_kmod
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/kmod
  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/kmod -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/kmod -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/kmod -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/kmod -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030580
  - NIST-800-53-AU-12(a)
  - NIST-800-53-AU-12.1(ii)
  - NIST-800-53-AU-12.1(iv)AU-12(c)
  - NIST-800-53-AU-3
  - NIST-800-53-AU-3.1
  - NIST-800-53-MA-4(1)(a)
  - audit_rules_privileged_commands_kmod
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/kmod -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - mount   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_mount
Identifiers and References

References:  CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, AU-2(d), AU-12(c), AC-6(9), CM-6(a), FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030300, SV-248758r853809_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030300
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_mount
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/mount
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/mount -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/mount -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/mount -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030300
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_mount
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/mount -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - newgrp   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_newgrp
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000169, CCI-000135, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030350, SV-248772r853820_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030350
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_newgrp
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/newgrp
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgrp -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgrp -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgrp -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030350
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/newgrp -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - pam_timestamp_check   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/pam_timestamp_check
-F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/pam_timestamp_check
-F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_pam_timestamp_check
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030340, SV-248771r853819_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030340
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_pam_timestamp_check
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/pam_timestamp_check
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset
        (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/pam_timestamp_check
        -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/pam_timestamp_check
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000 -F auid!=unset
        (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/pam_timestamp_check -F perm=x -F auid>=1000
        -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/pam_timestamp_check
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030340
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/pam_timestamp_check -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - passwd   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_passwd
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030290, SV-248757r853808_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030290
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_passwd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/passwd
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/passwd -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/passwd -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/passwd -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030290
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/passwd -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - postdrop   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_postdrop
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030311, SV-248762r853813_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030311
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_postdrop
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/postdrop
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postdrop -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postdrop -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postdrop -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030311
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/postdrop -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - postqueue   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_postqueue
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030312, SV-248763r853814_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030312
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_postqueue
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/postqueue
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postqueue -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postqueue -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postqueue -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030312
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/postqueue -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Any Attempts to Run ssh-agent   [ref]

At a minimum, the audit system should collect any execution attempt of the ssh-agent 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/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset -k privileged-ssh-agent
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/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset -k privileged-ssh-agent
Rationale:
Without generating audit records that are specific to the security and mission needs of the organization, it would be difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one. Audit records can be generated from various components within the information system (e.g., module or policy filter).
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_ssh_agent
Identifiers and References

References:  CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030280, SV-248756r853807_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030280
  - audit_rules_privileged_commands_ssh_agent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/ssh-agent
  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/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/ssh-agent -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/ssh-agent -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/ssh-agent -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/ssh-agent -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030280
  - audit_rules_privileged_commands_ssh_agent
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/ssh-agent -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - ssh-keysign   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_ssh_keysign
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030320, SV-248769r853817_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030320
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_ssh_keysign
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/libexec/openssh/ssh-keysign
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset
        (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/libexec/openssh/ssh-keysign
        -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/libexec/openssh/ssh-keysign
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset
        (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000
        -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/libexec/openssh/ssh-keysign
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030320
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/libexec/openssh/ssh-keysign -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - su   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_su
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000064-GPOS-0003, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, OL08-00-030190, SV-248747r853803_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030190
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_su
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/su
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/su -F perm=x -F auid>=1000
        -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/su -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset (?:-k
        |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/su -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030190
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/su -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - sudo   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_sudo
Identifiers and References

References:  BP28(R19), 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, OL08-00-030550, SV-248797r853830_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030550
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_sudo
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/sudo
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudo -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudo -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudo -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030550
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/sudo -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - umount   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_umount
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000169, CCI-000135, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030301, SV-248759r853810_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030301
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_umount
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/umount
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/umount -F perm=x -F
        auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/umount -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/umount -F perm=x
        -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030301
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/umount -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - unix_chkpwd   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_unix_chkpwd
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, CIP-007-3 R6.5, AC-2(4), AU-2(d), AU-3, AU-3.1, AU-12(a), AU-12(c), AU-12.1(ii), AU-12.1(iv), AC-6(9), CM-6(a), MA-4(1)(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030317, SV-248768r853816_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030317
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(a)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-12.1(ii)
  - NIST-800-53-AU-12.1(iv)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-AU-3
  - NIST-800-53-AU-3.1
  - NIST-800-53-CM-6(a)
  - NIST-800-53-MA-4(1)(a)
  - audit_rules_privileged_commands_unix_chkpwd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/unix_chkpwd
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/unix_chkpwd -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_chkpwd
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_chkpwd
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030317
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/unix_chkpwd -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - unix_update   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/unix_update -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/unix_update -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_unix_update
Identifiers and References

References:  CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000064-GPOS-00033, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030310, SV-248761r853812_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030310
  - audit_rules_privileged_commands_unix_update
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/unix_update
  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_update -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/unix_update -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_update
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/unix_update -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/unix_update -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_update
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030310
  - audit_rules_privileged_commands_unix_update
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/unix_update -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - userhelper   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_userhelper
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030315, SV-248766r779864_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030315
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - audit_rules_privileged_commands_userhelper
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/userhelper
  block:

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/rules.d/
    find:
      paths: /etc/audit/rules.d
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/userhelper -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/userhelper
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/userhelper
        -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030315
  - NIST-800-171-3.1.7
  - 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

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/userhelper -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on the Use of Privileged Commands - usermod   [ref]

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:
-a always,exit -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_usermod
Identifiers and References

References:  CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, OL08-00-030560, SV-248798r853831_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030560
  - audit_rules_privileged_commands_usermod
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/usermod
  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/usermod -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: '*.rules'
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Reset syscalls found per file
    set_fact:
      syscalls_per_file: {}
      found_paths_dict: {}

  - name: Declare syscalls found per file
    set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
      :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
    loop: '{{ find_command.results | selectattr(''matched'') | list }}'

  - name: Declare files where syscalls were found
    set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
      | map(attribute='path') | list }}"

  - name: Count occurrences of syscalls in paths
    set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
      0) }) }}"
    loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
      | list }}'

  - name: Get path with most syscalls
    set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
      | last).key }}"
    when: found_paths | length >= 1

  - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
    set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
    when: found_paths | length == 0

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
        | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/usermod -F perm=x
        -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/usermod -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0

  - name: Declare list of syscalls
    set_fact:
      syscalls: []
      syscall_grouping: []

  - name: Check existence of  in /etc/audit/audit.rules
    find:
      paths: /etc/audit
      contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
        path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
      patterns: audit.rules
    register: find_command
    loop: '{{ (syscall_grouping + syscalls) | unique }}'

  - name: Set path to /etc/audit/audit.rules
    set_fact: audit_file="/etc/audit/audit.rules"

  - name: Declare found syscalls
    set_fact: syscalls_found="{{ find_command.results | selectattr('matched') | map(attribute='item')
      | list }}"

  - name: Declare missing syscalls
    set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

  - name: Replace the audit rule in {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|") }}))\b)((?:(
        -S |,)\w+)+)( -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=unset
        (?:-k |-F key=)\w+)
      line: \1\2\3{{ missing_syscalls | join("\3") }}\4
      backrefs: true
      state: present
    when: syscalls_found | length > 0 and missing_syscalls | length > 0

  - name: Add the audit rule to {{ audit_file }}
    lineinfile:
      path: '{{ audit_file }}'
      line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/usermod -F
        perm=x -F auid>=1000 -F auid!=unset -F key=privileged
      create: true
      mode: o-rwx
      state: present
    when: syscalls_found | length == 0
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030560
  - audit_rules_privileged_commands_usermod
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/usermod -F perm=x"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Make the auditd Configuration Immutable   [ref]

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d in order to make the auditd configuration immutable:
-e 2
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file in order to make the auditd configuration immutable:
-e 2
With this setting, a reboot will be required to change any audit rules.
Rationale:
Making the audit configuration immutable prevents accidental as well as malicious modification of the audit rules, although it may be problematic if legitimate changes are needed during system operation.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_immutable
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 18, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO01.06, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, DSS06.02, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.3.1, 3.4.3, CCI-000162, CCI-000163, CCI-000164, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.310(a)(2)(iv), 164.312(d), 164.310(d)(2)(iii), 164.312(b), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 6.1, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, ID.SC-4, PR.AC-4, PR.DS-5, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.5.2, 10.3.2, SRG-OS-000057-GPOS-00027, SRG-OS-000058-GPOS-00028, SRG-OS-000059-GPOS-00029, OL08-00-030121, SV-248738r779780_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030121
  - NIST-800-171-3.3.1
  - NIST-800-171-3.4.3
  - NIST-800-53-AC-6(9)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.2
  - PCI-DSSv4-10.3.2
  - audit_rules_immutable
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Collect all files from /etc/audit/rules.d with .rules extension
  find:
    paths: /etc/audit/rules.d/
    patterns: '*.rules'
  register: find_rules_d
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030121
  - NIST-800-171-3.3.1
  - NIST-800-171-3.4.3
  - NIST-800-53-AC-6(9)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.2
  - PCI-DSSv4-10.3.2
  - audit_rules_immutable
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Remove the -e option from all Audit config files
  lineinfile:
    path: '{{ item }}'
    regexp: ^\s*(?:-e)\s+.*$
    state: absent
  loop: '{{ find_rules_d.files | map(attribute=''path'') | list + [''/etc/audit/audit.rules'']
    }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030121
  - NIST-800-171-3.3.1
  - NIST-800-171-3.4.3
  - NIST-800-53-AC-6(9)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.2
  - PCI-DSSv4-10.3.2
  - audit_rules_immutable
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add Audit -e option into /etc/audit/rules.d/immutable.rules and /etc/audit/audit.rules
  lineinfile:
    path: '{{ item }}'
    create: true
    line: -e 2
    mode: o-rwx
  loop:
  - /etc/audit/audit.rules
  - /etc/audit/rules.d/immutable.rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030121
  - NIST-800-171-3.3.1
  - NIST-800-171-3.4.3
  - NIST-800-53-AC-6(9)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5.2
  - PCI-DSSv4-10.3.2
  - audit_rules_immutable
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# Traverse all of:
#
# /etc/audit/audit.rules,			(for auditctl case)
# /etc/audit/rules.d/*.rules			(for augenrules case)
#
# files to check if '-e .*' setting is present in that '*.rules' file already.
# If found, delete such occurrence since auditctl(8) manual page instructs the
# '-e 2' rule should be placed as the last rule in the configuration
find /etc/audit /etc/audit/rules.d -maxdepth 1 -type f -name '*.rules' -exec sed -i '/-e[[:space:]]\+.*/d' {} ';'

# Append '-e 2' requirement at the end of both:
# * /etc/audit/audit.rules file 		(for auditctl case)
# * /etc/audit/rules.d/immutable.rules		(for augenrules case)

for AUDIT_FILE in "/etc/audit/audit.rules" "/etc/audit/rules.d/immutable.rules"
do
	echo '' >> $AUDIT_FILE
	echo '# Set the audit.rules configuration immutable per security requirements' >> $AUDIT_FILE
	echo '# Reboot is required to change audit rules once this setting is applied' >> $AUDIT_FILE
	echo '-e 2' >> $AUDIT_FILE
	chmod o-rwx $AUDIT_FILE
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects Information on Exporting to Media (successful)   [ref]

At a minimum, the audit system should collect media exportation events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S mount -F auid>=1000 -F auid!=unset -F key=export
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S mount -F auid>=1000 -F auid!=unset -F key=export
Rationale:
The unauthorized exportation of data to external media could result in an information leak where classified information, Privacy Act information, and intellectual property could be lost. An audit trail should be created each time a filesystem is mounted to help identify and guard against information loss.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_media_export
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, Req-10.2.7, 10.2.1.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030302, SV-248760r853811_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030302
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.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: b64
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
    == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030302
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_media_export
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for mount for 32bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030302
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_media_export
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Perform remediation of Audit rules for mount for 64bit 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'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - audit_arch == "b64"
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030302
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-6(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
  - PCI-DSSv4-10.2.1.7
  - audit_rules_media_export
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="mount"
	KEY="perm_mod"
	SYSCALL_GROUPING=""

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects System Administrator Actions - /etc/sudoers   [ref]

At a minimum, the audit system should collect administrator actions for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-w /etc/sudoers -p wa -k actions
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
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. Editing the sudoers file may be sign of an attacker trying to establish persistent methods to a system, auditing the editing of the sudoers files mitigates this risk.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_sudoers
Identifiers and References

References:  CCI-000018, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-002130, CCI-002132, CCI-002884, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, OL08-00-030171, SV-248745r853801_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030171
  - audit_rules_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Check if watch rule for /etc/sudoers already exists in /etc/audit/rules.d/
  find:
    paths: /etc/audit/rules.d
    contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+
    patterns: '*.rules'
  register: find_existing_watch_rules_d
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030171
  - audit_rules_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Search /etc/audit/rules.d for other rules with specified key actions
  find:
    paths: /etc/audit/rules.d
    contains: ^.*(?:-F key=|-k\s+)actions$
    patterns: '*.rules'
  register: find_watch_key
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - DISA-STIG-OL08-00-030171
  - audit_rules_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Use /etc/audit/rules.d/actions.rules as the recipient for the rule
  set_fact:
    all_files:
    - /etc/audit/rules.d/actions.rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - DISA-STIG-OL08-00-030171
  - audit_rules_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Use matched file as the recipient for the rule
  set_fact:
    all_files:
    - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - DISA-STIG-OL08-00-030171
  - audit_rules_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Add watch rule for /etc/sudoers in /etc/audit/rules.d/
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/sudoers -p wa -k actions
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - DISA-STIG-OL08-00-030171
  - audit_rules_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Check if watch rule for /etc/sudoers already exists in /etc/audit/audit.rules
  find:
    paths: /etc/audit/
    contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+
    patterns: audit.rules
  register: find_existing_watch_audit_rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030171
  - audit_rules_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Add watch rule for /etc/sudoers in /etc/audit/audit.rules
  lineinfile:
    line: -w /etc/sudoers -p wa -k actions
    state: present
    dest: /etc/audit/audit.rules
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
    == 0
  tags:
  - DISA-STIG-OL08-00-030171
  - audit_rules_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/actions.rules"
    # If the actions.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/   [ref]

At a minimum, the audit system should collect administrator actions for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:
-w /etc/sudoers.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.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. Editing the sudoers file may be sign of an attacker trying to establish persistent methods to a system, auditing the editing of the sudoers files mitigates this risk.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_sudoers_d
Identifiers and References

References:  CCI-000018, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-002130, CCI-002132, CCI-002884, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, OL08-00-030172, SV-248746r853802_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030172
  - audit_rules_sudoers_d
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Check if watch rule for /etc/sudoers.d/ already exists in /etc/audit/rules.d/
  find:
    paths: /etc/audit/rules.d
    contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+
    patterns: '*.rules'
  register: find_existing_watch_rules_d
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030172
  - audit_rules_sudoers_d
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Search /etc/audit/rules.d for other rules with specified key actions
  find:
    paths: /etc/audit/rules.d
    contains: ^.*(?:-F key=|-k\s+)actions$
    patterns: '*.rules'
  register: find_watch_key
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - DISA-STIG-OL08-00-030172
  - audit_rules_sudoers_d
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Use /etc/audit/rules.d/actions.rules as the recipient for the rule
  set_fact:
    all_files:
    - /etc/audit/rules.d/actions.rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - DISA-STIG-OL08-00-030172
  - audit_rules_sudoers_d
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Use matched file as the recipient for the rule
  set_fact:
    all_files:
    - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - DISA-STIG-OL08-00-030172
  - audit_rules_sudoers_d
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Add watch rule for /etc/sudoers.d/ in /etc/audit/rules.d/
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/sudoers.d/ -p wa -k actions
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - DISA-STIG-OL08-00-030172
  - audit_rules_sudoers_d
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Check if watch rule for /etc/sudoers.d/ already exists in /etc/audit/audit.rules
  find:
    paths: /etc/audit/
    contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+
    patterns: audit.rules
  register: find_existing_watch_audit_rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030172
  - audit_rules_sudoers_d
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Add watch rule for /etc/sudoers.d/ in /etc/audit/audit.rules
  lineinfile:
    line: -w /etc/sudoers.d/ -p wa -k actions
    state: present
    dest: /etc/audit/audit.rules
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
    == 0
  tags:
  - DISA-STIG-OL08-00-030172
  - audit_rules_sudoers_d
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers.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

Rule   Record Events When Privileged Executables Are Run   [ref]

Verify the system generates an audit record when privileged functions are executed. If audit is using the "auditctl" tool to load the rules, run the following command:
$ sudo grep execve /etc/audit/audit.rules
If audit is using the "augenrules" tool to load the rules, run the following command:
$ sudo grep -r execve /etc/audit/rules.d
-a always,exit -F arch=b32 -S execve -C uid!=euid -k setuid
-a always,exit -F arch=b64 -S execve -C uid!=euid -k setuid
-a always,exit -F arch=b32 -S execve -C gid!=egid -k setgid
-a always,exit -F arch=b64 -S execve -C gid!=egid -k setgid
If both the "b32" and "b64" audit rules for "SUID" files are not defined, this is a finding. If both the "b32" and "b64" audit rules for "SGID" files are not defined, this is a finding.
Warning:  Note that these rules can be configured in a number of ways while still achieving the desired effect.
Rationale:
Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised information 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 threats and the advanced persistent threat.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_suid_privilege_function
Identifiers and References

References:  CCI-001814, CCI-001882, CCI-001889, CCI-001880, CCI-001881, CCI-001878, CCI-001879, CCI-001875, CCI-001877, CCI-001914, CCI-002233, CCI-002234, CM-5(1), AU-7(a), AU-7(b), AU-8(b), AU-12(3), AC-6(9), SRG-OS-000326-GPOS-00126, SRG-OS-000327-GPOS-00127, OL08-00-030000, SV-248722r853794_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030000
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(3)
  - NIST-800-53-AU-7(a)
  - NIST-800-53-AU-7(b)
  - NIST-800-53-AU-8(b)
  - NIST-800-53-CM-5(1)
  - audit_rules_suid_privilege_function
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Service facts
  ansible.builtin.service_facts: null
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030000
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(3)
  - NIST-800-53-AU-7(a)
  - NIST-800-53-AU-7(b)
  - NIST-800-53-AU-8(b)
  - NIST-800-53-CM-5(1)
  - audit_rules_suid_privilege_function
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Check the rules script being used
  ansible.builtin.command: grep '^ExecStartPost' /usr/lib/systemd/system/auditd.service
  register: check_rules_scripts_result
  changed_when: false
  failed_when: false
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030000
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(3)
  - NIST-800-53-AU-7(a)
  - NIST-800-53-AU-7(b)
  - NIST-800-53-AU-8(b)
  - NIST-800-53-CM-5(1)
  - audit_rules_suid_privilege_function
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set suid_audit_rules fact
  ansible.builtin.set_fact:
    suid_audit_rules:
    - rule: -a always,exit -F arch=b32 -S execve -C gid!=egid -k setgid
      regex: ^[\s]*-a[\s]+always,exit[\s]+-F[\s]+arch=b32[\s]+-S[\s]+execve[\s]+-C[\s]+gid!=egid[\s]+(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$
    - rule: -a always,exit -F arch=b64 -S execve -C gid!=egid -k setgid
      regex: ^[\s]*-a[\s]+always,exit[\s]+-F[\s]+arch=b64[\s]+-S[\s]+execve[\s]+-C[\s]+gid!=egid[\s]+(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$
    - rule: -a always,exit -F arch=b32 -S execve -C uid!=euid -k setuid
      regex: ^[\s]*-a[\s]+always,exit[\s]+-F[\s]+arch=b32[\s]+-S[\s]+execve[\s]+-C[\s]+uid!=euid[\s]+(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$
    - rule: -a always,exit -F arch=b64 -S execve -C uid!=euid -k setuid
      regex: ^[\s]*-a[\s]+always,exit[\s]+-F[\s]+arch=b64[\s]+-S[\s]+execve[\s]+-C[\s]+uid!=euid[\s]+(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030000
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(3)
  - NIST-800-53-AU-7(a)
  - NIST-800-53-AU-7(b)
  - NIST-800-53-AU-8(b)
  - NIST-800-53-CM-5(1)
  - audit_rules_suid_privilege_function
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Update /etc/audit/rules.d/privileged.rules to audit privileged functions
  ansible.builtin.lineinfile:
    path: /etc/audit/rules.d/privileged.rules
    line: '{{  item.rule  }}'
    regexp: '{{ item.regex }}'
    create: true
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"auditd.service" in ansible_facts.services'
  - '"augenrules" in check_rules_scripts_result.stdout'
  register: augenrules_audit_rules_privilege_function_update_result
  with_items: '{{ suid_audit_rules }}'
  tags:
  - DISA-STIG-OL08-00-030000
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(3)
  - NIST-800-53-AU-7(a)
  - NIST-800-53-AU-7(b)
  - NIST-800-53-AU-8(b)
  - NIST-800-53-CM-5(1)
  - audit_rules_suid_privilege_function
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Update Update /etc/audit/audit.rules to audit privileged functions
  ansible.builtin.lineinfile:
    path: /etc/audit/audit.rules
    line: '{{  item.rule  }}'
    regexp: '{{ item.regex }}'
    create: true
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"auditd.service" in ansible_facts.services'
  - '"auditctl" in check_rules_scripts_result.stdout'
  register: auditctl_audit_rules_privilege_function_update_result
  with_items: '{{ suid_audit_rules }}'
  tags:
  - DISA-STIG-OL08-00-030000
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(3)
  - NIST-800-53-AU-7(a)
  - NIST-800-53-AU-7(b)
  - NIST-800-53-AU-8(b)
  - NIST-800-53-CM-5(1)
  - audit_rules_suid_privilege_function
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Restart Auditd
  ansible.builtin.command: /usr/sbin/service auditd restart
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (augenrules_audit_rules_privilege_function_update_result.changed or auditctl_audit_rules_privilege_function_update_result.changed)
  - ansible_facts.services["auditd.service"].state == "running"
  tags:
  - DISA-STIG-OL08-00-030000
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(3)
  - NIST-800-53-AU-7(a)
  - NIST-800-53-AU-7(b)
  - NIST-800-53-AU-8(b)
  - NIST-800-53-CM-5(1)
  - audit_rules_suid_privilege_function
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	
	OTHER_FILTERS="-C uid!=euid"
	
	AUID_FILTERS=""
	SYSCALL="execve"
	KEY="setuid"
	SYSCALL_GROUPING=""
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	
	OTHER_FILTERS="-C gid!=egid"
	
	AUID_FILTERS=""
	SYSCALL="execve"
	KEY="setgid"
	SYSCALL_GROUPING=""
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()



# If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# file to the list of files to be inspected
default_file="/etc/audit/audit.rules"
files_to_inspect+=('/etc/audit/audit.rules' )

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                   # A syscall was not found in the candidate rule
                   all_syscalls_found=1
                   }
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
        echo "$full_rule" >> "$default_file"
        chmod o-rwx ${default_file}
    else
        # Check if the syscalls are declared as a comma separated list or
        # as multiple -S parameters
        if grep -q -- "," <<< "${rule_syscalls_to_edit}"
        then
            delimiter=","
        else
            delimiter=" -S "
        fi
        new_grouped_syscalls="${rule_syscalls_to_edit}"
        for syscall in "${syscall_a[@]}"
        do
            grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
               # A syscall was not found in the candidate rule
               new_grouped_syscalls+="${delimiter}${syscall}"
               }
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify User/Group Information - /etc/group   [ref]

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/group -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/group -p wa -k audit_rules_usergroup_modification
Rationale:
In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_group
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000018, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.5, 10.2.1.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, OL08-00-030170, SV-248744r853800_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030170
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_group
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/group already exists in /etc/audit/rules.d/
  find:
    paths: /etc/audit/rules.d
    contains: ^\s*-w\s+/etc/group\s+-p\s+wa(\s|$)+
    patterns: '*.rules'
  register: find_existing_watch_rules_d
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030170
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_group
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification
  find:
    paths: /etc/audit/rules.d
    contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_watch_key
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030170
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_group
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient
    for the rule
  set_fact:
    all_files:
    - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030170
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.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_watch_key.files | map(attribute=''path'') | list | first }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030170
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_group
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/group in /etc/audit/rules.d/
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/group -p wa -k audit_rules_usergroup_modification
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030170
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_group
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/group already exists in /etc/audit/audit.rules
  find:
    paths: /etc/audit/
    contains: ^\s*-w\s+/etc/group\s+-p\s+wa(\s|$)+
    patterns: audit.rules
  register: find_existing_watch_audit_rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030170
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_group
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/group 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
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030170
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_group
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/group" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/group $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/group$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/group -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/group" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
    # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/group" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/group $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/group$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/group -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Record Events that Modify User/Group Information - /etc/gshadow   [ref]

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/gshadow -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/gshadow -p wa -k audit_rules_usergroup_modification
Rationale:
In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_gshadow
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000018, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.5, 10.2.1.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, OL08-00-030160, SV-248743r853799_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030160
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_gshadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/gshadow already exists in /etc/audit/rules.d/
  find:
    paths: /etc/audit/rules.d
    contains: ^\s*-w\s+/etc/gshadow\s+-p\s+wa(\s|$)+
    patterns: '*.rules'
  register: find_existing_watch_rules_d
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030160
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_gshadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification
  find:
    paths: /etc/audit/rules.d
    contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_watch_key
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030160
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_gshadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient
    for the rule
  set_fact:
    all_files:
    - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030160
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.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_watch_key.files | map(attribute=''path'') | list | first }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030160
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_gshadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/gshadow in /etc/audit/rules.d/
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030160
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_gshadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/gshadow already exists in /etc/audit/audit.rules
  find:
    paths: /etc/audit/
    contains: ^\s*-w\s+/etc/gshadow\s+-p\s+wa(\s|$)+
    patterns: audit.rules
  register: find_existing_watch_audit_rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030160
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_gshadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/gshadow 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
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030160
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_gshadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/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

Rule   Record Events that Modify User/Group Information - /etc/security/opasswd   [ref]

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
Rationale:
In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_opasswd
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000018, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.5, 10.2.1.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, OL08-00-030140, SV-248741r853797_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030140
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_opasswd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/security/opasswd already exists in /etc/audit/rules.d/
  find:
    paths: /etc/audit/rules.d
    contains: ^\s*-w\s+/etc/security/opasswd\s+-p\s+wa(\s|$)+
    patterns: '*.rules'
  register: find_existing_watch_rules_d
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030140
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_opasswd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification
  find:
    paths: /etc/audit/rules.d
    contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_watch_key
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030140
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_opasswd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient
    for the rule
  set_fact:
    all_files:
    - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030140
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.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_watch_key.files | map(attribute=''path'') | list | first }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030140
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_opasswd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/security/opasswd in /etc/audit/rules.d/
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030140
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_opasswd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/security/opasswd already exists in /etc/audit/audit.rules
  find:
    paths: /etc/audit/
    contains: ^\s*-w\s+/etc/security/opasswd\s+-p\s+wa(\s|$)+
    patterns: audit.rules
  register: find_existing_watch_audit_rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030140
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_opasswd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/security/opasswd 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
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030140
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_opasswd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/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

Rule   Record Events that Modify User/Group Information - /etc/passwd   [ref]

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/passwd -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/passwd -p wa -k audit_rules_usergroup_modification
Rationale:
In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_passwd
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000018, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.5, 10.2.1.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, SRG-OS-000274-GPOS-00104, SRG-OS-000275-GPOS-00105, SRG-OS-000276-GPOS-00106, SRG-OS-000277-GPOS-00107, OL08-00-030150, SV-248742r853798_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030150
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_passwd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/passwd already exists in /etc/audit/rules.d/
  find:
    paths: /etc/audit/rules.d
    contains: ^\s*-w\s+/etc/passwd\s+-p\s+wa(\s|$)+
    patterns: '*.rules'
  register: find_existing_watch_rules_d
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030150
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_passwd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification
  find:
    paths: /etc/audit/rules.d
    contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_watch_key
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030150
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_passwd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient
    for the rule
  set_fact:
    all_files:
    - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030150
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.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_watch_key.files | map(attribute=''path'') | list | first }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030150
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_passwd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/passwd in /etc/audit/rules.d/
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030150
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_passwd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/passwd already exists in /etc/audit/audit.rules
  find:
    paths: /etc/audit/
    contains: ^\s*-w\s+/etc/passwd\s+-p\s+wa(\s|$)+
    patterns: audit.rules
  register: find_existing_watch_audit_rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030150
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_passwd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/passwd 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
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030150
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_passwd
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/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

Rule   Record Events that Modify User/Group Information - /etc/shadow   [ref]

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/shadow -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/shadow -p wa -k audit_rules_usergroup_modification
Rationale:
In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_shadow
Identifiers and References

References:  BP28(R73), 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000018, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.5, 10.2.1.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, OL08-00-030130, SV-248740r853796_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030130
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_shadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/shadow already exists in /etc/audit/rules.d/
  find:
    paths: /etc/audit/rules.d
    contains: ^\s*-w\s+/etc/shadow\s+-p\s+wa(\s|$)+
    patterns: '*.rules'
  register: find_existing_watch_rules_d
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030130
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_shadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification
  find:
    paths: /etc/audit/rules.d
    contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_watch_key
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030130
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_shadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient
    for the rule
  set_fact:
    all_files:
    - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030130
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.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_watch_key.files | map(attribute=''path'') | list | first }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
    is defined and find_existing_watch_rules_d.matched == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030130
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_shadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/shadow in /etc/audit/rules.d/
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification
    create: true
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030130
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_shadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Check if watch rule for /etc/shadow already exists in /etc/audit/audit.rules
  find:
    paths: /etc/audit/
    contains: ^\s*-w\s+/etc/shadow\s+-p\s+wa(\s|$)+
    patterns: audit.rules
  register: find_existing_watch_audit_rules
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030130
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_shadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Add watch rule for /etc/shadow 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
    mode: '0640'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
    == 0
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030130
  - NIST-800-171-3.1.7
  - NIST-800-53-AC-2(4)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-10.2.1.5
  - audit_rules_usergroup_modification_shadow
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "^[\s]*-w[\s]+/etc/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

Rule   System Audit Directories Must Be Group Owned By Root   [ref]

All audit directories must be group owned by root user. By default, the path for audit log is
/var/log/audit/
. To properly set the group owner of /var/log/audit, run the command:
$ sudo chgrp root /var/log/audit
Rationale:
Unauthorized disclosure of audit records can reveal system and configuration data to attackers, thus compromising its confidentiality.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_directory_group_ownership_var_log_audit
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 18, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO01.06, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, DSS06.02, MEA02.01, 3.3.1, CCI-000162, CCI-000163, CCI-000164, CCI-001314, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 6.1, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), AU-9(4), DE.AE-3, DE.AE-5, PR.AC-4, PR.DS-5, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.5.1, SRG-OS-000057-GPOS-00027, SRG-OS-000058-GPOS-00028, SRG-OS-000059-GPOS-00029, SRG-OS-000206-GPOS-00084, OL08-00-030110, SV-248736r779774_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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 ' ')
else
  GROUP=root
fi
if LC_ALL=C grep -iw ^log_file /etc/audit/auditd.conf; then
  DIR=$(awk -F "=" '/^log_file/ {print $2}' /etc/audit/auditd.conf | tr -d ' ' | rev | cut -d"/" -f2- | rev)
else
  DIR="/var/log/audit"
fi


GROUP="root"

find ${DIR} -type d -exec chgrp ${GROUP} {} \;

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   System Audit Directories Must Be Owned By Root   [ref]

All audit directories must be owned by root user. 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 
Rationale:
Unauthorized disclosure of audit records can reveal system and configuration data to attackers, thus compromising its confidentiality.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_directory_ownership_var_log_audit
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 18, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO01.06, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, DSS06.02, MEA02.01, 3.3.1, CCI-000162, CCI-000163, CCI-000164, CCI-001314, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 6.1, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), AU-9(4), DE.AE-3, DE.AE-5, PR.AC-4, PR.DS-5, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.5.1, SRG-OS-000057-GPOS-00027, SRG-OS-000058-GPOS-00028, SRG-OS-000059-GPOS-00029, SRG-OS-000206-GPOS-00084, OL08-00-030100, SV-248735r779771_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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 ' ')
    LOGPATH="$(dirname "$FILE")"
    chown root $LOGPATH
else
    chown root /var/log/audit
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   System Audit Logs Must Have Mode 0750 or Less Permissive   [ref]

Verify the audit log directories have a mode of "0700" or less permissive by first determining where the audit logs are stored with the following command:
$ sudo grep -iw log_file /etc/audit/auditd.conf

log_file = /var/log/audit/audit.log
Configure the audit log directory to be protected from unauthorized read access by setting the correct permissive mode with the following command:
$ sudo chmod 0700 audit_log_directory
By default, audit_log_directory is "/var/log/audit".
Rationale:
If users can write to audit logs, audit trails can be modified or destroyed.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_directory_permissions_var_log_audit
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 18, 19, 3, 4, 5, 6, 7, 8, APO01.06, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, DSS06.02, MEA02.01, CCI-000162, CCI-000163, CCI-000164, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 6.1, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.2, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-004-6 R3.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CIP-007-3 R6.5, CM-6(a), AC-6(1), AU-9, DE.AE-3, DE.AE-5, PR.AC-4, PR.DS-5, PR.PT-1, RS.AN-1, RS.AN-4, SRG-OS-000057-GPOS-00027, SRG-OS-000058-GPOS-00028, SRG-OS-000059-GPOS-00029, OL08-00-030120, SV-248737r779777_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

if LC_ALL=C grep -iw ^log_file /etc/audit/auditd.conf; then
  DIR=$(awk -F "=" '/^log_file/ {print $2}' /etc/audit/auditd.conf | tr -d ' ' | rev | cut -d"/" -f2- | rev)
else
  DIR="/var/log/audit"
fi


chmod 0700 $DIR

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   System Audit Logs Must Be Group Owned By Root   [ref]

All audit logs must be group owned by root user. The path for audit log can be configured via log_file parameter in
/etc/audit/auditd.conf
or, by default, the path for audit log is
/var/log/audit/
. To properly set the group owner of /var/log/audit/*, run the command:
$ sudo chgrp root /var/log/audit/*
Rationale:
Unauthorized disclosure of audit records can reveal system and configuration data to attackers, thus compromising its confidentiality.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_group_ownership_var_log_audit
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 18, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO01.06, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, DSS06.02, MEA02.01, 3.3.1, CCI-000162, CCI-000163, CCI-000164, CCI-001314, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 6.1, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), AU-9(4), DE.AE-3, DE.AE-5, PR.AC-4, PR.DS-5, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.5.1, SRG-OS-000057-GPOS-00027, SRG-OS-000058-GPOS-00028, SRG-OS-000059-GPOS-00029, SRG-OS-000206-GPOS-00084, OL08-00-030090, SV-248734r779768_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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


chgrp root $FILE

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   System Audit Logs Must Be Owned By Root   [ref]

All audit logs must be owned by root user. The path for audit log can be configured via log_file parameter in
/etc/audit/auditd.conf
or 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/* 
Rationale:
Unauthorized disclosure of audit records can reveal system and configuration data to attackers, thus compromising its confidentiality.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_ownership_var_log_audit_stig
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 18, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO01.06, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, DSS06.02, MEA02.01, 3.3.1, CCI-000162, CCI-000163, CCI-000164, CCI-001314, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 6.1, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), AU-9(4), DE.AE-3, DE.AE-5, PR.AC-4, PR.DS-5, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.5.1, SRG-OS-000057-GPOS-00027, SRG-OS-000058-GPOS-00028, SRG-OS-000059-GPOS-00029, SRG-OS-000206-GPOS-00084, OL08-00-030080, SV-248733r779765_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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 ' ')
    chown root $FILE*
else
    chown root /var/log/audit/audit.log*
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   System Audit Logs Must Have Mode 0640 or Less Permissive   [ref]

Determine where the audit logs are stored with the following command:
$ sudo grep -iw log_file /etc/audit/auditd.conf
log_file = /var/log/audit/audit.log
Configure the audit log to be protected from unauthorized read access by setting the correct permissive mode with the following command:
$ sudo chmod 0600 audit_log_file
By default, audit_log_file is "/var/log/audit/audit.log".
Rationale:
If users can write to audit logs, audit trails can be modified or destroyed.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_var_log_audit
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 18, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO01.06, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, DSS06.02, MEA02.01, 3.3.1, CCI-000162, CCI-000163, CCI-000164, CCI-001314, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 6.1, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-6(a), AC-6(1), AU-9(4), DE.AE-3, DE.AE-5, PR.AC-4, PR.DS-5, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.5, 10.3.1, SRG-OS-000057-GPOS-00027, SRG-OS-000058-GPOS-00028, SRG-OS-000059-GPOS-00029, SRG-OS-000206-GPOS-00084, OL08-00-030070, SV-248732r779762_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030070
  - NIST-800-171-3.3.1
  - NIST-800-53-AC-6(1)
  - NIST-800-53-AU-9(4)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5
  - PCI-DSSv4-10.3.1
  - file_permissions_var_log_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Get audit log files
  command: grep -iw ^log_file /etc/audit/auditd.conf
  failed_when: false
  register: log_file_exists
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030070
  - NIST-800-171-3.3.1
  - NIST-800-53-AC-6(1)
  - NIST-800-53-AU-9(4)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5
  - PCI-DSSv4-10.3.1
  - file_permissions_var_log_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Parse log file line
  command: awk -F '=' '/^log_file/ {print $2}' /etc/audit/auditd.conf
  register: log_file_line
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (log_file_exists.stdout | length > 0)
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030070
  - NIST-800-171-3.3.1
  - NIST-800-53-AC-6(1)
  - NIST-800-53-AU-9(4)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5
  - PCI-DSSv4-10.3.1
  - file_permissions_var_log_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set default log_file if not set
  set_fact:
    log_file: /var/log/audit/audit.log
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (log_file_exists is undefined) or (log_file_exists.stdout | length == 0)
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030070
  - NIST-800-171-3.3.1
  - NIST-800-53-AC-6(1)
  - NIST-800-53-AU-9(4)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5
  - PCI-DSSv4-10.3.1
  - file_permissions_var_log_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set log_file from log_file_line if not set already
  set_fact:
    log_file: '{{ log_file_line.stdout | trim }}'
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (log_file_line.stdout is defined) and (log_file_line.stdout | length > 0)
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030070
  - NIST-800-171-3.3.1
  - NIST-800-53-AC-6(1)
  - NIST-800-53-AU-9(4)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5
  - PCI-DSSv4-10.3.1
  - file_permissions_var_log_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Apply mode to log file
  file:
    path: '{{ log_file }}'
    mode: 384
  failed_when: false
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030070
  - NIST-800-171-3.3.1
  - NIST-800-53-AC-6(1)
  - NIST-800-53-AU-9(4)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.5
  - PCI-DSSv4-10.3.1
  - file_permissions_var_log_audit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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


chmod 0600 $FILE

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Configure auditd Data Retention   Group contains 10 rules
[ref]   The audit system writes data to /var/log/audit/audit.log. By default, auditd rotates 5 logs by size (6MB), retaining a maximum of 30MB of data in total, and refuses to write entries when the disk is too full. This minimizes the risk of audit data filling its partition and impacting other services. This also minimizes the risk of the audit daemon temporarily disabling the system if it cannot write audit log (which it can be configured to do). For a busy system or a system which is thoroughly auditing system activity, the default settings for data retention may be insufficient. The log file size needed will depend heavily on what types of events are being audited. First configure auditing to log all the events of interest. Then monitor the log size manually for awhile to determine what file size will allow you to keep the required data for the correct time period.

Using a dedicated partition for /var/log/audit prevents the auditd logs from disrupting system functionality if they fill, and, more importantly, prevents other activity in /var from filling the partition and stopping the audit trail. (The audit logs are size-limited and therefore unlikely to grow without bound unless configured to do so.) Some machines may have requirements that no actions occur which cannot be audited. If this is the case, then auditd can be configured to halt the machine if it runs out of space. Note: Since older logs are rotated, configuring auditd this way does not prevent older logs from being rotated away before they can be viewed. If your system is configured to halt when logging cannot be performed, make sure this can never happen under normal circumstances! Ensure that /var/log/audit is on its own partition, and that this partition is larger than the maximum amount of data auditd will retain normally.

Rule   Configure a Sufficiently Large Partition for Audit Logs   [ref]

The Oracle Linux 8 operating system must allocate audit record storage capacity to store at least one weeks worth of audit records when audit records are not immediately sent to a central audit record storage facility. The partition size needed to capture a week's worth of audit records is based on the activity level of the system and the total storage capacity available. In normal circumstances, 10.0 GB of storage space for audit records will be sufficient. Determine which partition the audit records are being written to with the following command:
$ sudo grep log_file /etc/audit/auditd.conf
log_file = /var/log/audit/audit.log
Check the size of the partition that audit records are written to with the following command:
$ sudo df -h /var/log/audit/
/dev/sda2 24G 10.4G 13.6G 43% /var/log/audit
Rationale:
Information stored in one location is vulnerable to accidental or incidental deletion or alteration. Off-loading is a common process in information systems with limited audit storage capacity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_audispd_configure_sufficiently_large_partition
Identifiers and References

References:  CCI-001849, SRG-OS-000341-GPOS-00132, SRG-OS-000342-GPOS-00133, OL08-00-030660, SV-248811r877391_rule

Rule   Configure auditd Disk Error Action on Disk Error   [ref]

The auditd service can be configured to take an action when there is a disk error. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting ACTION appropriately:
disk_error_action = ACTION
Set this value to single to cause the system to switch to single-user mode for corrective action. Acceptable values also include syslog, exec, single, and halt. For certain systems, the need for availability outweighs the need to log all actions, and a different setting should be determined. Details regarding all possible values for ACTION are described in the auditd.conf man page.
Rationale:
Taking appropriate action in case of disk errors will minimize the possibility of losing audit records.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_data_disk_error_action
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, APO11.04, APO12.06, APO13.01, BAI03.05, BAI04.04, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, MEA02.01, CCI-000140, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 7.1, SR 7.2, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.16.1.4, A.16.1.5, A.16.1.7, A.17.2.1, AU-5(b), AU-5(2), AU-5(1), AU-5(4), CM-6(a), DE.AE-3, DE.AE-5, PR.DS-4, PR.PT-1, RS.AN-1, RS.AN-4, SRG-OS-000047-GPOS-00023, OL08-00-030040, SV-248726r779744_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030040
  - NIST-800-53-AU-5(1)
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(4)
  - NIST-800-53-AU-5(b)
  - NIST-800-53-CM-6(a)
  - auditd_data_disk_error_action
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_auditd_disk_error_action # promote to variable
  set_fact:
    var_auditd_disk_error_action: !!str syslog|single|halt
  tags:
    - always

- name: Configure auditd Disk Error Action on Disk Error
  lineinfile:
    dest: /etc/audit/auditd.conf
    line: disk_error_action = {{ var_auditd_disk_error_action.split('|')[0] }}
    regexp: ^\s*disk_error_action\s*=\s*.*$
    state: present
    create: true
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030040
  - NIST-800-53-AU-5(1)
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(4)
  - NIST-800-53-AU-5(b)
  - NIST-800-53-CM-6(a)
  - auditd_data_disk_error_action
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

var_auditd_disk_error_action='syslog|single|halt'


#
# If disk_error_action present in /etc/audit/auditd.conf, change value
# to var_auditd_disk_error_action, else
# add "disk_error_action = $var_auditd_disk_error_action" to /etc/audit/auditd.conf
#
var_auditd_disk_error_action="$(echo $var_auditd_disk_error_action | cut -d \| -f 1)"

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^disk_error_action")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_disk_error_action"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^disk_error_action\\>" "/etc/audit/auditd.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^disk_error_action\\>.*/$escaped_formatted_output/gi" "/etc/audit/auditd.conf"
else
    if [[ -s "/etc/audit/auditd.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/audit/auditd.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/audit/auditd.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/audit/auditd.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure auditd Disk Full Action when Disk Space Is Full   [ref]

The auditd service can be configured to take an action when disk space is running low but prior to running out of space completely. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting ACTION appropriately:
disk_full_action = ACTION
Set this value to single to cause the system to switch to single-user mode for corrective action. Acceptable values also include syslog, exec, single, and halt. For certain systems, the need for availability outweighs the need to log all actions, and a different setting should be determined. Details regarding all possible values for ACTION are described in the auditd.conf man page.
Rationale:
Taking appropriate action in case of a filled audit storage volume will minimize the possibility of losing audit records.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_data_disk_full_action
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, APO11.04, APO12.06, APO13.01, BAI03.05, BAI04.04, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, MEA02.01, CCI-000140, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 7.1, SR 7.2, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.16.1.4, A.16.1.5, A.16.1.7, A.17.2.1, AU-5(b), AU-5(2), AU-5(1), AU-5(4), CM-6(a), DE.AE-3, DE.AE-5, PR.DS-4, PR.PT-1, RS.AN-1, RS.AN-4, SRG-OS-000047-GPOS-00023, OL08-00-030060, SV-248728r779750_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030060
  - NIST-800-53-AU-5(1)
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(4)
  - NIST-800-53-AU-5(b)
  - NIST-800-53-CM-6(a)
  - auditd_data_disk_full_action
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_auditd_disk_full_action # promote to variable
  set_fact:
    var_auditd_disk_full_action: !!str syslog|single|halt
  tags:
    - always

- name: Configure auditd Disk Full Action when Disk Space Is Full
  lineinfile:
    dest: /etc/audit/auditd.conf
    line: disk_full_action = {{ var_auditd_disk_full_action.split('|')[0] }}
    regexp: ^\s*disk_full_action\s*=\s*.*$
    state: present
    create: true
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030060
  - NIST-800-53-AU-5(1)
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(4)
  - NIST-800-53-AU-5(b)
  - NIST-800-53-CM-6(a)
  - auditd_data_disk_full_action
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

var_auditd_disk_full_action='syslog|single|halt'


var_auditd_disk_full_action="$(echo $var_auditd_disk_full_action | cut -d \| -f 1)"

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^disk_full_action")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_disk_full_action"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^disk_full_action\\>" "/etc/audit/auditd.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^disk_full_action\\>.*/$escaped_formatted_output/gi" "/etc/audit/auditd.conf"
else
    if [[ -s "/etc/audit/auditd.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/audit/auditd.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/audit/auditd.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/audit/auditd.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure auditd mail_acct Action on Low Disk Space   [ref]

The auditd service can be configured to send email to a designated account in certain situations. Add or correct the following line in /etc/audit/auditd.conf to ensure that administrators are notified via email for those situations:
action_mail_acct = root
Rationale:
Email sent to the root account is typically aliased to the administrators of the system, who can take appropriate action.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO11.04, APO12.06, APO13.01, BAI03.05, BAI04.04, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, MEA02.01, 3.3.1, CCI-000139, CCI-001855, 164.312(a)(2)(ii), 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 7.1, SR 7.2, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.16.1.4, A.16.1.5, A.16.1.7, A.17.2.1, CIP-003-8 R1.3, CIP-003-8 R3, CIP-003-8 R3.1, CIP-003-8 R3.2, CIP-003-8 R3.3, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, IA-5(1), AU-5(a), AU-5(2), CM-6(a), DE.AE-3, DE.AE-5, PR.DS-4, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.7.a, 10.5.1, SRG-OS-000046-GPOS-00022, SRG-OS-000343-GPOS-00134, OL08-00-030020, SV-248724r779738_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030020
  - NIST-800-171-3.3.1
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)
  - PCI-DSS-Req-10.7.a
  - PCI-DSSv4-10.5.1
  - auditd_data_retention_action_mail_acct
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_auditd_action_mail_acct # promote to variable
  set_fact:
    var_auditd_action_mail_acct: !!str root
  tags:
    - always

- name: Configure auditd mail_acct Action on Low Disk Space
  lineinfile:
    dest: /etc/audit/auditd.conf
    line: action_mail_acct = {{ var_auditd_action_mail_acct }}
    state: present
    create: true
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030020
  - NIST-800-171-3.3.1
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)
  - PCI-DSS-Req-10.7.a
  - PCI-DSSv4-10.5.1
  - auditd_data_retention_action_mail_acct
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

var_auditd_action_mail_acct='root'


AUDITCONFIG=/etc/audit/auditd.conf

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^action_mail_acct")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_action_mail_acct"

# 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 "^action_mail_acct\\>" "$AUDITCONFIG"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^action_mail_acct\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG"
else
    if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG"
    fi
    printf '%s\n' "$formatted_output" >> "$AUDITCONFIG"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure auditd space_left Action on Low Disk Space   [ref]

The auditd service can be configured to take an action when disk space starts to run low. Edit the file /etc/audit/auditd.conf. Modify the following line, substituting ACTION appropriately:
space_left_action = ACTION
Possible values for ACTION are described in the auditd.conf man page. These include:
  • syslog
  • email
  • exec
  • suspend
  • single
  • halt
Set this to email (instead of the default, which is suspend) as it is more likely to get prompt attention. Acceptable values also include suspend, single, and halt.
Rationale:
Notifying administrators of an impending disk space problem may allow them to take corrective action prior to any disruption.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_data_retention_space_left_action
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO11.04, APO12.06, APO13.01, BAI03.05, BAI04.04, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, MEA02.01, 3.3.1, CCI-001855, 164.312(a)(2)(ii), 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 7.1, SR 7.2, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.16.1.4, A.16.1.5, A.16.1.7, A.17.2.1, AU-5(b), AU-5(2), AU-5(1), AU-5(4), CM-6(a), DE.AE-3, DE.AE-5, PR.DS-4, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.7, 10.5.1, SRG-OS-000343-GPOS-00134, OL08-00-030731, SV-248819r877389_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030731
  - NIST-800-171-3.3.1
  - NIST-800-53-AU-5(1)
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(4)
  - NIST-800-53-AU-5(b)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.7
  - PCI-DSSv4-10.5.1
  - auditd_data_retention_space_left_action
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_auditd_space_left_action # promote to variable
  set_fact:
    var_auditd_space_left_action: !!str email
  tags:
    - always

- name: Configure auditd space_left Action on Low Disk Space
  lineinfile:
    dest: /etc/audit/auditd.conf
    line: space_left_action = {{ var_auditd_space_left_action }}
    regexp: ^\s*space_left_action\s*=\s*.*$
    state: present
    create: true
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030731
  - NIST-800-171-3.3.1
  - NIST-800-53-AU-5(1)
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(4)
  - NIST-800-53-AU-5(b)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.7
  - PCI-DSSv4-10.5.1
  - auditd_data_retention_space_left_action
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

var_auditd_space_left_action='email'


#
# If space_left_action present in /etc/audit/auditd.conf, change value
# to var_auditd_space_left_action, else
# add "space_left_action = $var_auditd_space_left_action" to /etc/audit/auditd.conf
#

AUDITCONFIG=/etc/audit/auditd.conf

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^space_left_action")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_space_left_action"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^space_left_action\\>" "$AUDITCONFIG"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^space_left_action\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG"
else
    if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG"
    fi
    printf '%s\n' "$formatted_output" >> "$AUDITCONFIG"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure auditd space_left on Low Disk Space   [ref]

The auditd service can be configured to take an action when disk space is running low but prior to running out of space completely. Edit the file /etc/audit/auditd.conf. Add or modify the following line, substituting PERCENTAGE appropriately:
space_left = PERCENTAGE%
Set this value to at least 25 to cause the system to notify the user of an issue.
Rationale:
Notifying administrators of an impending disk space problem may allow them to take corrective action prior to any disruption.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_data_retention_space_left_percentage
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, APO11.04, APO12.06, APO13.01, BAI03.05, BAI04.04, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, MEA02.01, CCI-001855, 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 7.1, SR 7.2, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.16.1.4, A.16.1.5, A.16.1.7, A.17.2.1, AU-5(b), AU-5(2), AU-5(1), AU-5(4), CM-6(a), DE.AE-3, DE.AE-5, PR.DS-4, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.7, SRG-OS-000343-GPOS-00134, OL08-00-030730, SV-248818r877389_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030730
  - NIST-800-53-AU-5(1)
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(4)
  - NIST-800-53-AU-5(b)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.7
  - auditd_data_retention_space_left_percentage
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_auditd_space_left_percentage # promote to variable
  set_fact:
    var_auditd_space_left_percentage: !!str 25
  tags:
    - always

- name: Configure auditd space_left on Low Disk Space
  lineinfile:
    dest: /etc/audit/auditd.conf
    line: space_left = {{ var_auditd_space_left_percentage }}%
    regexp: ^\s*space_left\s*=\s*.*$
    state: present
    create: true
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030730
  - NIST-800-53-AU-5(1)
  - NIST-800-53-AU-5(2)
  - NIST-800-53-AU-5(4)
  - NIST-800-53-AU-5(b)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.7
  - auditd_data_retention_space_left_percentage
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

var_auditd_space_left_percentage='25'


grep -q "^space_left[[:space:]]*=.*$" /etc/audit/auditd.conf && \
  sed -i "s/^space_left[[:space:]]*=.*$/space_left = $var_auditd_space_left_percentage%/g" /etc/audit/auditd.conf || \
  echo "space_left = $var_auditd_space_left_percentage%" >> /etc/audit/auditd.conf

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Include Local Events in Audit Logs   [ref]

To configure Audit daemon to include local events in Audit logs, set local_events to yes in /etc/audit/auditd.conf. This is the default setting.
Rationale:
If option local_events isn't set to yes only events from network will be aggregated.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_local_events
Identifiers and References

References:  CCI-000366, CM-6, FAU_GEN.1, SRG-OS-000062-GPOS-00031, SRG-OS-000480-GPOS-00227, OL08-00-030061, SV-248729r779753_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030061
  - NIST-800-53-CM-6
  - auditd_local_events
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Include Local Events in Audit Logs
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/audit/auditd.conf
      create: false
      regexp: (?i)^\s*local_events\s*=\s*
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/audit/auditd.conf
    lineinfile:
      path: /etc/audit/auditd.conf
      create: false
      regexp: (?i)^\s*local_events\s*=\s*
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/audit/auditd.conf
    lineinfile:
      path: /etc/audit/auditd.conf
      create: true
      regexp: (?i)^\s*local_events\s*=\s*
      line: local_events = yes
      state: present
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030061
  - NIST-800-53-CM-6
  - auditd_local_events
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

if [ -e "/etc/audit/auditd.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*local_events\s*=\s*/Id" "/etc/audit/auditd.conf"
else
    touch "/etc/audit/auditd.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/audit/auditd.conf"

cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
# Insert at the end of the file
printf '%s\n' "local_events = yes" >> "/etc/audit/auditd.conf"
# Clean up after ourselves.
rm "/etc/audit/auditd.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Resolve information before writing to audit logs   [ref]

To configure Audit daemon to resolve all uid, gid, syscall, architecture, and socket address information before writing the events to disk, set log_format to ENRICHED in /etc/audit/auditd.conf.
Rationale:
If option log_format isn't set to ENRICHED, the audit records will be stored in a format exactly as the kernel sends them.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_auditd_log_format
Identifiers and References

References:  CCI-000366, CM-6, AU-3, FAU_GEN.1.2, SRG-OS-000255-GPOS-00096, SRG-OS-000480-GPOS-00227, OL08-00-030063, SV-248731r779759_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030063
  - NIST-800-53-AU-3
  - NIST-800-53-CM-6
  - auditd_log_format
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

- name: Resolve information before writing to audit logs
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/audit/auditd.conf
      create: false
      regexp: (?i)^\s*log_format\s*=\s*
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/audit/auditd.conf
    lineinfile:
      path: /etc/audit/auditd.conf
      create: false
      regexp: (?i)^\s*log_format\s*=\s*
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/audit/auditd.conf
    lineinfile:
      path: /etc/audit/auditd.conf
      create: true
      regexp: (?i)^\s*log_format\s*=\s*
      line: log_format = ENRICHED
      state: present
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030063
  - NIST-800-53-AU-3
  - NIST-800-53-CM-6
  - auditd_log_format
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

if [ -e "/etc/audit/auditd.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*log_format\s*=\s*/Id" "/etc/audit/auditd.conf"
else
    touch "/etc/audit/auditd.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/audit/auditd.conf"

cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
# Insert at the end of the file
printf '%s\n' "log_format = ENRICHED" >> "/etc/audit/auditd.conf"
# Clean up after ourselves.
rm "/etc/audit/auditd.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Set hostname as computer node name in audit logs   [ref]

To configure Audit daemon to use value returned by gethostname syscall as computer node name in the audit events, set name_format to hostname in /etc/audit/auditd.conf.
Rationale:
If option name_format is left at its default value of none, audit events from different computers may be hard to distinguish.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_name_format
Identifiers and References

References:  CCI-001851, CM-6, AU-3, FAU_GEN.1.2, SRG-OS-000039-GPOS-00017, SRG-OS-000342-GPOS-00133, SRG-OS-000479-GPOS-00224, OL08-00-030062, SV-248730r877390_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030062
  - NIST-800-53-AU-3
  - NIST-800-53-CM-6
  - auditd_name_format
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set hostname as computer node name in audit logs
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/audit/auditd.conf
      create: false
      regexp: (?i)^\s*name_format\s*=\s*
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/audit/auditd.conf
    lineinfile:
      path: /etc/audit/auditd.conf
      create: false
      regexp: (?i)^\s*name_format\s*=\s*
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/audit/auditd.conf
    lineinfile:
      path: /etc/audit/auditd.conf
      create: true
      regexp: (?i)^\s*name_format\s*=\s*
      line: name_format = hostname
      state: present
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030062
  - NIST-800-53-AU-3
  - NIST-800-53-CM-6
  - auditd_name_format
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

if [ -e "/etc/audit/auditd.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*name_format\s*=\s*/Id" "/etc/audit/auditd.conf"
else
    touch "/etc/audit/auditd.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/audit/auditd.conf"

cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
# Insert at the end of the file
printf '%s\n' "name_format = hostname" >> "/etc/audit/auditd.conf"
# Clean up after ourselves.
rm "/etc/audit/auditd.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Appropriate Action Must be Setup When the Internal Audit Event Queue is Full   [ref]

The audit system should have an action setup in the event the internal event queue becomes full. To setup an overflow action edit /etc/audit/auditd.conf. Set overflow_action to one of the following values: syslog, single, halt.
Rationale:
The audit system should have an action setup in the event the internal event queue becomes full so that no data is lost.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_auditd_overflow_action
Identifiers and References

References:  CCI-001851, AU-4(1), SRG-OS-000342-GPOS-00133, SRG-OS-000479-GPOS-00224, OL08-00-030700, SV-248815r877390_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030700
  - NIST-800-53-AU-4(1)
  - auditd_overflow_action
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Appropriate Action Must be Setup When the Internal Audit Event Queue is Full
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/audit/auditd.conf
      create: false
      regexp: (?i)^\s*overflow_action\s*=\s*
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/audit/auditd.conf
    lineinfile:
      path: /etc/audit/auditd.conf
      create: false
      regexp: (?i)^\s*overflow_action\s*=\s*
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/audit/auditd.conf
    lineinfile:
      path: /etc/audit/auditd.conf
      create: true
      regexp: (?i)^\s*overflow_action\s*=\s*
      line: overflow_action = syslog
      state: present
  when:
  - '"audit" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030700
  - NIST-800-53-AU-4(1)
  - auditd_overflow_action
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

if [ -e "/etc/audit/auditd.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*overflow_action\s*=\s*/Id" "/etc/audit/auditd.conf"
else
    touch "/etc/audit/auditd.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/audit/auditd.conf"

cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
# Insert at the end of the file
printf '%s\n' "overflow_action = syslog" >> "/etc/audit/auditd.conf"
# Clean up after ourselves.
rm "/etc/audit/auditd.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   System Accounting with auditd   Group contains 1 rule
[ref]   The auditd program can perform comprehensive monitoring of system activity. This section makes use of recommended configuration settings for specific policies or use cases. The rules in this section make use of rules defined in /usr/share/doc/audit-VERSION/rules.

Rule   Ensure the audit Subsystem is Installed   [ref]

The audit package should be installed.
Rationale:
The auditd service is an access monitoring and accounting daemon, watching system calls to audit any access, in comparison with potential local access control policy such as SELinux policy.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_audit_installed
Identifiers and References

References:  BP28(R50), CCI-000130, CCI-000131, CCI-000132, CCI-000133, CCI-000134, CCI-000135, CCI-000154, CCI-000158, CCI-000172, CCI-001464, CCI-001487, CCI-001814, CCI-001875, CCI-001876, CCI-001877, CCI-001878, CCI-001879, CCI-001880, CCI-001881, CCI-001882, CCI-001889, CCI-001914, CCI-002884, CCI-000169, CIP-004-6 R3.3, CIP-007-3 R6.5, AC-7(a), AU-7(1), AU-7(2), AU-14, AU-12(2), AU-2(a), CM-6(a), FAU_GEN.1, Req-10.1, 10.2.1, SRG-OS-000062-GPOS-00031, SRG-OS-000037-GPOS-00015, SRG-OS-000038-GPOS-00016, SRG-OS-000039-GPOS-00017, SRG-OS-000040-GPOS-00018, SRG-OS-000041-GPOS-00019, SRG-OS-000042-GPOS-00021, SRG-OS-000051-GPOS-00024, SRG-OS-000054-GPOS-00025, SRG-OS-000122-GPOS-00063, SRG-OS-000254-GPOS-00095, SRG-OS-000255-GPOS-00096, SRG-OS-000337-GPOS-00129, SRG-OS-000348-GPOS-00136, SRG-OS-000349-GPOS-00137, SRG-OS-000350-GPOS-00138, SRG-OS-000351-GPOS-00139, SRG-OS-000352-GPOS-00140, SRG-OS-000353-GPOS-00141, SRG-OS-000354-GPOS-00142, SRG-OS-000358-GPOS-00145, SRG-OS-000365-GPOS-00152, SRG-OS-000392-GPOS-00172, SRG-OS-000475-GPOS-00220, OL08-00-030180, SV-248519r853745_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=audit


[[packages]]
name = "audit"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_audit

class install_audit {
  package { 'audit':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure audit is installed
  package:
    name: audit
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030180
  - NIST-800-53-AC-7(a)
  - NIST-800-53-AU-12(2)
  - NIST-800-53-AU-14
  - NIST-800-53-AU-2(a)
  - NIST-800-53-AU-7(1)
  - NIST-800-53-AU-7(2)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-10.1
  - PCI-DSSv4-10.2.1
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_audit_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "audit" ; then
    yum install -y "audit"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable auditd Service   [ref]

The auditd service is an essential userspace component of the Linux Auditing System, as it is responsible for writing audit records to disk. The auditd service can be enabled with the following command:
$ sudo systemctl enable auditd.service
Rationale:
Without establishing what type of events occurred, it would be difficult to establish, correlate, and investigate the events leading up to an outage or attack. Ensuring the auditd service is active ensures audit records generated by the kernel are appropriately recorded.

Additionally, a properly configured audit subsystem ensures that actions of individual system users can be uniquely traced to those users so they can be held accountable for their actions.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_auditd_enabled
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.3.1, 3.3.2, 3.3.6, CCI-000126, CCI-000130, CCI-000131, CCI-000132, CCI-000133, CCI-000134, CCI-000135, CCI-000154, CCI-000158, CCI-000172, CCI-000366, CCI-001464, CCI-001487, CCI-001814, CCI-001875, CCI-001876, CCI-001877, CCI-002884, CCI-001878, CCI-001879, CCI-001880, CCI-001881, CCI-001882, CCI-001889, CCI-001914, CCI-000169, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(C), 164.310(a)(2)(iv), 164.310(d)(2)(iii), 164.312(b), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, CIP-004-6 R3.3, CIP-007-3 R6.5, AC-2(g), AU-3, AU-10, AU-2(d), AU-12(c), AU-14(1), AC-6(9), CM-6(a), SI-4(23), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1, Req-10.1, 10.2.1, SRG-OS-000062-GPOS-00031, SRG-OS-000037-GPOS-00015, SRG-OS-000038-GPOS-00016, SRG-OS-000039-GPOS-00017, SRG-OS-000040-GPOS-00018, SRG-OS-000041-GPOS-00019, SRG-OS-000042-GPOS-00021, SRG-OS-000051-GPOS-00024, SRG-OS-000054-GPOS-00025, SRG-OS-000122-GPOS-00063, SRG-OS-000254-GPOS-00095, SRG-OS-000255-GPOS-00096, SRG-OS-000337-GPOS-00129, SRG-OS-000348-GPOS-00136, SRG-OS-000349-GPOS-00137, SRG-OS-000350-GPOS-00138, SRG-OS-000351-GPOS-00139, SRG-OS-000352-GPOS-00140, SRG-OS-000353-GPOS-00141, SRG-OS-000354-GPOS-00142, SRG-OS-000358-GPOS-00145, SRG-OS-000365-GPOS-00152, SRG-OS-000392-GPOS-00172, SRG-OS-000475-GPOS-00220, OL08-00-030181, SV-248520r853746_rule


---
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
spec:
  config:
    ignition:
      version: 3.1.0
    systemd:
      units:
      - name: auditd.service
        enabled: true


[customizations.services]
enabled = ["auditd"]

Complexity:low
Disruption:low
Strategy:enable
include enable_auditd

class enable_auditd {
  service {'auditd':
    enable => true,
    ensure => 'running',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030181
  - NIST-800-171-3.3.1
  - NIST-800-171-3.3.2
  - NIST-800-171-3.3.6
  - NIST-800-53-AC-2(g)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-10
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-14(1)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-AU-3
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-4(23)
  - PCI-DSS-Req-10.1
  - PCI-DSSv4-10.2.1
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_auditd_enabled

- name: Enable service auditd
  block:

  - name: Gather the package facts
    package_facts:
      manager: auto

  - name: Enable service auditd
    systemd:
      name: auditd
      enabled: 'yes'
      state: started
      masked: 'no'
    when:
    - '"audit" in ansible_facts.packages'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"audit" in ansible_facts.packages'
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030181
  - NIST-800-171-3.3.1
  - NIST-800-171-3.3.2
  - NIST-800-171-3.3.6
  - NIST-800-53-AC-2(g)
  - NIST-800-53-AC-6(9)
  - NIST-800-53-AU-10
  - NIST-800-53-AU-12(c)
  - NIST-800-53-AU-14(1)
  - NIST-800-53-AU-2(d)
  - NIST-800-53-AU-3
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-4(23)
  - PCI-DSS-Req-10.1
  - PCI-DSSv4-10.2.1
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_auditd_enabled

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q audit; }; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'auditd.service'
"$SYSTEMCTL_EXEC" start 'auditd.service'
"$SYSTEMCTL_EXEC" enable 'auditd.service'

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable Auditing for Processes Which Start Prior to the Audit Daemon   [ref]

To ensure all processes can be audited, even those which start prior to the audit daemon, add the argument audit=1 to the default GRUB 2 command line for the Linux operating system. To ensure that audit=1 is added as a kernel command line argument to newly installed kernels, add audit=1 to the default Grub2 command line for Linux operating systems. Modify the line within /etc/default/grub as shown below:
GRUB_CMDLINE_LINUX="... audit=1 ..."
Run the following command to update command line for already installed kernels:
# grubby --update-kernel=ALL --args="audit=1"
Rationale:
Each process on the system carries an "auditable" flag which indicates whether its activities can be audited. Although auditd takes care of enabling this for all processes which launch after it does, adding the kernel argument ensures it is set for every process during boot.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_grub2_audit_argument
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.02, DSS05.03, DSS05.04, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.3.1, CCI-001464, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(C), 164.310(a)(2)(iv), 164.310(d)(2)(iii), 164.312(b), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AC-17(1), AU-14(1), AU-10, CM-6(a), IR-5(1), DE.AE-3, DE.AE-5, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1, Req-10.3, 10.7, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000473-GPOS-00218, SRG-OS-000254-GPOS-00095, OL08-00-030601, SV-248803r853836_rule


[customizations.kernel]
append = "audit=1"

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030601
  - 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
  - PCI-DSSv4-10.7
  - grub2_audit_argument
  - low_disruption
  - low_severity
  - medium_complexity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="audit=1"
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"grub2-common" in ansible_facts.packages'
  tags:
  - CJIS-5.4.1.1
  - DISA-STIG-OL08-00-030601
  - 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
  - PCI-DSSv4-10.7
  - grub2_audit_argument
  - low_disruption
  - low_severity
  - medium_complexity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q grub2-common; }; then

grubby --update-kernel=ALL --args=audit=1 --env=/boot/grub2/grubenv

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Extend Audit Backlog Limit for the Audit Daemon   [ref]

To improve the kernel capacity to queue all log events, even those which occurred prior to the audit daemon, add the argument audit_backlog_limit=8192 to the default GRUB 2 command line for the Linux operating system. To ensure that audit_backlog_limit=8192 is added as a kernel command line argument to newly installed kernels, add audit_backlog_limit=8192 to the default Grub2 command line for Linux operating systems. Modify the line within /etc/default/grub as shown below:
GRUB_CMDLINE_LINUX="... audit_backlog_limit=8192 ..."
Run the following command to update command line for already installed kernels:
# grubby --update-kernel=ALL --args="audit_backlog_limit=8192"
Rationale:
audit_backlog_limit sets the queue length for audit events awaiting transfer to the audit daemon. Until the audit daemon is up and running, all log messages are stored in this queue. If the queue is overrun during boot process, the action defined by audit failure flag is taken.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_grub2_audit_backlog_limit_argument
Identifiers and References

References:  CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-001849, CCI-002884, CM-6(a), FAU_STG.1, FAU_STG.3, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000254-GPOS-00095, SRG-OS-000341-GPOS-00132, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, OL08-00-030602, SV-248804r853837_rule


[customizations.kernel]
append = "audit_backlog_limit=8192"

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030602
  - NIST-800-53-CM-6(a)
  - grub2_audit_backlog_limit_argument
  - low_disruption
  - low_severity
  - medium_complexity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="audit_backlog_limit=8192"
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"grub2-common" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-030602
  - NIST-800-53-CM-6(a)
  - grub2_audit_backlog_limit_argument
  - low_disruption
  - low_severity
  - medium_complexity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q grub2-common; }; then

grubby --update-kernel=ALL --args=audit_backlog_limit=8192 --env=/boot/grub2/grubenv

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   GRUB2 bootloader configuration   Group contains 2 groups and 7 rules
[ref]   During the boot process, the boot loader is responsible for starting the execution of the kernel and passing options to it. The boot loader allows for the selection of different kernels - possibly on different partitions or media. The default Oracle Linux 8 boot loader for x86 systems is called GRUB2. Options it can pass to the kernel include single-user mode, which provides root access without any authentication, and the ability to disable SELinux. To prevent local users from modifying the boot parameters and endangering security, protect the boot loader configuration with a password and ensure its configuration file's permissions are set properly.
Group   Non-UEFI GRUB2 bootloader configuration   Group contains 2 rules
[ref]   Non-UEFI GRUB2 bootloader configuration

Rule   Set the Boot Loader Admin Username to a Non-Default Value   [ref]

The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings.

To maximize the protection, select a password-protected superuser account with unique name, and modify the /etc/grub.d/01_users configuration file to reflect the account name change.

Do not to use common administrator account names like root, admin, or administrator for the grub2 superuser account.

Change the superuser to a different username (The default is 'root').
$ sed -i 's/\(set superuser=\).*/\1"<unique user ID>"/g' /etc/grub.d/01_users


Once the superuser account has been added, update the grub.cfg file by running:
grubby --update-kernel=ALL --env=/boot/grub2/grubenv
Warning:  To prevent hard-coded admin usernames, 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:
Having a non-default grub superuser username makes password-guessing attacks less effective.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_grub2_admin_username
Identifiers and References

References:  BP28(R17), 1, 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.4.5, CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048, OL08-00-010149, SV-248539r818605_rule

Rule   Set Boot Loader Password in grub2   [ref]

The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings.

Since plaintext passwords are a security risk, generate a hash for the password by running the following command:
# grub2-setpassword
When prompted, enter the password that was selected.

Warning:  To prevent hard-coded passwords, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the grub.cfg file as the grub2-mkconfig command overwrites this file.
Rationale:
Password protection on the boot loader configuration ensures users with physical access cannot trivially alter important bootloader settings. These include which kernel to use, and whether to enter single-user mode.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_grub2_password
Identifiers and References

References:  BP28(R17), 1, 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.4.5, CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048, OL08-00-010150, SV-248540r779186_rule

Group   UEFI GRUB2 bootloader configuration   Group contains 2 rules
[ref]   UEFI GRUB2 bootloader configuration

Rule   Set the UEFI Boot Loader Admin Username to a Non-Default Value   [ref]

The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings.

To maximize the protection, select a password-protected superuser account with unique name, and modify the /etc/grub.d/01_users configuration file to reflect the account name change.

It is highly suggested not to use common administrator account names like root, admin, or administrator for the grub2 superuser account.

Change the superuser to a different username (The default is 'root').
$ sed -i 's/\(set superusers=\).*/\1"<unique user ID>"/g' /etc/grub.d/01_users


Once the superuser account has been added, update the grub.cfg file by running:
grubby --update-kernel=ALL --env=/boot/grub2/grubenv
Warning:  To prevent hard-coded admin usernames, 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:
Having a non-default grub superuser username makes password-guessing attacks less effective.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_uefi_admin_username
Identifiers and References

References:  BP28(R17), 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06, 3.4.5, CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), PR.AC-4, PR.AC-6, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048, OL08-00-010141, SV-248538r818603_rule

Rule   Set the UEFI Boot Loader Password   [ref]

The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings.

Since plaintext passwords are a security risk, generate a hash for the password by running the following command:
# grub2-setpassword
When prompted, enter the password that was selected.

Warning:  To prevent hard-coded passwords, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the grub.cfg file as the grub2-mkconfig command overwrites this file.
Rationale:
Password protection on the boot loader configuration ensures users with physical access cannot trivially alter important bootloader settings. These include which kernel to use, and whether to enter single-user mode.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_grub2_uefi_password
Identifiers and References

References:  BP28(R17), 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06, 3.4.5, CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), PR.AC-4, PR.AC-6, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048, OL08-00-010140, SV-248537r779177_rule

Rule   System Must Avoid Meltdown and Spectre Exploit Vulnerabilities in Modern Processors   [ref]

Determine the default kernel:
$ sudo grubby --default-kernel

/boot/vmlinuz-5.4.17-2011.1.2.el8uek.x86_64
Using the default kernel, verify that Meltdown mitigations are not disabled:
$ sudo grubby --info=path-to-default-kernel | grep mitigation
The mitigation must be set to "on".
Rationale:
Hardware vulnerabilities allow programs to steal data that is currently processed on the computer. While programs are typically not permitted to read data from other programs, a malicious program can exploit Meltdown and Spectre to obtain secrets stored in the memory of other running programs. This might include passwords stored in a password manager or browser; personal photos, emails, and instant messages; and business-critical documents.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_mitigation_argument
Identifiers and References

References:  CCI-000366, CM-6(b), CM-6.1(iv), SRG-OS-000480-GPOS-00227, OL08-00-010424, SV-248593r779345_rule


[customizations.kernel]
append = "mitigation=on"

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010424
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - grub2_mitigation_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="mitigation=on"
  when: '"grub2-common" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010424
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - grub2_mitigation_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if rpm --quiet -q grub2-common; then

grubby --update-kernel=ALL --args=mitigation=on --env=/boot/grub2/grubenv

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable Kernel Page-Table Isolation (KPTI)   [ref]

To enable Kernel page-table isolation, add the argument pti=on to the default GRUB 2 command line for the Linux operating system. To ensure that pti=on is added as a kernel command line argument to newly installed kernels, add pti=on to the default Grub2 command line for Linux operating systems. Modify the line within /etc/default/grub as shown below:
GRUB_CMDLINE_LINUX="... pti=on ..."
Run the following command to update command line for already installed kernels:
# grubby --update-kernel=ALL --args="pti=on"
Rationale:
Kernel page-table isolation is a kernel feature that mitigates the Meltdown security vulnerability and hardens the kernel against attempts to bypass kernel address space layout randomization (KASLR).
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_grub2_pti_argument
Identifiers and References

References:  BP28(R8), CCI-000381, SI-16, SRG-OS-000433-GPOS-00193, SRG-OS-000095-GPOS-00049, OL08-00-040004, SV-248826r780044_rule


[customizations.kernel]
append = "pti=on"

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-040004
  - NIST-800-53-SI-16
  - grub2_pti_argument
  - low_disruption
  - low_severity
  - medium_complexity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="pti=on"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040004
  - NIST-800-53-SI-16
  - grub2_pti_argument
  - low_disruption
  - low_severity
  - medium_complexity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

grubby --update-kernel=ALL --args=pti=on --env=/boot/grub2/grubenv

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable vsyscalls   [ref]

To disable use of virtual syscalls, add the argument vsyscall=none to the default GRUB 2 command line for the Linux operating system. To ensure that vsyscall=none is added as a kernel command line argument to newly installed kernels, add vsyscall=none to the default Grub2 command line for Linux operating systems. Modify the line within /etc/default/grub as shown below:
GRUB_CMDLINE_LINUX="... vsyscall=none ..."
Run the following command to update command line for already installed kernels:
# grubby --update-kernel=ALL --args="vsyscall=none"
Rationale:
Virtual Syscalls provide an opportunity of attack for a user who has control of the return instruction pointer.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_vsyscall_argument
Identifiers and References

References:  CCI-001084, CM-7(a), FPT_ASLR_EXT.1, SRG-OS-000480-GPOS-00227, SRG-OS-000134-GPOS-00068, OL08-00-010422, SV-248591r779339_rule


[customizations.kernel]
append = "vsyscall=none"

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010422
  - NIST-800-53-CM-7(a)
  - grub2_vsyscall_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="vsyscall=none"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010422
  - NIST-800-53-CM-7(a)
  - grub2_vsyscall_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

grubby --update-kernel=ALL --args=vsyscall=none --env=/boot/grub2/grubenv

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Configure Syslog   Group contains 2 groups and 9 rules
[ref]   The syslog service has been the default Unix logging mechanism for many years. It has a number of downsides, including inconsistent log format, lack of authentication for received messages, and lack of authentication, encryption, or reliable transport for messages sent over a network. However, due to its long history, syslog is a de facto standard which is supported by almost all Unix applications.

In Oracle Linux 8, rsyslog has replaced ksyslogd as the syslog daemon of choice, and it includes some additional security features such as reliable, connection-oriented (i.e. TCP) transmission of logs, the option to log to database formats, and the encryption of log data en route to a central logging server. This section discusses how to configure rsyslog for best effect, and how to use tools provided with the system to maintain and monitor logs.
Group   Ensure Proper Configuration of Log Files   Group contains 5 rules
[ref]   The file /etc/rsyslog.conf controls where log message are written. These are controlled by lines called rules, which consist of a selector and an action. These rules are often customized depending on the role of the system, the requirements of the environment, and whatever may enable the administrator to most effectively make use of log data. The default rules in Oracle Linux 8 are:
*.info;mail.none;authpriv.none;cron.none                /var/log/messages
authpriv.*                                              /var/log/secure
mail.*                                                  -/var/log/maillog
cron.*                                                  /var/log/cron
*.emerg                                                 *
uucp,news.crit                                          /var/log/spooler
local7.*                                                /var/log/boot.log
See the man page rsyslog.conf(5) for more information. Note that the rsyslog daemon can be configured to use a timestamp format that some log processing programs may not understand. If this occurs, edit the file /etc/rsyslog.conf and add or edit the following line:
$ ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat

Rule   Ensure cron Is Logging To Rsyslog   [ref]

Cron logging must be implemented to spot intrusions or trace cron job status. If cron is not logging to rsyslog, it can be implemented by adding the following to the RULES section of /etc/rsyslog.conf:
cron.*                                                  /var/log/cron
Rationale:
Cron logging can be used to trace the successful or unsuccessful execution of cron jobs. It can also be used to spot intrusions into the use of the cron facility by unauthorized and malicious users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_rsyslog_cron_logging
Identifiers and References

References:  1, 14, 15, 16, 3, 5, 6, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS05.04, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, CCI-000366, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, 0988, 1405, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.15.2.1, A.15.2.2, CM-6(a), ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000480-GPOS-00227, OL08-00-030010, SV-248723r779735_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! grep -s "^\s*cron\.\*\s*/var/log/cron$" /etc/rsyslog.conf /etc/rsyslog.d/*.conf; then
	mkdir -p /etc/rsyslog.d
	echo "cron.*	/var/log/cron" >> /etc/rsyslog.d/cron.conf
fi

systemctl restart rsyslog.service

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure Rsyslog Authenticates Off-Loaded Audit Records   [ref]

Rsyslogd is a system utility providing support for message logging. Support for both internet and UNIX domain sockets enables this utility to support both local and remote logging. Couple this utility with gnutls (which is a secure communications library implementing the SSL, TLS and DTLS protocols), and you have a method to securely encrypt and off-load auditing. When using rsyslogd to off-load logs the remote system must be authenticated.
Rationale:
The audit records generated by Rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Audit records should be protected from unauthorized access.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_rsyslog_encrypt_offload_actionsendstreamdriverauthmode
Identifiers and References

References:  CCI-001851, AU-4(1), SRG-OS-000342-GPOS-00133, SRG-OS-000479-GPOS-00224, OL08-00-030720, SV-248817r877390_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Ensure Rsyslog Authenticates Off-Loaded Audit Records
  block:

  - name: Deduplicate values from /etc/rsyslog.conf
    lineinfile:
      path: /etc/rsyslog.conf
      create: false
      regexp: ^\s*{{ "$ActionSendStreamDriverAuthMode"| regex_escape }}\s
      state: absent

  - name: Check if /etc/rsyslog.d exists
    stat:
      path: /etc/rsyslog.d
    register: _etc_rsyslog_d_exists

  - name: Check if the parameter $ActionSendStreamDriverAuthMode is present in /etc/rsyslog.d
    find:
      paths: /etc/rsyslog.d
      recurse: 'yes'
      follow: 'no'
      contains: ^\s*{{ "$ActionSendStreamDriverAuthMode"| regex_escape }}\s
    register: _etc_rsyslog_d_has_parameter
    when: _etc_rsyslog_d_exists.stat.isdir is defined and _etc_rsyslog_d_exists.stat.isdir

  - name: Remove parameter from files in /etc/rsyslog.d
    lineinfile:
      path: '{{ item.path }}'
      create: false
      regexp: ^\s*{{ "$ActionSendStreamDriverAuthMode"| regex_escape }}\s
      state: absent
    with_items: '{{ _etc_rsyslog_d_has_parameter.files }}'
    when: _etc_rsyslog_d_has_parameter.matched

  - name: Insert correct line to /etc/rsyslog.conf
    lineinfile:
      path: /etc/rsyslog.conf
      create: true
      regexp: ^\s*{{ "$ActionSendStreamDriverAuthMode"| regex_escape }}\s
      line: $ActionSendStreamDriverAuthMode x509/name
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030720
  - NIST-800-53-AU-4(1)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_encrypt_offload_actionsendstreamdriverauthmode

Complexity:low
Disruption:low
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

sed -i '/^.*\$ActionSendStreamDriverAuthMode.*/d' /etc/rsyslog.conf /etc/rsyslog.d/*.conf 2> /dev/null

if [ -e "/etc/rsyslog.d/stream_driver_auth.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*\$ActionSendStreamDriverAuthMode /Id" "/etc/rsyslog.d/stream_driver_auth.conf"
else
    touch "/etc/rsyslog.d/stream_driver_auth.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/rsyslog.d/stream_driver_auth.conf"

cp "/etc/rsyslog.d/stream_driver_auth.conf" "/etc/rsyslog.d/stream_driver_auth.conf.bak"
# Insert at the end of the file
printf '%s\n' "\$ActionSendStreamDriverAuthMode x509/name" >> "/etc/rsyslog.d/stream_driver_auth.conf"
# Clean up after ourselves.
rm "/etc/rsyslog.d/stream_driver_auth.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure Rsyslog Encrypts Off-Loaded Audit Records   [ref]

Rsyslogd is a system utility providing support for message logging. Support for both internet and UNIX domain sockets enables this utility to support both local and remote logging. Couple this utility with gnutls (which is a secure communications library implementing the SSL, TLS and DTLS protocols), and you have a method to securely encrypt and off-load auditing. When using rsyslogd to off-load logs off a encrpytion system must be used.
Rationale:
The audit records generated by Rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Audit records should be protected from unauthorized access.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_rsyslog_encrypt_offload_actionsendstreamdrivermode
Identifiers and References

References:  CCI-001851, AU-4(1), SRG-OS-000342-GPOS-00133, SRG-OS-000479-GPOS-00224, OL08-00-030710, SV-248816r877390_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Ensure Rsyslog Encrypts Off-Loaded Audit Records
  block:

  - name: Deduplicate values from /etc/rsyslog.conf
    lineinfile:
      path: /etc/rsyslog.conf
      create: false
      regexp: '^\s*{{ "$ActionSendStreamDriverMode"| regex_escape }} '
      state: absent

  - name: Check if /etc/rsyslog.d exists
    stat:
      path: /etc/rsyslog.d
    register: _etc_rsyslog_d_exists

  - name: Check if the parameter $ActionSendStreamDriverMode is present in /etc/rsyslog.d
    find:
      paths: /etc/rsyslog.d
      recurse: 'yes'
      follow: 'no'
      contains: '^\s*{{ "$ActionSendStreamDriverMode"| regex_escape }} '
    register: _etc_rsyslog_d_has_parameter
    when: _etc_rsyslog_d_exists.stat.isdir is defined and _etc_rsyslog_d_exists.stat.isdir

  - name: Remove parameter from files in /etc/rsyslog.d
    lineinfile:
      path: '{{ item.path }}'
      create: false
      regexp: '^\s*{{ "$ActionSendStreamDriverMode"| regex_escape }} '
      state: absent
    with_items: '{{ _etc_rsyslog_d_has_parameter.files }}'
    when: _etc_rsyslog_d_has_parameter.matched

  - name: Insert correct line to /etc/rsyslog.conf
    lineinfile:
      path: /etc/rsyslog.conf
      create: true
      regexp: '^\s*{{ "$ActionSendStreamDriverMode"| regex_escape }} '
      line: $ActionSendStreamDriverMode 1
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030710
  - NIST-800-53-AU-4(1)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_encrypt_offload_actionsendstreamdrivermode

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if [ -e "/etc/rsyslog.d/encrypt.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*\$ActionSendStreamDriverMode /Id" "/etc/rsyslog.d/encrypt.conf"
else
    touch "/etc/rsyslog.d/encrypt.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/rsyslog.d/encrypt.conf"

cp "/etc/rsyslog.d/encrypt.conf" "/etc/rsyslog.d/encrypt.conf.bak"
# Insert at the end of the file
printf '%s\n' "\$ActionSendStreamDriverMode 1" >> "/etc/rsyslog.d/encrypt.conf"
# Clean up after ourselves.
rm "/etc/rsyslog.d/encrypt.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure Rsyslog Encrypts Off-Loaded Audit Records   [ref]

Rsyslogd is a system utility providing support for message logging. Support for both internet and UNIX domain sockets enables this utility to support both local and remote logging. Couple this utility with gnutls (which is a secure communications library implementing the SSL, TLS and DTLS protocols), and you have a method to securely encrypt and off-load auditing. When using rsyslogd to off-load logs off an encryption system must be used.
Rationale:
The audit records generated by Rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Audit records should be protected from unauthorized access.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_rsyslog_encrypt_offload_defaultnetstreamdriver
Identifiers and References

References:  CCI-001851, AU-4(1), SRG-OS-000342-GPOS-00133, SRG-OS-000479-GPOS-00224, OL08-00-030710, SV-248816r877390_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Ensure Rsyslog Encrypts Off-Loaded Audit Records
  block:

  - name: Deduplicate values from /etc/rsyslog.conf
    lineinfile:
      path: /etc/rsyslog.conf
      create: false
      regexp: '^\s*{{ "$DefaultNetstreamDriver"| regex_escape }} '
      state: absent

  - name: Check if /etc/rsyslog.d exists
    stat:
      path: /etc/rsyslog.d
    register: _etc_rsyslog_d_exists

  - name: Check if the parameter $DefaultNetstreamDriver is present in /etc/rsyslog.d
    find:
      paths: /etc/rsyslog.d
      recurse: 'yes'
      follow: 'no'
      contains: '^\s*{{ "$DefaultNetstreamDriver"| regex_escape }} '
    register: _etc_rsyslog_d_has_parameter
    when: _etc_rsyslog_d_exists.stat.isdir is defined and _etc_rsyslog_d_exists.stat.isdir

  - name: Remove parameter from files in /etc/rsyslog.d
    lineinfile:
      path: '{{ item.path }}'
      create: false
      regexp: '^\s*{{ "$DefaultNetstreamDriver"| regex_escape }} '
      state: absent
    with_items: '{{ _etc_rsyslog_d_has_parameter.files }}'
    when: _etc_rsyslog_d_has_parameter.matched

  - name: Insert correct line to /etc/rsyslog.conf
    lineinfile:
      path: /etc/rsyslog.conf
      create: true
      regexp: '^\s*{{ "$DefaultNetstreamDriver"| regex_escape }} '
      line: $DefaultNetstreamDriver gtls
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030710
  - NIST-800-53-AU-4(1)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_encrypt_offload_defaultnetstreamdriver

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if [ -e "/etc/rsyslog.d/encrypt.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*\$DefaultNetstreamDriver /Id" "/etc/rsyslog.d/encrypt.conf"
else
    touch "/etc/rsyslog.d/encrypt.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/rsyslog.d/encrypt.conf"

cp "/etc/rsyslog.d/encrypt.conf" "/etc/rsyslog.d/encrypt.conf.bak"
# Insert at the end of the file
printf '%s\n' "\$DefaultNetstreamDriver gtls" >> "/etc/rsyslog.d/encrypt.conf"
# Clean up after ourselves.
rm "/etc/rsyslog.d/encrypt.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure remote access methods are monitored in Rsyslog   [ref]

Logging of remote access methods must be implemented to help identify cyber attacks and ensure ongoing compliance with remote access policies are being audited and upheld. An examples of a remote access method is the use of the Remote Desktop Protocol (RDP) from an external, non-organization controlled network. The /etc/rsyslog.conf or /etc/rsyslog.d/*.conf file should contain a match for the following selectors: auth.*, authpriv.*, and daemon.*. If not, use the following as an example configuration:
auth.*;authpriv.*;daemon.*                              /var/log/secure
Rationale:
Logging remote access methods can be used to trace the decrease the risks associated with remote user access management. It can also be used to spot cyber attacks and ensure ongoing compliance with organizational policies surrounding the use of remote access methods.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_rsyslog_remote_access_monitoring
Identifiers and References

References:  CCI-000067, AC-17(1), SRG-OS-000032-GPOS-00013, OL08-00-010070, SV-248530r779156_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: 'Ensure remote access methods are monitored in Rsyslog: Set facts'
  set_fact:
    conf_files:
    - /etc/rsyslog.conf
    remote_methods:
    - selector: auth.*
      regexp: ^.*auth\.\*.*$
    - selector: authpriv.*
      regexp: ^.*authpriv\.\*.*$
    - selector: daemon.*
      regexp: ^.*daemon\.\*.*$
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010070
  - NIST-800-53-AC-17(1)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_remote_access_monitoring

- name: 'Ensure remote access methods are monitored in Rsyslog: Ensure rsyslog.conf
    exists'
  file:
    path: '{{ conf_files.0 }}'
    state: touch
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010070
  - NIST-800-53-AC-17(1)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_remote_access_monitoring

- name: 'Ensure remote access methods are monitored in Rsyslog: Gather conf.d files'
  find:
    patterns:
    - '*.conf'
    paths:
    - /etc/rsyslog.d
  register: rsyslogd
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010070
  - NIST-800-53-AC-17(1)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_remote_access_monitoring

- name: 'Ensure remote access methods are monitored in Rsyslog: Set conf file(s)'
  set_fact:
    conf_files: '{{ conf_files + [item.path] }}'
  loop: '{{ rsyslogd.files }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - rsyslogd.matched > 0
  tags:
  - DISA-STIG-OL08-00-010070
  - NIST-800-53-AC-17(1)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_remote_access_monitoring

- name: 'Ensure remote access methods are monitored in Rsyslog: Check for existing
    values'
  lineinfile:
    path: '{{ item.1 }}'
    regexp: '{{ item.0.regexp }}'
    state: absent
  check_mode: true
  changed_when: false
  register: remote_method_values
  loop: '{{ remote_methods|product(conf_files)|list }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010070
  - NIST-800-53-AC-17(1)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_remote_access_monitoring

- name: 'Ensure remote access methods are monitored in Rsyslog: Configure'
  lineinfile:
    path: /etc/rsyslog.conf
    line: '{{ item.item.0.selector }} /var/log/secure'
    insertafter: ^.*\/var\/log\/secure.*$
    create: true
  loop: '{{ remote_method_values.results }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - item.found == 0
  tags:
  - DISA-STIG-OL08-00-010070
  - NIST-800-53-AC-17(1)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - rsyslog_remote_access_monitoring

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

declare -A REMOTE_METHODS=( ['auth.*']='^[^#]*auth\.\*.*$' ['authpriv.*']='^[^#]*authpriv\.\*.*$' ['daemon.*']='^[^#]*daemon\.\*.*$' )

if [[ ! -f /etc/rsyslog.conf ]]; then
	# Something is not right, create the file
	touch /etc/rsyslog.conf
fi

APPEND_LINE=$(sed -rn '/^\S+\s+\/var\/log\/secure$/p' /etc/rsyslog.conf)

# Loop through the remote methods associative array
for K in "${!REMOTE_METHODS[@]}"
do
	# Check to see if selector/value exists
	if ! grep -rq "${REMOTE_METHODS[$K]}" /etc/rsyslog.*; then
		# Make sure we have a line to insert after, otherwise append to end
		if [[ ! -z ${APPEND_LINE} ]]; then
			# Add selector to file
			sed -r -i "0,/^(\S+\s+\/var\/log\/secure$)/s//\1\n${K} \/var\/log\/secure/" /etc/rsyslog.conf
		else
			echo "${K} /var/log/secure" >> /etc/rsyslog.conf
		fi
	fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Rsyslog Logs Sent To Remote Host   Group contains 1 rule
[ref]   If system logs are to be useful in detecting malicious activities, it is necessary to send logs to a remote server. An intruder who has compromised the root account on a system may delete the log entries which indicate that the system was attacked before they are seen by an administrator.

However, it is recommended that logs be stored on the local host in addition to being sent to the loghost, especially if rsyslog has been configured to use the UDP protocol to send messages over a network. UDP does not guarantee reliable delivery, and moderately busy sites will lose log messages occasionally, especially in periods of high traffic which may be the result of an attack. In addition, remote rsyslog messages are not authenticated in any way by default, so it is easy for an attacker to introduce spurious messages to the central log server. Also, some problems cause loss of network connectivity, which will prevent the sending of messages to the central server. For all of these reasons, it is better to store log messages both centrally and on each host, so that they can be correlated if necessary.

Rule   Ensure Logs Sent To Remote Host   [ref]

To configure rsyslog to send logs to a remote log server, open /etc/rsyslog.conf and read and understand the last section of the file, which describes the multiple directives necessary to activate remote logging. Along with these other directives, the system can be configured to forward its logs to a particular log server by adding or correcting one of the following lines, substituting logcollector appropriately. The choice of protocol depends on the environment of the system; although TCP and RELP provide more reliable message delivery, they may not be supported in all environments.
To use UDP for log message delivery:
*.* @logcollector

To use TCP for log message delivery:
*.* @@logcollector

To use RELP for log message delivery:
*.* :omrelp:logcollector

There must be a resolvable DNS CNAME or Alias record set to "logcollector" for logs to be sent correctly to the centralized logging utility.
Warning:  It is important to configure queues in case the client is sending log messages to a remote server. If queues are not configured, the system will stop functioning when the connection to the remote server is not available. Please consult Rsyslog documentation for more information about configuration of queues. The example configuration which should go into /etc/rsyslog.conf can look like the following lines:
$ActionQueueType LinkedList
$ActionQueueFileName queuefilename
$ActionQueueMaxDiskSpace 1g
$ActionQueueSaveOnShutdown on
$ActionResumeRetryCount -1
Rationale:
A log server (loghost) receives syslog messages from one or more systems. This data can be used as an additional log source in the event a system is compromised and its local logs are suspect. Forwarding log messages to a remote loghost also provides system administrators with a centralized place to view the status of multiple hosts within the enterprise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_rsyslog_remote_loghost
Identifiers and References

References:  BP28(R7), NT28(R43), NT12(R5), 1, 13, 14, 15, 16, 2, 3, 5, 6, APO11.04, APO13.01, BAI03.05, BAI04.04, DSS05.04, DSS05.07, MEA02.01, CCI-000366, CCI-001348, CCI-000136, CCI-001851, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(B), 164.308(a)(5)(ii)(C), 164.308(a)(6)(ii), 164.308(a)(8), 164.310(d)(2)(iii), 164.312(b), 164.314(a)(2)(i)(C), 164.314(a)(2)(iii), 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 7.1, SR 7.2, 0988, 1405, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.17.2.1, CIP-003-8 R5.2, CIP-004-6 R3.3, CM-6(a), AU-4(1), AU-9(2), PR.DS-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000479-GPOS-00224, SRG-OS-000480-GPOS-00227, SRG-OS-000342-GPOS-00133, OL08-00-030690, SV-248814r877390_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: XCCDF Value rsyslog_remote_loghost_address # promote to variable
  set_fact:
    rsyslog_remote_loghost_address: !!str logcollector
  tags:
    - always

- name: Set rsyslog remote loghost
  lineinfile:
    dest: /etc/rsyslog.conf
    regexp: ^\*\.\*
    line: '*.* @@{{ rsyslog_remote_loghost_address }}'
    create: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030690
  - NIST-800-53-AU-4(1)
  - NIST-800-53-AU-9(2)
  - NIST-800-53-CM-6(a)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - rsyslog_remote_loghost

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

rsyslog_remote_loghost_address='logcollector'


# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^\*\.\*")

# shellcheck disable=SC2059
printf -v formatted_output "%s %s" "$stripped_key" "@@$rsyslog_remote_loghost_address"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^\*\.\*\\>" "/etc/rsyslog.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^\*\.\*\\>.*/$escaped_formatted_output/gi" "/etc/rsyslog.conf"
else
    if [[ -s "/etc/rsyslog.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/rsyslog.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/rsyslog.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/rsyslog.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure rsyslog-gnutls is installed   [ref]

TLS protocol support for rsyslog is installed. The rsyslog-gnutls package can be installed with the following command:
$ sudo yum install rsyslog-gnutls
Rationale:
The rsyslog-gnutls package provides Transport Layer Security (TLS) support for the rsyslog daemon, which enables secure remote logging.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_rsyslog-gnutls_installed
Identifiers and References

References:  BP28(R43), CCI-000366, FTP_ITC_EXT.1.1, SRG-OS-000480-GPOS-00227, SRG-OS-000120-GPOS-00061, OL08-00-030680, SV-248813r780005_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=rsyslog-gnutls


[[packages]]
name = "rsyslog-gnutls"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_rsyslog-gnutls

class install_rsyslog-gnutls {
  package { 'rsyslog-gnutls':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure rsyslog-gnutls is installed
  package:
    name: rsyslog-gnutls
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030680
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_rsyslog-gnutls_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "rsyslog-gnutls" ; then
    yum install -y "rsyslog-gnutls"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure rsyslog is Installed   [ref]

Rsyslog is installed by default. The rsyslog package can be installed with the following command:
 $ sudo yum install rsyslog
Rationale:
The rsyslog package provides the rsyslog daemon, which provides system logging services.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_rsyslog_installed
Identifiers and References

References:  1, 14, 15, 16, 3, 5, 6, APO11.04, BAI03.05, DSS05.04, DSS05.07, MEA02.01, CCI-001311, CCI-001312, CCI-000366, 164.312(a)(2)(ii), 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, CM-6(a), PR.PT-1, FTP_ITC_EXT.1.1, SRG-OS-000479-GPOS-00224, SRG-OS-000051-GPOS-00024, SRG-OS-000480-GPOS-00227, OL08-00-030670, SV-248812r780002_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=rsyslog


[[packages]]
name = "rsyslog"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_rsyslog

class install_rsyslog {
  package { 'rsyslog':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure rsyslog is installed
  package:
    name: rsyslog
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030670
  - NIST-800-53-CM-6(a)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_rsyslog_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "rsyslog" ; then
    yum install -y "rsyslog"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable rsyslog Service   [ref]

The rsyslog service provides syslog-style logging by default on Oracle Linux 8. The rsyslog service can be enabled with the following command:
$ sudo systemctl enable rsyslog.service
Rationale:
The rsyslog service must be running in order to provide logging services, which are essential to system administration.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_rsyslog_enabled
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO13.01, BAI03.05, BAI04.04, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, CCI-001311, CCI-001312, CCI-001557, CCI-001851, CCI-000366, 164.312(a)(2)(ii), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, SR 7.1, SR 7.2, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, A.17.2.1, CM-6(a), AU-4(1), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.DS-4, PR.PT-1, SRG-OS-000480-GPOS-00227, OL08-00-010561, SV-248615r779411_rule



[customizations.services]
enabled = ["rsyslog"]

Complexity:low
Disruption:low
Strategy:enable
include enable_rsyslog

class enable_rsyslog {
  service {'rsyslog':
    enable => true,
    ensure => 'running',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Enable service rsyslog
  block:

  - name: Gather the package facts
    package_facts:
      manager: auto

  - name: Enable service rsyslog
    systemd:
      name: rsyslog
      enabled: 'yes'
      state: started
      masked: 'no'
    when:
    - '"rsyslog" in ansible_facts.packages'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010561
  - NIST-800-53-AU-4(1)
  - NIST-800-53-CM-6(a)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_rsyslog_enabled

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'rsyslog.service'
"$SYSTEMCTL_EXEC" start 'rsyslog.service'
"$SYSTEMCTL_EXEC" enable 'rsyslog.service'

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Network Configuration and Firewalls   Group contains 11 groups and 30 rules
[ref]   Most systems must be connected to a network of some sort, and this brings with it the substantial risk of network attack. This section discusses the security impact of decisions about networking which must be made when configuring a system.

This section also discusses firewalls, network access controls, and other network security frameworks, which allow system-level rules to be written that can limit an attackers' ability to connect to your system. These rules can specify that network traffic should be allowed or denied from certain IP addresses, hosts, and networks. The rules can also specify which of the system's network services are available to particular hosts or networks.
Group   firewalld   Group contains 2 groups and 5 rules
[ref]   The dynamic firewall daemon firewalld provides a dynamically managed firewall with support for network “zones” to assign a level of trust to a network and its associated connections and interfaces. It has support for IPv4 and IPv6 firewall settings. It supports Ethernet bridges and has a separation of runtime and permanent configuration options. It also has an interface for services or applications to add firewall rules directly.
A graphical configuration tool, firewall-config, is used to configure firewalld, which in turn uses iptables tool to communicate with Netfilter in the kernel which implements packet filtering.
The firewall service provided by firewalld is dynamic rather than static because changes to the configuration can be made at anytime and are immediately implemented. There is no need to save or apply the changes. No unintended disruption of existing network connections occurs as no part of the firewall has to be reloaded.
Group   Inspect and Activate Default firewalld Rules   Group contains 2 rules
[ref]   Firewalls can be used to separate networks into different zones based on the level of trust the user has decided to place on the devices and traffic within that network. NetworkManager informs firewalld to which zone an interface belongs. An interface's assigned zone can be changed by NetworkManager or via the firewall-config tool.
The zone settings in /etc/firewalld/ are a range of preset settings which can be quickly applied to a network interface. These are the zones provided by firewalld sorted according to the default trust level of the zones from untrusted to trusted:
  • drop

    Any incoming network packets are dropped, there is no reply. Only outgoing network connections are possible.

  • block

    Any incoming network connections are rejected with an icmp-host-prohibited message for IPv4 and icmp6-adm-prohibited for IPv6. Only network connections initiated from within the system are possible.

  • public

    For use in public areas. You do not trust the other computers on the network to not harm your computer. Only selected incoming connections are accepted.

  • external

    For use on external networks with masquerading enabled especially for routers. You do not trust the other computers on the network to not harm your computer. Only selected incoming connections are accepted.

  • dmz

    For computers in your demilitarized zone that are publicly-accessible with limited access to your internal network. Only selected incoming connections are accepted.

  • work

    For use in work areas. You mostly trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.

  • home

    For use in home areas. You mostly trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.

  • internal

    For use on internal networks. You mostly trust the other computers on the networks to not harm your computer. Only selected incoming connections are accepted.

  • trusted

    All network connections are accepted.


It is possible to designate one of these zones to be the default zone. When interface connections are added to NetworkManager, they are assigned to the default zone. On installation, the default zone in firewalld is set to be the public zone.
To find out all the settings of a zone, for example the public zone, enter the following command as root:
# firewall-cmd --zone=public --list-all
Example output of this command might look like the following:
# firewall-cmd --zone=public --list-all
public
  interfaces:
  services: mdns dhcpv6-client ssh
  ports:
  forward-ports:
  icmp-blocks: source-quench
To view the network zones currently active, enter the following command as root:
# firewall-cmd --get-service
The following listing displays the result of this command on common Oracle Linux 8 system:
# firewall-cmd --get-service
amanda-client bacula bacula-client dhcp dhcpv6 dhcpv6-client dns ftp
high-availability http https imaps ipp ipp-client ipsec kerberos kpasswd
ldap ldaps libvirt libvirt-tls mdns mountd ms-wbt mysql nfs ntp openvpn
pmcd pmproxy pmwebapi pmwebapis pop3s postgresql proxy-dhcp radius rpc-bind
samba samba-client smtp ssh telnet tftp tftp-client transmission-client
vnc-server wbem-https
Finally to view the network zones that will be active after the next firewalld service reload, enter the following command as root:
# firewall-cmd --get-service --permanent

Rule   Install firewalld Package   [ref]

The firewalld package can be installed with the following command:
$ sudo yum install firewalld
Rationale:
"Firewalld" provides an easy and effective way to block/limit remote access to the system via ports, services, and protocols. Remote access services, such as those providing remote access to network devices and information systems, which lack automated control capabilities, increase risk and make remote user access management difficult at best. Remote access is access to DoD nonpublic information systems by an authorized user (or an information system) communicating through an external, non-organization-controlled network. Remote access methods include, for example, dial-up, broadband, and wireless. Oracle Linux 8 functionality (e.g., SSH) must be capable of taking enforcement action if the audit reveals unauthorized activity. Automated control of remote access sessions allows organizations to ensure ongoing compliance with remote access policies by enforcing connection rules of remote access applications on a variety of information system components (e.g., servers, workstations, notebook computers, smartphones, and tablets)."
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_firewalld_installed
Identifiers and References

References:  CCI-002314, CM-6(a), FMT_SMF_EXT.1, SRG-OS-000096-GPOS-00050, SRG-OS-000297-GPOS-00115, SRG-OS-000298-GPOS-00116, SRG-OS-000480-GPOS-00227, SRG-OS-000480-GPOS-00232, OL08-00-040100, SV-248840r853847_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=firewalld


[[packages]]
name = "firewalld"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_firewalld

class install_firewalld {
  package { 'firewalld':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure firewalld is installed
  package:
    name: firewalld
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040100
  - NIST-800-53-CM-6(a)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_firewalld_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "firewalld" ; then
    yum install -y "firewalld"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Verify firewalld Enabled   [ref]

The firewalld service can be enabled with the following command:
$ sudo systemctl enable firewalld.service
Rationale:
Access control methods provide the ability to enhance system security posture by restricting services and known good IP addresses and address ranges. This prevents connections from unknown hosts and protocols.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_firewalld_enabled
Identifiers and References

References:  11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.3, 3.4.7, CCI-000366, CCI-000382, CCI-002314, 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, CIP-003-8 R4, CIP-003-8 R5, CIP-004-6 R3, AC-4, CM-7(b), CA-3(5), SC-7(21), CM-6(a), PR.IP-1, FMT_SMF_EXT.1, SRG-OS-000096-GPOS-00050, SRG-OS-000297-GPOS-00115, SRG-OS-000480-GPOS-00227, SRG-OS-000480-GPOS-00231, SRG-OS-000480-GPOS-00232, OL08-00-040101, SV-248841r853848_rule



[customizations.services]
enabled = ["firewalld"]

Complexity:low
Disruption:low
Strategy:enable
include enable_firewalld

class enable_firewalld {
  service {'firewalld':
    enable => true,
    ensure => 'running',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Enable service firewalld
  block:

  - name: Gather the package facts
    package_facts:
      manager: auto

  - name: Enable service firewalld
    systemd:
      name: firewalld
      enabled: 'yes'
      state: started
      masked: 'no'
    when:
    - '"firewalld" in ansible_facts.packages'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040101
  - NIST-800-171-3.1.3
  - NIST-800-171-3.4.7
  - NIST-800-53-AC-4
  - NIST-800-53-CA-3(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(21)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_firewalld_enabled

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'firewalld.service'
"$SYSTEMCTL_EXEC" start 'firewalld.service'
"$SYSTEMCTL_EXEC" enable 'firewalld.service'

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Strengthen the Default Ruleset   Group contains 2 rules
[ref]   The default rules can be strengthened. The system scripts that activate the firewall rules expect them to be defined in configuration files under the /etc/firewalld/services and /etc/firewalld/zones directories.

The following recommendations describe how to strengthen the default ruleset configuration file. An alternative to editing this configuration file is to create a shell script that makes calls to the firewall-cmd program to load in rules under the /etc/firewalld/services and /etc/firewalld/zones directories.

Instructions apply to both unless otherwise noted. Language and address conventions for regular firewalld rules are used throughout this section.
Warning:  The program firewall-config allows additional services to penetrate the default firewall rules and automatically adjusts the firewalld ruleset(s).

Rule   Configure the Firewalld Ports   [ref]

Configure the firewalld ports to allow approved services to have access to the system. To configure firewalld to open ports, run the following command:
firewall-cmd --permanent --add-port=port_number/tcp
To configure firewalld to allow access for pre-defined services, run the following command:
firewall-cmd --permanent --add-service=service_name
Rationale:
In order to prevent unauthorized connection of devices, unauthorized transfer of information, or unauthorized tunneling (i.e., embedding of data types within data types), organizations must disable or restrict unused or unnecessary physical and logical ports/protocols on information systems.

Operating systems are capable of providing a wide variety of functions and services. Some of the functions and services provided by default may not be necessary to support essential organizational operations. Additionally, it is sometimes convenient to provide multiple services from a single component (e.g., VPN and IPS); however, doing so increases risk over limiting the services provided by one component.

To support the requirements and principles of least functionality, the operating system must support the organizational requirements, providing only essential capabilities and limiting the use of ports, protocols, and/or services to only those required, authorized, and approved to conduct official business.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configure_firewalld_ports
Identifiers and References

References:  11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000382, CCI-002314, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, 1416, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, AC-4, CM-7(b), CA-3(5), SC-7(21), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000096-GPOS-00050, SRG-OS-000297-GPOS-00115, OL08-00-040030, SV-248835r780071_rule

Rule   Firewalld Must Employ a Deny-all, Allow-by-exception Policy for Allowing Connections to Other Systems   [ref]

Oracle Linux 8 incorporates the "firewalld" daemon, which allows for many different configurations. One of these configurations is zones. Zones can be utilized to a deny-all, allow-by-exception approach. The default "drop" zone will drop all incoming network packets unless it is explicitly allowed by the configuration file or is related to an outgoing network connection.
Rationale:
Failure to restrict network connectivity only to authorized systems permits inbound connections from malicious systems. It also permits outbound connections that may facilitate exfiltration of data.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_configured_firewalld_default_deny
Identifiers and References

References:  CCI-002314, AC-17 (1), SRG-OS-000297-GPOS-00115, OL08-00-040090, SV-248839r853846_rule

Rule   Configure Firewalld to Use the Nftables Backend   [ref]

Firewalld can be configured with many backends, such as nftables.
Rationale:
Nftables is modern kernel module for controling network connections coming into a system. Utilizing the limit statement in "nftables" can help to mitigate DoS attacks.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_firewalld-backend
Identifiers and References

References:  CCI-002385, SC-5, SRG-OS-000420-GPOS-00186, OL08-00-040150, SV-248865r902788_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Setting shell-quoted shell-style assignment of 'FirewallBackend' to 'nftables'
    in '/etc/firewalld/firewalld.conf'
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/firewalld/firewalld.conf
      create: false
      regexp: ^\s*FirewallBackend=
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/firewalld/firewalld.conf
    lineinfile:
      path: /etc/firewalld/firewalld.conf
      create: false
      regexp: ^\s*FirewallBackend=
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/firewalld/firewalld.conf
    lineinfile:
      path: /etc/firewalld/firewalld.conf
      create: true
      regexp: ^\s*FirewallBackend=
      line: FirewallBackend="nftables"
      state: present
      insertbefore: ^# FirewallBackend
      validate: /usr/bin/bash -n %s
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040150
  - NIST-800-53-SC-5
  - firewalld-backend
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if [ -e "/etc/firewalld/firewalld.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*FirewallBackend\s*=\s*/d" "/etc/firewalld/firewalld.conf"
else
    touch "/etc/firewalld/firewalld.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/firewalld/firewalld.conf"

cp "/etc/firewalld/firewalld.conf" "/etc/firewalld/firewalld.conf.bak"
# Insert before the line matching the regex '^#\s*FirewallBackend'.
line_number="$(LC_ALL=C grep -n "^#\s*FirewallBackend" "/etc/firewalld/firewalld.conf.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^#\s*FirewallBackend', insert at
    # the end of the file.
    printf '%s\n' "FirewallBackend='nftables'" >> "/etc/firewalld/firewalld.conf"
else
    head -n "$(( line_number - 1 ))" "/etc/firewalld/firewalld.conf.bak" > "/etc/firewalld/firewalld.conf"
    printf '%s\n' "FirewallBackend='nftables'" >> "/etc/firewalld/firewalld.conf"
    tail -n "+$(( line_number ))" "/etc/firewalld/firewalld.conf.bak" >> "/etc/firewalld/firewalld.conf"
fi
# Clean up after ourselves.
rm "/etc/firewalld/firewalld.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   IPv6   Group contains 1 group and 7 rules
[ref]   The system includes support for Internet Protocol version 6. A major and often-mentioned improvement over IPv4 is its enormous increase in the number of available addresses. Another important feature is its support for automatic configuration of many network settings.
Group   Configure IPv6 Settings if Necessary   Group contains 7 rules
[ref]   A major feature of IPv6 is the extent to which systems implementing it can automatically configure their networking devices using information from the network. From a security perspective, manually configuring important configuration information is preferable to accepting it from the network in an unauthenticated fashion.

Rule   Configure Accepting Router Advertisements on All IPv6 Interfaces   [ref]

To set the runtime status of the net.ipv6.conf.all.accept_ra kernel parameter, run the following command:
$ sudo sysctl -w net.ipv6.conf.all.accept_ra=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv6.conf.all.accept_ra = 0
Rationale:
An illicit router advertisement message could result in a man-in-the-middle attack.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_all_accept_ra
Identifiers and References

References:  11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, 3.1.20, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-040261, SV-248884r858671_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv6.conf.all.accept_ra.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040261
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_ra

- name: Comment out any occurrences of net.ipv6.conf.all.accept_ra from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv6.conf.all.accept_ra
    replace: '#net.ipv6.conf.all.accept_ra'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040261
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_ra
- name: XCCDF Value sysctl_net_ipv6_conf_all_accept_ra_value # promote to variable
  set_fact:
    sysctl_net_ipv6_conf_all_accept_ra_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv6.conf.all.accept_ra is set
  sysctl:
    name: net.ipv6.conf.all.accept_ra
    value: '{{ sysctl_net_ipv6_conf_all_accept_ra_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040261
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_ra

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv6.conf.all.accept_ra from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_ra.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv6.conf.all.accept_ra" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv6_conf_all_accept_ra_value='0'


#
# Set runtime for net.ipv6.conf.all.accept_ra
#
/sbin/sysctl -q -n -w net.ipv6.conf.all.accept_ra="$sysctl_net_ipv6_conf_all_accept_ra_value"

#
# If net.ipv6.conf.all.accept_ra present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv6.conf.all.accept_ra = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_ra")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_ra_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_ra\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_ra\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Accepting ICMP Redirects for All IPv6 Interfaces   [ref]

To set the runtime status of the net.ipv6.conf.all.accept_redirects kernel parameter, run the following command:
$ sudo sysctl -w net.ipv6.conf.all.accept_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv6.conf.all.accept_redirects = 0
Rationale:
An illicit ICMP redirect message could result in a man-in-the-middle attack.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_all_accept_redirects
Identifiers and References

References:  BP28(R22), 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, 3.1.20, CCI-000366, CCI-001551, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), CM-6(b), CM-6.1(iv), PR.IP-1, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-040280, SV-248888r858679_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv6.conf.all.accept_redirects.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040280
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_redirects

- name: Comment out any occurrences of net.ipv6.conf.all.accept_redirects from config
    files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv6.conf.all.accept_redirects
    replace: '#net.ipv6.conf.all.accept_redirects'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040280
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_redirects
- name: XCCDF Value sysctl_net_ipv6_conf_all_accept_redirects_value # promote to variable
  set_fact:
    sysctl_net_ipv6_conf_all_accept_redirects_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv6.conf.all.accept_redirects is set
  sysctl:
    name: net.ipv6.conf.all.accept_redirects
    value: '{{ sysctl_net_ipv6_conf_all_accept_redirects_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040280
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_redirects

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv6.conf.all.accept_redirects from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_redirects.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv6.conf.all.accept_redirects" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv6_conf_all_accept_redirects_value='0'


#
# Set runtime for net.ipv6.conf.all.accept_redirects
#
/sbin/sysctl -q -n -w net.ipv6.conf.all.accept_redirects="$sysctl_net_ipv6_conf_all_accept_redirects_value"

#
# If net.ipv6.conf.all.accept_redirects present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv6.conf.all.accept_redirects = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_redirects")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_redirects_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_redirects\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces   [ref]

To set the runtime status of the net.ipv6.conf.all.accept_source_route kernel parameter, run the following command:
$ sudo sysctl -w net.ipv6.conf.all.accept_source_route=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv6.conf.all.accept_source_route = 0
Rationale:
Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv6 forwarding is enabled and the system is functioning as a router.

Accepting source-routed packets in the IPv6 protocol has few legitimate uses. It should be disabled unless it is absolutely required.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_all_accept_source_route
Identifiers and References

References:  BP28(R22), 1, 12, 13, 14, 15, 16, 18, 4, 6, 8, 9, APO01.06, APO13.01, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.07, DSS06.02, 3.1.20, CCI-000366, 4.2.3.4, 4.3.3.4, 4.4.3.3, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-7(a), CM-7(b), CM-6(a), DE.AE-1, ID.AM-3, PR.AC-5, PR.DS-5, PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-040240, SV-248880r858661_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv6.conf.all.accept_source_route.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040240
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_source_route

- name: Comment out any occurrences of net.ipv6.conf.all.accept_source_route from
    config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv6.conf.all.accept_source_route
    replace: '#net.ipv6.conf.all.accept_source_route'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040240
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_source_route
- name: XCCDF Value sysctl_net_ipv6_conf_all_accept_source_route_value # promote to variable
  set_fact:
    sysctl_net_ipv6_conf_all_accept_source_route_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv6.conf.all.accept_source_route is set
  sysctl:
    name: net.ipv6.conf.all.accept_source_route
    value: '{{ sysctl_net_ipv6_conf_all_accept_source_route_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040240
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_accept_source_route

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv6.conf.all.accept_source_route from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_source_route.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv6.conf.all.accept_source_route" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv6_conf_all_accept_source_route_value='0'


#
# Set runtime for net.ipv6.conf.all.accept_source_route
#
/sbin/sysctl -q -n -w net.ipv6.conf.all.accept_source_route="$sysctl_net_ipv6_conf_all_accept_source_route_value"

#
# If net.ipv6.conf.all.accept_source_route present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv6.conf.all.accept_source_route = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_source_route")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_source_route_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_source_route\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for IPv6 Forwarding   [ref]

To set the runtime status of the net.ipv6.conf.all.forwarding kernel parameter, run the following command:
$ sudo sysctl -w net.ipv6.conf.all.forwarding=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv6.conf.all.forwarding = 0
Rationale:
IP forwarding permits the kernel to forward packets from one network interface to another. The ability to forward packets between two networks is only appropriate for systems acting as routers.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_all_forwarding
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 2, 3, 7, 8, 9, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS05.02, DSS05.05, DSS05.07, DSS06.06, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 6.2, SR 7.1, SR 7.2, SR 7.6, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), CM-6(b), CM-6.1(iv), DE.CM-1, PR.DS-4, PR.IP-1, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-040260, SV-248883r858669_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv6.conf.all.forwarding.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040260
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_forwarding

- name: Comment out any occurrences of net.ipv6.conf.all.forwarding from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv6.conf.all.forwarding
    replace: '#net.ipv6.conf.all.forwarding'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040260
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_forwarding
- name: XCCDF Value sysctl_net_ipv6_conf_all_forwarding_value # promote to variable
  set_fact:
    sysctl_net_ipv6_conf_all_forwarding_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv6.conf.all.forwarding is set
  sysctl:
    name: net.ipv6.conf.all.forwarding
    value: '{{ sysctl_net_ipv6_conf_all_forwarding_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040260
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_all_forwarding

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv6.conf.all.forwarding from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.forwarding.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv6.conf.all.forwarding" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv6_conf_all_forwarding_value='0'


#
# Set runtime for net.ipv6.conf.all.forwarding
#
/sbin/sysctl -q -n -w net.ipv6.conf.all.forwarding="$sysctl_net_ipv6_conf_all_forwarding_value"

#
# If net.ipv6.conf.all.forwarding present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv6.conf.all.forwarding = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.forwarding")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_forwarding_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.forwarding\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.forwarding\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Accepting Router Advertisements on all IPv6 Interfaces by Default   [ref]

To set the runtime status of the net.ipv6.conf.default.accept_ra kernel parameter, run the following command:
$ sudo sysctl -w net.ipv6.conf.default.accept_ra=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv6.conf.default.accept_ra = 0
Rationale:
An illicit router advertisement message could result in a man-in-the-middle attack.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_default_accept_ra
Identifiers and References

References:  11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, 3.1.20, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-040262, SV-248885r858673_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv6.conf.default.accept_ra.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040262
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_ra

- name: Comment out any occurrences of net.ipv6.conf.default.accept_ra from config
    files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv6.conf.default.accept_ra
    replace: '#net.ipv6.conf.default.accept_ra'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040262
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_ra
- name: XCCDF Value sysctl_net_ipv6_conf_default_accept_ra_value # promote to variable
  set_fact:
    sysctl_net_ipv6_conf_default_accept_ra_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv6.conf.default.accept_ra is set
  sysctl:
    name: net.ipv6.conf.default.accept_ra
    value: '{{ sysctl_net_ipv6_conf_default_accept_ra_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040262
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_ra

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv6.conf.default.accept_ra from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_ra.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv6.conf.default.accept_ra" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv6_conf_default_accept_ra_value='0'


#
# Set runtime for net.ipv6.conf.default.accept_ra
#
/sbin/sysctl -q -n -w net.ipv6.conf.default.accept_ra="$sysctl_net_ipv6_conf_default_accept_ra_value"

#
# If net.ipv6.conf.default.accept_ra present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv6.conf.default.accept_ra = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_ra")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_ra_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_ra\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_ra\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces   [ref]

To set the runtime status of the net.ipv6.conf.default.accept_redirects kernel parameter, run the following command:
$ sudo sysctl -w net.ipv6.conf.default.accept_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv6.conf.default.accept_redirects = 0
Rationale:
An illicit ICMP redirect message could result in a man-in-the-middle attack.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_default_accept_redirects
Identifiers and References

References:  BP28(R22), 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, 3.1.20, CCI-000366, CCI-001551, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-040210, SV-248876r858653_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv6.conf.default.accept_redirects.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040210
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_redirects

- name: Comment out any occurrences of net.ipv6.conf.default.accept_redirects from
    config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv6.conf.default.accept_redirects
    replace: '#net.ipv6.conf.default.accept_redirects'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040210
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_redirects
- name: XCCDF Value sysctl_net_ipv6_conf_default_accept_redirects_value # promote to variable
  set_fact:
    sysctl_net_ipv6_conf_default_accept_redirects_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv6.conf.default.accept_redirects is set
  sysctl:
    name: net.ipv6.conf.default.accept_redirects
    value: '{{ sysctl_net_ipv6_conf_default_accept_redirects_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040210
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_redirects

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv6.conf.default.accept_redirects from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_redirects.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv6.conf.default.accept_redirects" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv6_conf_default_accept_redirects_value='0'


#
# Set runtime for net.ipv6.conf.default.accept_redirects
#
/sbin/sysctl -q -n -w net.ipv6.conf.default.accept_redirects="$sysctl_net_ipv6_conf_default_accept_redirects_value"

#
# If net.ipv6.conf.default.accept_redirects present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv6.conf.default.accept_redirects = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_redirects")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_redirects_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_redirects\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces by Default   [ref]

To set the runtime status of the net.ipv6.conf.default.accept_source_route kernel parameter, run the following command:
$ sudo sysctl -w net.ipv6.conf.default.accept_source_route=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv6.conf.default.accept_source_route = 0
Rationale:
Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv6 forwarding is enabled and the system is functioning as a router. Accepting source-routed packets in the IPv6 protocol has few legitimate uses. It should be disabled unless it is absolutely required.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_default_accept_source_route
Identifiers and References

References:  BP28(R22), 1, 12, 13, 14, 15, 16, 18, 4, 6, 8, 9, APO01.06, APO13.01, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.07, DSS06.02, 3.1.20, CCI-000366, 4.2.3.4, 4.3.3.4, 4.4.3.3, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-7(a), CM-7(b), CM-6(a), CM-6(b), CM-6.1(iv), DE.AE-1, ID.AM-3, PR.AC-5, PR.DS-5, PR.PT-4, Req-1.4.3, SRG-OS-000480-GPOS-00227, OL08-00-040250, SV-248882r858665_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv6.conf.default.accept_source_route.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040250
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_source_route

- name: Comment out any occurrences of net.ipv6.conf.default.accept_source_route from
    config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv6.conf.default.accept_source_route
    replace: '#net.ipv6.conf.default.accept_source_route'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040250
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_source_route
- name: XCCDF Value sysctl_net_ipv6_conf_default_accept_source_route_value # promote to variable
  set_fact:
    sysctl_net_ipv6_conf_default_accept_source_route_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv6.conf.default.accept_source_route is set
  sysctl:
    name: net.ipv6.conf.default.accept_source_route
    value: '{{ sysctl_net_ipv6_conf_default_accept_source_route_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040250
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv6_conf_default_accept_source_route

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv6.conf.default.accept_source_route from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_source_route.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv6.conf.default.accept_source_route" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv6_conf_default_accept_source_route_value='0'


#
# Set runtime for net.ipv6.conf.default.accept_source_route
#
/sbin/sysctl -q -n -w net.ipv6.conf.default.accept_source_route="$sysctl_net_ipv6_conf_default_accept_source_route_value"

#
# If net.ipv6.conf.default.accept_source_route present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv6.conf.default.accept_source_route = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_source_route")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_source_route_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_source_route\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Kernel Parameters Which Affect Networking   Group contains 2 groups and 9 rules
[ref]   The sysctl utility is used to set parameters which affect the operation of the Linux kernel. Kernel parameters which affect networking and have security implications are described here.
Group   Network Related Kernel Runtime Parameters for Hosts and Routers   Group contains 6 rules
[ref]   Certain kernel parameters should be set for systems which are acting as either hosts or routers to improve the system's ability defend against certain types of IPv4 protocol attacks.

Rule   Disable Accepting ICMP Redirects for All IPv4 Interfaces   [ref]

To set the runtime status of the net.ipv4.conf.all.accept_redirects kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.conf.all.accept_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.conf.all.accept_redirects = 0
Rationale:
ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages modify the host's route table and are unauthenticated. An illicit ICMP redirect message could result in a man-in-the-middle attack.
This feature of the IPv4 protocol has few legitimate uses. It should be disabled unless absolutely required."
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_accept_redirects
Identifiers and References

References:  BP28(R22), 1, 11, 12, 13, 14, 15, 16, 2, 3, 7, 8, 9, 5.10.1.1, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS05.02, DSS05.05, DSS05.07, DSS06.06, 3.1.20, CCI-000366, CCI-001503, CCI-001551, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 6.2, SR 7.1, SR 7.2, SR 7.6, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), SC-7(a), DE.CM-1, PR.DS-4, PR.IP-1, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-040279, SV-248887r858677_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.conf.all.accept_redirects.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040279
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_accept_redirects

- name: Comment out any occurrences of net.ipv4.conf.all.accept_redirects from config
    files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.conf.all.accept_redirects
    replace: '#net.ipv4.conf.all.accept_redirects'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040279
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_accept_redirects
- name: XCCDF Value sysctl_net_ipv4_conf_all_accept_redirects_value # promote to variable
  set_fact:
    sysctl_net_ipv4_conf_all_accept_redirects_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv4.conf.all.accept_redirects is set
  sysctl:
    name: net.ipv4.conf.all.accept_redirects
    value: '{{ sysctl_net_ipv4_conf_all_accept_redirects_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040279
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_accept_redirects

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.conf.all.accept_redirects from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.accept_redirects.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.conf.all.accept_redirects" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv4_conf_all_accept_redirects_value='0'


#
# Set runtime for net.ipv4.conf.all.accept_redirects
#
/sbin/sysctl -q -n -w net.ipv4.conf.all.accept_redirects="$sysctl_net_ipv4_conf_all_accept_redirects_value"

#
# If net.ipv4.conf.all.accept_redirects present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv4.conf.all.accept_redirects = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.accept_redirects")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_accept_redirects_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.accept_redirects\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces   [ref]

To set the runtime status of the net.ipv4.conf.all.accept_source_route kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.conf.all.accept_source_route=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.conf.all.accept_source_route = 0
Rationale:
Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv4 forwarding is enabled and the system is functioning as a router.

Accepting source-routed packets in the IPv4 protocol has few legitimate uses. It should be disabled unless it is absolutely required.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_accept_source_route
Identifiers and References

References:  BP28(R22), 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9, APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, 3.1.20, CCI-000366, 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, 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.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-7(a), CM-7(b), SC-5, CM-6(a), SC-7(a), DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-040239, SV-248879r858659_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.conf.all.accept_source_route.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040239
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_accept_source_route

- name: Comment out any occurrences of net.ipv4.conf.all.accept_source_route from
    config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.conf.all.accept_source_route
    replace: '#net.ipv4.conf.all.accept_source_route'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040239
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_accept_source_route
- name: XCCDF Value sysctl_net_ipv4_conf_all_accept_source_route_value # promote to variable
  set_fact:
    sysctl_net_ipv4_conf_all_accept_source_route_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv4.conf.all.accept_source_route is set
  sysctl:
    name: net.ipv4.conf.all.accept_source_route
    value: '{{ sysctl_net_ipv4_conf_all_accept_source_route_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040239
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_accept_source_route

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.conf.all.accept_source_route from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.accept_source_route.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.conf.all.accept_source_route" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv4_conf_all_accept_source_route_value='0'


#
# Set runtime for net.ipv4.conf.all.accept_source_route
#
/sbin/sysctl -q -n -w net.ipv4.conf.all.accept_source_route="$sysctl_net_ipv4_conf_all_accept_source_route_value"

#
# If net.ipv4.conf.all.accept_source_route present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv4.conf.all.accept_source_route = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.accept_source_route")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_accept_source_route_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.accept_source_route\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces   [ref]

To set the runtime status of the net.ipv4.conf.all.rp_filter kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.conf.all.rp_filter=1
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.conf.all.rp_filter = 1
Rationale:
Enabling reverse path filtering drops packets with source addresses that should not have been able to be received on the interface they were received on. It should not be used on systems which are routers for complicated networks, but is helpful for end hosts and routers serving small networks.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_rp_filter
Identifiers and References

References:  BP28(R22), 1, 12, 13, 14, 15, 16, 18, 2, 4, 6, 7, 8, 9, APO01.06, APO13.01, BAI04.04, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.07, DSS06.02, 3.1.20, CCI-000366, CCI-001551, 4.2.3.4, 4.3.3.4, 4.4.3.3, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, 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.1.3, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-7(a), CM-7(b), CM-6(a), SC-7(a), DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.PT-4, Req-1.4.3, SRG-OS-000480-GPOS-00227, OL08-00-040285, SV-248893r858689_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.conf.all.rp_filter.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040285
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_rp_filter

- name: Comment out any occurrences of net.ipv4.conf.all.rp_filter from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.conf.all.rp_filter
    replace: '#net.ipv4.conf.all.rp_filter'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040285
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_rp_filter
- name: XCCDF Value sysctl_net_ipv4_conf_all_rp_filter_value # promote to variable
  set_fact:
    sysctl_net_ipv4_conf_all_rp_filter_value: !!str 1
  tags:
    - always

- name: Ensure sysctl net.ipv4.conf.all.rp_filter is set
  sysctl:
    name: net.ipv4.conf.all.rp_filter
    value: '{{ sysctl_net_ipv4_conf_all_rp_filter_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040285
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_rp_filter

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.conf.all.rp_filter from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.rp_filter.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.conf.all.rp_filter" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv4_conf_all_rp_filter_value='1'


#
# Set runtime for net.ipv4.conf.all.rp_filter
#
/sbin/sysctl -q -n -w net.ipv4.conf.all.rp_filter="$sysctl_net_ipv4_conf_all_rp_filter_value"

#
# If net.ipv4.conf.all.rp_filter present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv4.conf.all.rp_filter = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.rp_filter")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_rp_filter_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.rp_filter\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.rp_filter\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces   [ref]

To set the runtime status of the net.ipv4.conf.default.accept_redirects kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.conf.default.accept_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.conf.default.accept_redirects = 0
Rationale:
ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages modify the host's route table and are unauthenticated. An illicit ICMP redirect message could result in a man-in-the-middle attack.
This feature of the IPv4 protocol has few legitimate uses. It should be disabled unless absolutely required.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_accept_redirects
Identifiers and References

References:  BP28(R22), 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9, 5.10.1.1, APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, 3.1.20, CCI-000366, CCI-001551, 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, 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.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-7(a), CM-7(b), CM-6(a), SC-7(a), DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4, Req-1.4.3, SRG-OS-000480-GPOS-00227, OL08-00-040209, SV-248875r858651_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.conf.default.accept_redirects.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040209
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_accept_redirects

- name: Comment out any occurrences of net.ipv4.conf.default.accept_redirects from
    config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.conf.default.accept_redirects
    replace: '#net.ipv4.conf.default.accept_redirects'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040209
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_accept_redirects
- name: XCCDF Value sysctl_net_ipv4_conf_default_accept_redirects_value # promote to variable
  set_fact:
    sysctl_net_ipv4_conf_default_accept_redirects_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv4.conf.default.accept_redirects is set
  sysctl:
    name: net.ipv4.conf.default.accept_redirects
    value: '{{ sysctl_net_ipv4_conf_default_accept_redirects_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040209
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_accept_redirects

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.conf.default.accept_redirects from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.accept_redirects.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.conf.default.accept_redirects" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv4_conf_default_accept_redirects_value='0'


#
# Set runtime for net.ipv4.conf.default.accept_redirects
#
/sbin/sysctl -q -n -w net.ipv4.conf.default.accept_redirects="$sysctl_net_ipv4_conf_default_accept_redirects_value"

#
# If net.ipv4.conf.default.accept_redirects present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv4.conf.default.accept_redirects = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.accept_redirects")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_accept_redirects_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.accept_redirects\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces by Default   [ref]

To set the runtime status of the net.ipv4.conf.default.accept_source_route kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.conf.default.accept_source_route=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.conf.default.accept_source_route = 0
Rationale:
Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures.
Accepting source-routed packets in the IPv4 protocol has few legitimate uses. It should be disabled unless it is absolutely required, such as when IPv4 forwarding is enabled and the system is legitimately functioning as a router.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_accept_source_route
Identifiers and References

References:  BP28(R22), 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9, 5.10.1.1, APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, 3.1.20, CCI-000366, CCI-001551, 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, 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.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-7(a), CM-7(b), SC-5, SC-7(a), DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-040249, SV-248881r858663_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.conf.default.accept_source_route.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040249
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_accept_source_route

- name: Comment out any occurrences of net.ipv4.conf.default.accept_source_route from
    config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.conf.default.accept_source_route
    replace: '#net.ipv4.conf.default.accept_source_route'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040249
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_accept_source_route
- name: XCCDF Value sysctl_net_ipv4_conf_default_accept_source_route_value # promote to variable
  set_fact:
    sysctl_net_ipv4_conf_default_accept_source_route_value: !!str 0
  tags:
    - always

- name: Ensure sysctl net.ipv4.conf.default.accept_source_route is set
  sysctl:
    name: net.ipv4.conf.default.accept_source_route
    value: '{{ sysctl_net_ipv4_conf_default_accept_source_route_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040249
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_accept_source_route

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.conf.default.accept_source_route from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.accept_source_route.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.conf.default.accept_source_route" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv4_conf_default_accept_source_route_value='0'


#
# Set runtime for net.ipv4.conf.default.accept_source_route
#
/sbin/sysctl -q -n -w net.ipv4.conf.default.accept_source_route="$sysctl_net_ipv4_conf_default_accept_source_route_value"

#
# If net.ipv4.conf.default.accept_source_route present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv4.conf.default.accept_source_route = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.accept_source_route")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_accept_source_route_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.accept_source_route\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces   [ref]

To set the runtime status of the net.ipv4.icmp_echo_ignore_broadcasts kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.icmp_echo_ignore_broadcasts = 1
Rationale:
Responding to broadcast (ICMP) echoes facilitates network mapping and provides a vector for amplification attacks.
Ignoring ICMP echo requests (pings) sent to broadcast or multicast addresses makes the system slightly more difficult to enumerate on the network.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_icmp_echo_ignore_broadcasts
Identifiers and References

References:  1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9, 5.10.1.1, APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, 3.1.20, CCI-000366, 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, 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.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-7(a), CM-7(b), SC-5, DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4, Req-1.4.3, SRG-OS-000480-GPOS-00227, OL08-00-040230, SV-248878r858657_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.icmp_echo_ignore_broadcasts.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040230
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_icmp_echo_ignore_broadcasts

- name: Comment out any occurrences of net.ipv4.icmp_echo_ignore_broadcasts from config
    files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.icmp_echo_ignore_broadcasts
    replace: '#net.ipv4.icmp_echo_ignore_broadcasts'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040230
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_icmp_echo_ignore_broadcasts
- name: XCCDF Value sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value # promote to variable
  set_fact:
    sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value: !!str 1
  tags:
    - always

- name: Ensure sysctl net.ipv4.icmp_echo_ignore_broadcasts is set
  sysctl:
    name: net.ipv4.icmp_echo_ignore_broadcasts
    value: '{{ sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040230
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - PCI-DSS-Req-1.4.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_icmp_echo_ignore_broadcasts

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.icmp_echo_ignore_broadcasts from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.icmp_echo_ignore_broadcasts.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.icmp_echo_ignore_broadcasts" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value='1'


#
# Set runtime for net.ipv4.icmp_echo_ignore_broadcasts
#
/sbin/sysctl -q -n -w net.ipv4.icmp_echo_ignore_broadcasts="$sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value"

#
# If net.ipv4.icmp_echo_ignore_broadcasts present in /etc/sysctl.conf, change value to appropriate value
#	else, add "net.ipv4.icmp_echo_ignore_broadcasts = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.icmp_echo_ignore_broadcasts")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.icmp_echo_ignore_broadcasts\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.icmp_echo_ignore_broadcasts\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Network Parameters for Hosts Only   Group contains 3 rules
[ref]   If the system is not going to be used as a router, then setting certain kernel parameters ensure that the host will not perform routing of network traffic.

Rule   Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces   [ref]

To set the runtime status of the net.ipv4.conf.all.send_redirects kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.conf.all.send_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.conf.all.send_redirects = 0
Rationale:
ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages contain information from the system's route table possibly revealing portions of the network topology.
The ability to send ICMP redirects is only appropriate for systems acting as routers.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_send_redirects
Identifiers and References

References:  BP28(R22), 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9, 5.10.1.1, APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, 3.1.20, CCI-000366, 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, 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.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-7(a), CM-7(b), SC-5, CM-6(a), SC-7(a), DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4, 1.4.2, SRG-OS-000480-GPOS-00227, OL08-00-040220, SV-248877r858655_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.conf.all.send_redirects.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040220
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - PCI-DSSv4-1.4.2
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_send_redirects

- name: Comment out any occurrences of net.ipv4.conf.all.send_redirects from config
    files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.conf.all.send_redirects
    replace: '#net.ipv4.conf.all.send_redirects'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040220
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - PCI-DSSv4-1.4.2
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_send_redirects

- name: Ensure sysctl net.ipv4.conf.all.send_redirects is set to 0
  sysctl:
    name: net.ipv4.conf.all.send_redirects
    value: '0'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040220
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - PCI-DSSv4-1.4.2
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_all_send_redirects

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.conf.all.send_redirects from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.send_redirects.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.conf.all.send_redirects" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for net.ipv4.conf.all.send_redirects
#
/sbin/sysctl -q -n -w net.ipv4.conf.all.send_redirects="0"

#
# If net.ipv4.conf.all.send_redirects present in /etc/sysctl.conf, change value to "0"
#	else, add "net.ipv4.conf.all.send_redirects = 0" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.send_redirects")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "0"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.send_redirects\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.send_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces by Default   [ref]

To set the runtime status of the net.ipv4.conf.default.send_redirects kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.conf.default.send_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.conf.default.send_redirects = 0
Rationale:
ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages contain information from the system's route table possibly revealing portions of the network topology.
The ability to send ICMP redirects is only appropriate for systems acting as routers.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_send_redirects
Identifiers and References

References:  BP28(R22), 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9, 5.10.1.1, APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, 3.1.20, CCI-000366, 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, 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.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-7(a), CM-7(b), SC-5, CM-6(a), SC-7(a), DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-040270, SV-248886r858675_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.conf.default.send_redirects.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040270
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_send_redirects

- name: Comment out any occurrences of net.ipv4.conf.default.send_redirects from config
    files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.conf.default.send_redirects
    replace: '#net.ipv4.conf.default.send_redirects'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040270
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_send_redirects

- name: Ensure sysctl net.ipv4.conf.default.send_redirects is set to 0
  sysctl:
    name: net.ipv4.conf.default.send_redirects
    value: '0'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.1
  - DISA-STIG-OL08-00-040270
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_conf_default_send_redirects

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.conf.default.send_redirects from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.send_redirects.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.conf.default.send_redirects" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for net.ipv4.conf.default.send_redirects
#
/sbin/sysctl -q -n -w net.ipv4.conf.default.send_redirects="0"

#
# If net.ipv4.conf.default.send_redirects present in /etc/sysctl.conf, change value to "0"
#	else, add "net.ipv4.conf.default.send_redirects = 0" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.send_redirects")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "0"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.send_redirects\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.send_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces   [ref]

To set the runtime status of the net.ipv4.ip_forward kernel parameter, run the following command:
$ sudo sysctl -w net.ipv4.ip_forward=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.ipv4.ip_forward = 0
Warning:  Certain technologies such as virtual machines, containers, etc. rely on IPv4 forwarding to enable and use networking. Disabling IPv4 forwarding would cause those technologies to stop working. Therefore, this rule should not be used in profiles or benchmarks that target usage of IPv4 forwarding.
Rationale:
Routing protocol daemons are typically used on routers to exchange network topology information with other routers. If this capability is used when not required, system network information may be unnecessarily transmitted across the network.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_ip_forward
Identifiers and References

References:  BP28(R22), 1, 11, 12, 13, 14, 15, 16, 2, 3, 7, 8, 9, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS05.02, DSS05.05, DSS05.07, DSS06.06, 3.1.20, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.9.1.2, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-7(a), CM-7(b), SC-5, CM-6(a), SC-7(a), DE.CM-1, PR.DS-4, PR.IP-1, PR.PT-3, PR.PT-4, Req-1.3.1, Req-1.3.2, 1.4.2, SRG-OS-000480-GPOS-00227, OL08-00-040259, SV-252662r858667_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.ipv4.ip_forward.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040259
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.3.1
  - PCI-DSS-Req-1.3.2
  - PCI-DSSv4-1.4.2
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_ip_forward

- name: Comment out any occurrences of net.ipv4.ip_forward from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.ipv4.ip_forward
    replace: '#net.ipv4.ip_forward'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040259
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.3.1
  - PCI-DSS-Req-1.3.2
  - PCI-DSSv4-1.4.2
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_ip_forward

- name: Ensure sysctl net.ipv4.ip_forward is set to 0
  sysctl:
    name: net.ipv4.ip_forward
    value: '0'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040259
  - NIST-800-171-3.1.20
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-SC-5
  - NIST-800-53-SC-7(a)
  - PCI-DSS-Req-1.3.1
  - PCI-DSS-Req-1.3.2
  - PCI-DSSv4-1.4.2
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_ipv4_ip_forward

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.ipv4.ip_forward from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.ip_forward.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.ipv4.ip_forward" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for net.ipv4.ip_forward
#
/sbin/sysctl -q -n -w net.ipv4.ip_forward="0"

#
# If net.ipv4.ip_forward present in /etc/sysctl.conf, change value to "0"
#	else, add "net.ipv4.ip_forward = 0" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.ip_forward")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "0"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.ip_forward\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.ip_forward\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Uncommon Network Protocols   Group contains 5 rules
[ref]   The system includes support for several network protocols which are not commonly used. Although security vulnerabilities in kernel networking code are not frequently discovered, the consequences can be dramatic. Ensuring uncommon network protocols are disabled reduces the system's risk to attacks targeted at its implementation of those protocols.
Warning:  Although these protocols are not commonly used, avoid disruption in your network environment by ensuring they are not needed prior to disabling them.

Rule   Disable ATM Support   [ref]

The Asynchronous Transfer Mode (ATM) is a protocol operating on network, data link, and physical layers, based on virtual circuits and virtual paths. To configure the system to prevent the atm kernel module from being loaded, add the following line to the file /etc/modprobe.d/atm.conf:
install atm /bin/true
To configure the system to prevent the atm from being used, add the following line to file /etc/modprobe.d/atm.conf:
blacklist atm
Rationale:
Disabling ATM protects the system against exploitation of any flaws in its implementation.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_atm_disabled
Identifiers and References

References:  CCI-000381, CCI-000366, AC-18, FMT_SMF_EXT.1, SRG-OS-000095-GPOS-00049, SRG-OS-000480-GPOS-00227, OL08-00-040021, SV-248829r780053_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'atm' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/atm.conf
    regexp: install\s+atm
    line: install atm /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040021
  - NIST-800-53-AC-18
  - disable_strategy
  - kernel_module_atm_disabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required

- name: Ensure kernel module 'atm' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/atm.conf
    regexp: ^blacklist atm$
    line: blacklist atm
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040021
  - NIST-800-53-AC-18
  - disable_strategy
  - kernel_module_atm_disabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install atm" /etc/modprobe.d/atm.conf ; then
	
	sed -i 's#^install atm.*#install atm /bin/true#g' /etc/modprobe.d/atm.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/atm.conf
	echo "install atm /bin/true" >> /etc/modprobe.d/atm.conf
fi

if ! LC_ALL=C grep -q -m 1 "^blacklist atm$" /etc/modprobe.d/atm.conf ; then
	echo "blacklist atm" >> /etc/modprobe.d/atm.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable CAN Support   [ref]

The Controller Area Network (CAN) is a serial communications protocol which was initially developed for automotive and is now also used in marine, industrial, and medical applications. To configure the system to prevent the can kernel module from being loaded, add the following line to the file /etc/modprobe.d/can.conf:
install can /bin/true
To configure the system to prevent the can from being used, add the following line to file /etc/modprobe.d/can.conf:
blacklist can
Rationale:
Disabling CAN protects the system against exploitation of any flaws in its implementation.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_can_disabled
Identifiers and References

References:  CCI-000381, CCI-000366, AC-18, FMT_SMF_EXT.1, SRG-OS-000095-GPOS-00049, SRG-OS-000480-GPOS-00227, OL08-00-040022, SV-248830r780056_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'can' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/can.conf
    regexp: install\s+can
    line: install can /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040022
  - NIST-800-53-AC-18
  - disable_strategy
  - kernel_module_can_disabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required

- name: Ensure kernel module 'can' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/can.conf
    regexp: ^blacklist can$
    line: blacklist can
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040022
  - NIST-800-53-AC-18
  - disable_strategy
  - kernel_module_can_disabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install can" /etc/modprobe.d/can.conf ; then
	
	sed -i 's#^install can.*#install can /bin/true#g' /etc/modprobe.d/can.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/can.conf
	echo "install can /bin/true" >> /etc/modprobe.d/can.conf
fi

if ! LC_ALL=C grep -q -m 1 "^blacklist can$" /etc/modprobe.d/can.conf ; then
	echo "blacklist can" >> /etc/modprobe.d/can.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable IEEE 1394 (FireWire) Support   [ref]

The IEEE 1394 (FireWire) is a serial bus standard for high-speed real-time communication. To configure the system to prevent the firewire-core kernel module from being loaded, add the following line to the file /etc/modprobe.d/firewire-core.conf:
install firewire-core /bin/true
To configure the system to prevent the firewire-core from being used, add the following line to file /etc/modprobe.d/firewire-core.conf:
blacklist firewire-core
Rationale:
Disabling FireWire protects the system against exploitation of any flaws in its implementation.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_firewire-core_disabled
Identifiers and References

References:  CCI-000381, AC-18, FMT_SMF_EXT.1, SRG-OS-000095-GPOS-00049, OL08-00-040026, SV-248834r780068_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'firewire-core' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/firewire-core.conf
    regexp: install\s+firewire-core
    line: install firewire-core /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040026
  - NIST-800-53-AC-18
  - disable_strategy
  - kernel_module_firewire-core_disabled
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required

- name: Ensure kernel module 'firewire-core' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/firewire-core.conf
    regexp: ^blacklist firewire-core$
    line: blacklist firewire-core
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040026
  - NIST-800-53-AC-18
  - disable_strategy
  - kernel_module_firewire-core_disabled
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install firewire-core" /etc/modprobe.d/firewire-core.conf ; then
	
	sed -i 's#^install firewire-core.*#install firewire-core /bin/true#g' /etc/modprobe.d/firewire-core.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/firewire-core.conf
	echo "install firewire-core /bin/true" >> /etc/modprobe.d/firewire-core.conf
fi

if ! LC_ALL=C grep -q -m 1 "^blacklist firewire-core$" /etc/modprobe.d/firewire-core.conf ; then
	echo "blacklist firewire-core" >> /etc/modprobe.d/firewire-core.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable SCTP Support   [ref]

The Stream Control Transmission Protocol (SCTP) is a transport layer protocol, designed to support the idea of message-oriented communication, with several streams of messages within one connection. To configure the system to prevent the sctp kernel module from being loaded, add the following line to the file /etc/modprobe.d/sctp.conf:
install sctp /bin/true
To configure the system to prevent the sctp from being used, add the following line to file /etc/modprobe.d/sctp.conf:
blacklist sctp
Rationale:
Disabling SCTP protects the system against exploitation of any flaws in its implementation.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_sctp_disabled
Identifiers and References

References:  11, 14, 3, 9, 5.10.1, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, 3.4.6, CCI-000381, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, Req-1.4.2, 1.4.2, SRG-OS-000095-GPOS-00049, SRG-OS-000480-GPOS-00227, OL08-00-040023, SV-248831r780059_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'sctp' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/sctp.conf
    regexp: install\s+sctp
    line: install sctp /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1
  - DISA-STIG-OL08-00-040023
  - NIST-800-171-3.4.6
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-1.4.2
  - PCI-DSSv4-1.4.2
  - disable_strategy
  - kernel_module_sctp_disabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required

- name: Ensure kernel module 'sctp' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/sctp.conf
    regexp: ^blacklist sctp$
    line: blacklist sctp
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1
  - DISA-STIG-OL08-00-040023
  - NIST-800-171-3.4.6
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-1.4.2
  - PCI-DSSv4-1.4.2
  - disable_strategy
  - kernel_module_sctp_disabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install sctp" /etc/modprobe.d/sctp.conf ; then
	
	sed -i 's#^install sctp.*#install sctp /bin/true#g' /etc/modprobe.d/sctp.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/sctp.conf
	echo "install sctp /bin/true" >> /etc/modprobe.d/sctp.conf
fi

if ! LC_ALL=C grep -q -m 1 "^blacklist sctp$" /etc/modprobe.d/sctp.conf ; then
	echo "blacklist sctp" >> /etc/modprobe.d/sctp.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable TIPC Support   [ref]

The Transparent Inter-Process Communication (TIPC) protocol is designed to provide communications between nodes in a cluster. To configure the system to prevent the tipc kernel module from being loaded, add the following line to the file /etc/modprobe.d/tipc.conf:
install tipc /bin/true
To configure the system to prevent the tipc from being used, add the following line to file /etc/modprobe.d/tipc.conf:
blacklist tipc
Warning:  This configuration baseline was created to deploy the base operating system for general purpose workloads. When the operating system is configured for certain purposes, such as a node in High Performance Computing cluster, it is expected that the tipc kernel module will be loaded.
Rationale:
Disabling TIPC protects the system against exploitation of any flaws in its implementation.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_tipc_disabled
Identifiers and References

References:  11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000381, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, FMT_SMF_EXT.1, SRG-OS-000095-GPOS-00049, OL08-00-040024, SV-248832r818694_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'tipc' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/tipc.conf
    regexp: install\s+tipc
    line: install tipc /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040024
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - kernel_module_tipc_disabled
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required

- name: Ensure kernel module 'tipc' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/tipc.conf
    regexp: ^blacklist tipc$
    line: blacklist tipc
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040024
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - kernel_module_tipc_disabled
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install tipc" /etc/modprobe.d/tipc.conf ; then
	
	sed -i 's#^install tipc.*#install tipc /bin/true#g' /etc/modprobe.d/tipc.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/tipc.conf
	echo "install tipc /bin/true" >> /etc/modprobe.d/tipc.conf
fi

if ! LC_ALL=C grep -q -m 1 "^blacklist tipc$" /etc/modprobe.d/tipc.conf ; then
	echo "blacklist tipc" >> /etc/modprobe.d/tipc.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Wireless Networking   Group contains 1 group and 2 rules
[ref]   Wireless networking, such as 802.11 (WiFi) and Bluetooth, can present a security risk to sensitive or classified systems and networks. Wireless networking hardware is much more likely to be included in laptop or portable systems than in desktops or servers.

Removal of hardware provides the greatest assurance that the wireless capability remains disabled. Acquisition policies often include provisions to prevent the purchase of equipment that will be used in sensitive spaces and includes wireless capabilities. If it is impractical to remove the wireless hardware, and policy permits the device to enter sensitive spaces as long as wireless is disabled, efforts should instead focus on disabling wireless capability via software.
Group   Disable Wireless Through Software Configuration   Group contains 2 rules
[ref]   If it is impossible to remove the wireless hardware from the device in question, disable as much of it as possible through software. The following methods can disable software support for wireless networking, but note that these methods do not prevent malicious software or careless users from re-activating the devices.

Rule   Disable Bluetooth Kernel Module   [ref]

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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_bluetooth_disabled
Identifiers and References

References:  11, 12, 14, 15, 3, 8, 9, 5.13.1.3, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, 3.1.16, CCI-000085, CCI-001443, CCI-001444, CCI-001551, CCI-002418, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, AC-18(a), AC-18(3), CM-7(a), CM-7(b), CM-6(a), MP-7, PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000095-GPOS-00049, SRG-OS-000300-GPOS-00118, OL08-00-040111, SV-248843r860921_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'bluetooth' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/bluetooth.conf
    regexp: install\s+bluetooth
    line: install bluetooth /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.13.1.3
  - DISA-STIG-OL08-00-040111
  - 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

- name: Ensure kernel module 'bluetooth' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/bluetooth.conf
    regexp: ^blacklist bluetooth$
    line: blacklist bluetooth
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.13.1.3
  - DISA-STIG-OL08-00-040111
  - 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

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install 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

if ! LC_ALL=C grep -q -m 1 "^blacklist bluetooth$" /etc/modprobe.d/bluetooth.conf ; then
	echo "blacklist bluetooth" >> /etc/modprobe.d/bluetooth.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Deactivate Wireless Network Interfaces   [ref]

Deactivating wireless network interfaces should prevent normal usage of the wireless capability.

Configure the system to disable all wireless network interfaces with the following command:
$ sudo nmcli radio all off
Rationale:
The use of wireless networking can introduce many different attack vectors into the organization's network. Common attack vectors such as malicious association and ad hoc networks will allow an attacker to spoof a wireless access point (AP), allowing validated systems to connect to the malicious AP and enabling the attacker to monitor and record network traffic. These malicious APs can also serve to create a man-in-the-middle attack or be used to create a denial of service to valid network resources.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_wireless_disable_interfaces
Identifiers and References

References:  11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, 3.1.16, CCI-000085, CCI-002418, CCI-002421, CCI-001443, CCI-001444, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, 1315, 1319, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, AC-18(a), AC-18(3), CM-7(a), CM-7(b), CM-6(a), MP-7, PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, Req-1.3.3, 1.4.3, SRG-OS-000299-GPOS-00117, SRG-OS-000300-GPOS-00118, SRG-OS-000424-GPOS-00188, SRG-OS-000481-GPOS-000481, OL08-00-040110, SV-248842r853849_rule


Complexity:low
Disruption:medium
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-040110
  - NIST-800-171-3.1.16
  - NIST-800-53-AC-18(3)
  - NIST-800-53-AC-18(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - PCI-DSS-Req-1.3.3
  - PCI-DSSv4-1.4.3
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy
  - wireless_disable_interfaces

- name: Ensure NetworkManager is installed
  ansible.builtin.package:
    name: '{{ item }}'
    state: present
  with_items:
  - NetworkManager
  tags:
  - DISA-STIG-OL08-00-040110
  - NIST-800-171-3.1.16
  - NIST-800-53-AC-18(3)
  - NIST-800-53-AC-18(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - PCI-DSS-Req-1.3.3
  - PCI-DSSv4-1.4.3
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy
  - wireless_disable_interfaces

- name: Deactivate Wireless Network Interfaces
  command: nmcli radio wifi off
  when: '''NetworkManager'' in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-040110
  - NIST-800-171-3.1.16
  - NIST-800-53-AC-18(3)
  - NIST-800-53-AC-18(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - PCI-DSS-Req-1.3.3
  - PCI-DSSv4-1.4.3
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy
  - wireless_disable_interfaces


if ! rpm -q --quiet "NetworkManager" ; then
    yum install -y "NetworkManager"
fi

nmcli radio all off

Rule   Configure Multiple DNS Servers in /etc/resolv.conf   [ref]

Determine whether the system is using local or DNS name resolution with the following command:
$ sudo grep hosts /etc/nsswitch.conf
hosts: files dns
If the DNS entry is missing from the host's line in the "/etc/nsswitch.conf" file, the "/etc/resolv.conf" file must be empty. Verify the "/etc/resolv.conf" file is empty with the following command:
$ sudo ls -al /etc/resolv.conf
-rw-r--r-- 1 root root 0 Aug 19 08:31 resolv.conf
If the DNS entry is found on the host's line of the "/etc/nsswitch.conf" file, then verify the following:
Multiple Domain Name System (DNS) Servers should be configured in /etc/resolv.conf. This provides redundant name resolution services in the event that a domain server crashes. To configure the system to contain as least 2 DNS servers, add a corresponding nameserver ip_address entry in /etc/resolv.conf for each DNS server where ip_address is the IP address of a valid DNS server. For example:
search example.com
nameserver 192.168.0.1
nameserver 192.168.0.2
Rationale:
To provide availability for name resolution services, multiple redundant name servers are mandated. A failure in name resolution could lead to the failure of security functions requiring name resolution, which may include time synchronization, centralized authentication, and remote system logging.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_network_configure_name_resolution
Identifiers and References

References:  12, 15, 8, APO13.01, DSS05.02, CCI-000366, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.13.1.1, A.13.2.1, A.14.1.3, SC-20(a), CM-6(a), PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-010680, SV-248634r779468_rule

Rule   Ensure System is Not Acting as a Network Sniffer   [ref]

The system should not be acting as a network sniffer, which can capture all traffic on the network to which it is connected. Run the following to determine if any interface is running in promiscuous mode:
$ ip link | grep PROMISC
Promiscuous mode of an interface can be disabled with the following command:
$ sudo ip link set dev device_name multicast off promisc off
Rationale:
Network interfaces in promiscuous mode allow for the capture of all network traffic visible to the system. If unauthorized individuals can access these applications, it may allow them to collect information such as logon IDs, passwords, and key exchanges between systems.

If the system is being used to perform a network troubleshooting function, the use of these tools must be documented with the Information Systems Security Manager (ISSM) and restricted to only authorized personnel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_network_sniffer_disabled
Identifiers and References

References:  1, 11, 14, 3, 9, APO11.06, APO12.06, BAI03.10, BAI09.01, BAI09.02, BAI09.03, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.05, DSS04.05, DSS05.02, DSS05.05, DSS06.06, CCI-000366, 4.2.3.4, 4.3.3.3.7, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, SR 7.8, A.11.1.2, A.11.2.4, A.11.2.5, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.16.1.6, A.8.1.1, A.8.1.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), CM-7(2), MA-3, DE.DP-5, ID.AM-1, PR.IP-1, PR.MA-1, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-040330, SV-248899r780263_rule

Group   File Permissions and Masks   Group contains 10 groups and 68 rules
[ref]   Traditional Unix security relies heavily on file and directory permissions to prevent unauthorized users from reading or modifying files to which they should not have access.

Several of the commands in this section search filesystems for files or directories with certain characteristics, and are intended to be run on every local partition on a given system. When the variable PART appears in one of the commands below, it means that the command is intended to be run repeatedly, with the name of each local partition substituted for PART in turn.

The following command prints a list of all xfs partitions on the local system, which is the default filesystem for Oracle Linux 8 installations:
$ mount -t xfs | awk '{print $3}'
For any systems that use a different local filesystem type, modify this command as appropriate.
Group   Verify Permissions on Important Files and Directories   Group contains 2 groups and 24 rules
[ref]   Permissions for many files on a system must be set restrictively to ensure sensitive information is properly protected. This section discusses important permission restrictions which can be verified to ensure that no harmful discrepancies have arisen.
Group   Verify Permissions on Files within /var/log Directory   Group contains 6 rules
[ref]   The /var/log directory contains files with logs of error messages in the system and should only be accessed by authorized personnel.

Rule   Verify Group Who Owns /var/log Directory   [ref]

To properly set the group owner of /var/log, run the command:
$ sudo chgrp root /var/log
Rationale:
The /var/log directory contains files with logs of error messages in the system and should only be accessed by authorized personnel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupowner_var_log
Identifiers and References

References:  CCI-001314, SRG-OS-000206-GPOS-00084, OL08-00-010260, SV-248559r779243_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Ensure group owner on /var/log/
  file:
    path: /var/log/
    state: directory
    group: '0'
  tags:
  - DISA-STIG-OL08-00-010260
  - configure_strategy
  - file_groupowner_var_log
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
find -H /var/log/ -maxdepth 1 -type d -exec chgrp 0 {} \;

Rule   Verify Group Who Owns /var/log/messages File   [ref]

To properly set the group owner of /var/log/messages, run the command:
$ sudo chgrp root /var/log/messages
Rationale:
The /var/log/messages file contains logs of error messages in the system and should only be accessed by authorized personnel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupowner_var_log_messages
Identifiers and References

References:  CCI-001314, SRG-OS-000206-GPOS-00084, OL08-00-010230, SV-248556r779234_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /var/log/messages
  stat:
    path: /var/log/messages
  register: file_exists
  tags:
  - DISA-STIG-OL08-00-010230
  - configure_strategy
  - file_groupowner_var_log_messages
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /var/log/messages
  file:
    path: /var/log/messages
    group: '0'
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-010230
  - configure_strategy
  - file_groupowner_var_log_messages
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
chgrp 0 /var/log/messages

Rule   Verify User Who Owns /var/log Directory   [ref]

To properly set the owner of /var/log, run the command:
$ sudo chown root /var/log 
Rationale:
The /var/log directory contains files with logs of error messages in the system and should only be accessed by authorized personnel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_owner_var_log
Identifiers and References

References:  CCI-001314, SRG-OS-000206-GPOS-00084, OL08-00-010250, SV-248558r779240_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Ensure owner on directory /var/log/
  file:
    path: /var/log/
    state: directory
    owner: '0'
  tags:
  - DISA-STIG-OL08-00-010250
  - configure_strategy
  - file_owner_var_log
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
find -H /var/log/ -maxdepth 1 -type d -exec chown 0 {} \;

Rule   Verify User Who Owns /var/log/messages File   [ref]

To properly set the owner of /var/log/messages, run the command:
$ sudo chown root /var/log/messages 
Rationale:
The /var/log/messages file contains logs of error messages in the system and should only be accessed by authorized personnel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_owner_var_log_messages
Identifiers and References

References:  CCI-001314, SRG-OS-000206-GPOS-00084, OL08-00-010220, SV-248555r779231_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /var/log/messages
  stat:
    path: /var/log/messages
  register: file_exists
  tags:
  - DISA-STIG-OL08-00-010220
  - configure_strategy
  - file_owner_var_log_messages
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /var/log/messages
  file:
    path: /var/log/messages
    owner: '0'
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-010220
  - configure_strategy
  - file_owner_var_log_messages
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
chown 0 /var/log/messages

Rule   Verify Permissions on /var/log Directory   [ref]

To properly set the permissions of /var/log, run the command:
$ sudo chmod 0755 /var/log
Rationale:
The /var/log directory contains files with logs of error messages in the system and should only be accessed by authorized personnel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_var_log
Identifiers and References

References:  CCI-001314, SRG-OS-000206-GPOS-00084, OL08-00-010240, SV-248557r779237_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Set permissions for /var/log/
  file:
    path: /var/log/
    state: directory
    mode: u-s,g-ws,o-wt
  tags:
  - DISA-STIG-OL08-00-010240
  - configure_strategy
  - file_permissions_var_log
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure





find -H /var/log/ -maxdepth 1 -perm /u+s,g+ws,o+wt -type d -exec chmod u-s,g-ws,o-wt {} \;

Rule   Verify Permissions on /var/log/messages File   [ref]

To properly set the permissions of /var/log/messages, run the command:
$ sudo chmod 0640 /var/log/messages
Rationale:
The /var/log/messages file contains logs of error messages in the system and should only be accessed by authorized personnel.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_var_log_messages
Identifiers and References

References:  CCI-001314, SRG-OS-000206-GPOS-00084, OL08-00-010210, SV-248554r779228_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /var/log/messages
  stat:
    path: /var/log/messages
  register: file_exists
  tags:
  - DISA-STIG-OL08-00-010210
  - configure_strategy
  - file_permissions_var_log_messages
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-xs,g-xws,o-xwrt on /var/log/messages
  file:
    path: /var/log/messages
    mode: u-xs,g-xws,o-xwrt
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-010210
  - configure_strategy
  - file_permissions_var_log_messages
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure





chmod u-xs,g-xws,o-xwrt /var/log/messages
Group   Verify File Permissions Within Some Important Directories   Group contains 9 rules
[ref]   Some directories contain files whose confidentiality or integrity is notably important and may also be susceptible to misconfiguration over time, particularly if unpackaged software is installed. As such, an argument exists to verify that files' permissions within these directories remain configured correctly and restrictively.

Rule   Verify that Shared Library Directories Have Root Group Ownership   [ref]

System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default:
/lib
/lib64
/usr/lib
/usr/lib64
Kernel modules, which can be added to the kernel during runtime, are also stored in /lib/modules. All files in these directories should be group-owned by the root user. If the directories, is found to be owned by a user other than root correct its ownership with the following command:
$ sudo chgrp root DIR
Rationale:
Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Proper ownership of library directories is necessary to protect the integrity of the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dir_group_ownership_library_dirs
Identifiers and References

References:  CCI-001499, CM-5(6), CM-5(6).1, SRG-OS-000259-GPOS-00100, OL08-00-010351, SV-252653r818755_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Ensure group owner on /lib/ recursively
  file:
    path: /lib/
    state: directory
    recurse: true
    group: '0'
  tags:
  - DISA-STIG-OL08-00-010351
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_group_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner on /lib64/ recursively
  file:
    path: /lib64/
    state: directory
    recurse: true
    group: '0'
  tags:
  - DISA-STIG-OL08-00-010351
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_group_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner on /usr/lib/ recursively
  file:
    path: /usr/lib/
    state: directory
    recurse: true
    group: '0'
  tags:
  - DISA-STIG-OL08-00-010351
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_group_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner on /usr/lib64/ recursively
  file:
    path: /usr/lib64/
    state: directory
    recurse: true
    group: '0'
  tags:
  - DISA-STIG-OL08-00-010351
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_group_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
find -H /lib/  -type d -exec chgrp 0 {} \;
find -H /lib64/  -type d -exec chgrp 0 {} \;
find -H /usr/lib/  -type d -exec chgrp 0 {} \;
find -H /usr/lib64/  -type d -exec chgrp 0 {} \;

Rule   Verify that Shared Library Directories Have Root Ownership   [ref]

System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default:
/lib
/lib64
/usr/lib
/usr/lib64
Kernel modules, which can be added to the kernel during runtime, are also stored in /lib/modules. All files in these directories should be owned by the root user. If the directories, is found to be owned by a user other than root correct its ownership with the following command:
$ sudo chown root DIR
Rationale:
Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Proper ownership of library directories is necessary to protect the integrity of the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dir_ownership_library_dirs
Identifiers and References

References:  CCI-001499, CM-5(6), CM-5(6).1, SRG-OS-000259-GPOS-00100, OL08-00-010341, SV-252652r818752_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Ensure owner on directory /lib/ recursively
  file:
    path: /lib/
    state: directory
    recurse: true
    owner: '0'
  tags:
  - DISA-STIG-OL08-00-010341
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner on directory /lib64/ recursively
  file:
    path: /lib64/
    state: directory
    recurse: true
    owner: '0'
  tags:
  - DISA-STIG-OL08-00-010341
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner on directory /usr/lib/ recursively
  file:
    path: /usr/lib/
    state: directory
    recurse: true
    owner: '0'
  tags:
  - DISA-STIG-OL08-00-010341
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner on directory /usr/lib64/ recursively
  file:
    path: /usr/lib64/
    state: directory
    recurse: true
    owner: '0'
  tags:
  - DISA-STIG-OL08-00-010341
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
find -H /lib/  -type d -exec chown 0 {} \;
find -H /lib64/  -type d -exec chown 0 {} \;
find -H /usr/lib/  -type d -exec chown 0 {} \;
find -H /usr/lib64/  -type d -exec chown 0 {} \;

Rule   Verify that Shared Library Directories Have Restrictive Permissions   [ref]

System-wide shared library directories, which contain are linked to executables during process load time or run time, are stored in the following directories by default:
/lib
/lib64
/usr/lib
/usr/lib64
Kernel modules, which can be added to the kernel during runtime, are stored in /lib/modules. All sub-directories in these directories should not be group-writable or world-writable. If any file in these directories is found to be group-writable or world-writable, correct its permission with the following command:
$ sudo chmod go-w DIR
Rationale:
If the operating system were to allow any user to make changes to software libraries, then those changes might be implemented without undergoing the appropriate testing and approvals that are part of a robust change management process. This requirement applies to operating systems with software libraries that are accessible and configurable, as in the case of interpreted languages. Software libraries also include privileged programs which execute with escalated privileges. Only qualified and authorized individuals must be allowed to obtain access to information system components for purposes of initiating changes, including upgrades and modifications.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dir_permissions_library_dirs
Identifiers and References

References:  CCI-001499, CIP-003-8 R6, CM-5, CM-5(6), CM-5(6).1, SRG-OS-000259-GPOS-00100, OL08-00-010331, SV-252651r818749_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Set permissions for /lib/ recursively
  file:
    path: /lib/
    state: directory
    recurse: true
    mode: g-w,o-w
  tags:
  - DISA-STIG-OL08-00-010331
  - NIST-800-53-CM-5
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /lib64/ recursively
  file:
    path: /lib64/
    state: directory
    recurse: true
    mode: g-w,o-w
  tags:
  - DISA-STIG-OL08-00-010331
  - NIST-800-53-CM-5
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /usr/lib/ recursively
  file:
    path: /usr/lib/
    state: directory
    recurse: true
    mode: g-w,o-w
  tags:
  - DISA-STIG-OL08-00-010331
  - NIST-800-53-CM-5
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /usr/lib64/ recursively
  file:
    path: /usr/lib64/
    state: directory
    recurse: true
    mode: g-w,o-w
  tags:
  - DISA-STIG-OL08-00-010331
  - NIST-800-53-CM-5
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - dir_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure





find -H /lib/  -perm /g+w,o+w -type d -exec chmod g-w,o-w {} \;

find -H /lib64/  -perm /g+w,o+w -type d -exec chmod g-w,o-w {} \;

find -H /usr/lib/  -perm /g+w,o+w -type d -exec chmod g-w,o-w {} \;

find -H /usr/lib64/  -perm /g+w,o+w -type d -exec chmod g-w,o-w {} \;

Rule   Verify that system commands files are group owned by root or a system account   [ref]

System commands files are stored in the following directories by default:
/bin
/sbin
/usr/bin
/usr/sbin
/usr/local/bin
/usr/local/sbin
All files in these directories should be owned by the root group, or a system account. If the directory, or any file in these directories, is found to be owned by a group other than root or a a system account correct its ownership with the following command:
$ sudo chgrp root FILE
Rationale:
If the operating system allows any user to make changes to software libraries, then those changes might be implemented without undergoing the appropriate testing and approvals that are part of a robust change management process. This requirement applies to operating systems with software libraries that are accessible and configurable, as in the case of interpreted languages. Software libraries also include privileged programs which execute with escalated privileges. Only qualified and authorized individuals must be allowed to obtain access to information system components for purposes of initiating changes, including upgrades and modifications.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupownership_system_commands_dirs
Identifiers and References

References:  CCI-001499, CM-5(6), CM-5(6).1, SRG-OS-000259-GPOS-00100, OL08-00-010320, SV-248569r818625_rule


Complexity:medium
Disruption:medium
Strategy:restrict
- name: Retrieve the system command files and set their group ownership to root
  command: find -L {{ item }}  ! -group root -type f -exec chgrp root '{}' \;
  with_items:
  - /bin
  - /sbin
  - /usr/bin
  - /usr/sbin
  - /usr/local/bin
  - /usr/local/sbin
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010320
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - file_groupownership_system_commands_dirs
  - medium_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy


for SYSCMDFILES in /bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin
do
   find -L $SYSCMDFILES \! -group root -type f -exec chgrp root '{}' \;
done

Rule   Verify that System Executables Have Root Ownership   [ref]

System executables are stored in the following directories by default:
/bin
/sbin
/usr/bin
/usr/libexec
/usr/local/bin
/usr/local/sbin
/usr/sbin
All files in these directories should be owned by the root user. If any file FILE in these directories is found to be owned by a user other than root, correct its ownership with the following command:
$ sudo chown root FILE
Rationale:
System binaries are executed by privileged users as well as system services, and restrictive permissions are necessary to ensure that their execution of these programs cannot be co-opted.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_ownership_binary_dirs
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001499, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-5(6), CM-5(6).1, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000259-GPOS-00100, OL08-00-010310, SV-248568r779270_rule


Complexity:medium
Disruption:medium
Strategy:restrict
- name: Read list of system executables without root ownership
  command: find /bin/ /usr/bin/ /usr/local/bin/ /sbin/ /usr/sbin/ /usr/local/sbin/
    /usr/libexec \! -user root
  register: no_root_system_executables
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010310
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - file_ownership_binary_dirs
  - medium_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set ownership to root of system executables
  file:
    path: '{{ item }}'
    owner: root
  with_items: '{{ no_root_system_executables.stdout_lines }}'
  when: no_root_system_executables.stdout_lines | length > 0
  tags:
  - DISA-STIG-OL08-00-010310
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - file_ownership_binary_dirs
  - medium_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

find /bin/ \
/usr/bin/ \
/usr/local/bin/ \
/sbin/ \
/usr/sbin/ \
/usr/local/sbin/ \
/usr/libexec \
\! -user root -execdir chown root {} \;

Rule   Verify that Shared Library Files Have Root Ownership   [ref]

System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default:
/lib
/lib64
/usr/lib
/usr/lib64
Kernel modules, which can be added to the kernel during runtime, are also stored in /lib/modules. All files in these directories should be owned by the root user. If the directory, or any file in these directories, is found to be owned by a user other than root correct its ownership with the following command:
$ sudo chown root FILE
Rationale:
Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Proper ownership is necessary to protect the integrity of the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_ownership_library_dirs
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001499, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-5(6), CM-5(6).1, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000259-GPOS-00100, OL08-00-010340, SV-248571r779279_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Find /lib/ file(s) matching ^.*$ recursively
  command: find -H /lib/  -type f ! -uid 0 -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010340
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner on /lib/ file(s) matching ^.*$
  file:
    path: '{{ item }}'
    owner: '0'
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010340
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Find /lib64/ file(s) matching ^.*$ recursively
  command: find -H /lib64/  -type f ! -uid 0 -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010340
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner on /lib64/ file(s) matching ^.*$
  file:
    path: '{{ item }}'
    owner: '0'
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010340
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Find /usr/lib/ file(s) matching ^.*$ recursively
  command: find -H /usr/lib/  -type f ! -uid 0 -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010340
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner on /usr/lib/ file(s) matching ^.*$
  file:
    path: '{{ item }}'
    owner: '0'
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010340
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Find /usr/lib64/ file(s) matching ^.*$ recursively
  command: find -H /usr/lib64/  -type f ! -uid 0 -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010340
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner on /usr/lib64/ file(s) matching ^.*$
  file:
    path: '{{ item }}'
    owner: '0'
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010340
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_ownership_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure

find /lib/  -type f ! -uid 0 -regex '^.*$' -exec chown 0 {} \;

find /lib64/  -type f ! -uid 0 -regex '^.*$' -exec chown 0 {} \;

find /usr/lib/  -type f ! -uid 0 -regex '^.*$' -exec chown 0 {} \;

find /usr/lib64/  -type f ! -uid 0 -regex '^.*$' -exec chown 0 {} \;

Rule   Verify that System Executables Have Restrictive Permissions   [ref]

System executables are stored in the following directories by default:
/bin
/sbin
/usr/bin
/usr/libexec
/usr/local/bin
/usr/local/sbin
/usr/sbin
All files in these directories should not be group-writable or world-writable. If any file FILE in these directories is found to be group-writable or world-writable, correct its permission with the following command:
$ sudo chmod go-w FILE
Rationale:
System binaries are executed by privileged users, as well as system services, and restrictive permissions are necessary to ensure execution of these programs cannot be co-opted.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_binary_dirs
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001499, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-5(6), CM-5(6).1, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000259-GPOS-00100, OL08-00-010300, SV-248567r818622_rule


Complexity:medium
Disruption:medium
Strategy:restrict
- name: Read list of world and group writable system executables
  ansible.builtin.command: find /bin /usr/bin /usr/local/bin /sbin /usr/sbin /usr/local/sbin
    /usr/libexec -perm /022 -type f
  register: world_writable_library_files
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010300
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - file_permissions_binary_dirs
  - medium_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Remove world/group writability of system executables
  ansible.builtin.file:
    path: '{{ item }}'
    mode: go-w
    state: file
  with_items: '{{ world_writable_library_files.stdout_lines }}'
  when: world_writable_library_files.stdout_lines | length > 0
  tags:
  - DISA-STIG-OL08-00-010300
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - file_permissions_binary_dirs
  - medium_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

DIRS="/bin /usr/bin /usr/local/bin /sbin /usr/sbin /usr/local/sbin /usr/libexec"
for dirPath in $DIRS; do
	find "$dirPath" -perm /022 -exec chmod go-w '{}' \;
done

Rule   Verify that Shared Library Files Have Restrictive Permissions   [ref]

System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default:
/lib
/lib64
/usr/lib
/usr/lib64
Kernel modules, which can be added to the kernel during runtime, are stored in /lib/modules. All files in these directories should not be group-writable or world-writable. If any file in these directories is found to be group-writable or world-writable, correct its permission with the following command:
$ sudo chmod go-w FILE
Rationale:
Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Restrictive permissions are necessary to protect the integrity of the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_library_dirs
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001499, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-6(a), CM-5(6), CM-5(6).1, AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000259-GPOS-00100, OL08-00-010330, SV-248570r818628_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Find /lib/ file(s) recursively
  command: find -H /lib/  -perm /g+w,o+w  -type f -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010330
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /lib/ file(s)
  file:
    path: '{{ item }}'
    mode: g-w,o-w
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010330
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Find /lib64/ file(s) recursively
  command: find -H /lib64/  -perm /g+w,o+w  -type f -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010330
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /lib64/ file(s)
  file:
    path: '{{ item }}'
    mode: g-w,o-w
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010330
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Find /usr/lib/ file(s) recursively
  command: find -H /usr/lib/  -perm /g+w,o+w  -type f -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010330
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /usr/lib/ file(s)
  file:
    path: '{{ item }}'
    mode: g-w,o-w
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010330
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Find /usr/lib64/ file(s) recursively
  command: find -H /usr/lib64/  -perm /g+w,o+w  -type f -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010330
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /usr/lib64/ file(s)
  file:
    path: '{{ item }}'
    mode: g-w,o-w
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010330
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_library_dirs
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure





find -H /lib/  -perm /g+w,o+w  -type f -regex '^.*$' -exec chmod g-w,o-w {} \;

find -H /lib64/  -perm /g+w,o+w  -type f -regex '^.*$' -exec chmod g-w,o-w {} \;

find -H /usr/lib/  -perm /g+w,o+w  -type f -regex '^.*$' -exec chmod g-w,o-w {} \;

find -H /usr/lib64/  -perm /g+w,o+w  -type f -regex '^.*$' -exec chmod g-w,o-w {} \;

Rule   Verify the system-wide library files in directories "/lib", "/lib64", "/usr/lib/" and "/usr/lib64" are group-owned by root.   [ref]

System-wide library files are stored in the following directories by default:
/lib
/lib64
/usr/lib
/usr/lib64
All system-wide shared library files should be protected from unauthorised access. If any of these files is not group-owned by root, correct its group-owner with the following command:
$ sudo chgrp root FILE
Rationale:
If the operating system were to allow any user to make changes to software libraries, then those changes might be implemented without undergoing the appropriate testing and approvals that are part of a robust change management process. This requirement applies to operating systems with software libraries that are accessible and configurable, as in the case of interpreted languages. Software libraries also include privileged programs which execute with escalated privileges. Only qualified and authorized individuals must be allowed to obtain access to information system components for purposes of initiating changes, including upgrades and modifications.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_root_permissions_syslibrary_files
Identifiers and References

References:  CCI-001499, CM-5(6), CM-5(6).1, SRG-OS-000259-GPOS-00100, OL08-00-010350, SV-248572r779282_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Find /lib/ file(s) matching ^.*$ recursively
  command: find -H /lib/  -type f ! -gid 0 -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010350
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - root_permissions_syslibrary_files

- name: Ensure group owner on /lib/ file(s) matching ^.*$
  file:
    path: '{{ item }}'
    group: '0'
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010350
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - root_permissions_syslibrary_files

- name: Find /lib64/ file(s) matching ^.*$ recursively
  command: find -H /lib64/  -type f ! -gid 0 -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010350
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - root_permissions_syslibrary_files

- name: Ensure group owner on /lib64/ file(s) matching ^.*$
  file:
    path: '{{ item }}'
    group: '0'
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010350
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - root_permissions_syslibrary_files

- name: Find /usr/lib/ file(s) matching ^.*$ recursively
  command: find -H /usr/lib/  -type f ! -gid 0 -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010350
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - root_permissions_syslibrary_files

- name: Ensure group owner on /usr/lib/ file(s) matching ^.*$
  file:
    path: '{{ item }}'
    group: '0'
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010350
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - root_permissions_syslibrary_files

- name: Find /usr/lib64/ file(s) matching ^.*$ recursively
  command: find -H /usr/lib64/  -type f ! -gid 0 -regex "^.*$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-010350
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - root_permissions_syslibrary_files

- name: Ensure group owner on /usr/lib64/ file(s) matching ^.*$
  file:
    path: '{{ item }}'
    group: '0'
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010350
  - NIST-800-53-CM-5(6)
  - NIST-800-53-CM-5(6).1
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - root_permissions_syslibrary_files

Complexity:low
Disruption:low
Strategy:configure

find /lib/  -type f ! -gid 0 -regex '^.*$' -exec chgrp 0 {} \;

find /lib64/  -type f ! -gid 0 -regex '^.*$' -exec chgrp 0 {} \;

find /usr/lib/  -type f ! -gid 0 -regex '^.*$' -exec chgrp 0 {} \;

find /usr/lib64/  -type f ! -gid 0 -regex '^.*$' -exec chgrp 0 {} \;

Rule   Verify that All World-Writable Directories Have Sticky Bits Set   [ref]

When the so-called 'sticky bit' is set on a directory, only the owner of a given file may remove that file from the directory. Without the sticky bit, any user with write access to a directory may remove any file in the directory. Setting the sticky bit prevents users from removing each other's files. In cases where there is no reason for a directory to be world-writable, a better solution is to remove that permission rather than to set the sticky bit. However, if a directory is used by a particular application, consult that application's documentation instead of blindly changing modes.
To set the sticky bit on a world-writable directory DIR, run the following command:
$ sudo chmod +t DIR
Rationale:
Failing to set the sticky bit on public directories allows unauthorized users to delete files in the directory structure.

The only authorized public directories are those temporary directories supplied with the system, or those designed to be temporary file repositories. The setting is normally reserved for directories used by the system, by users for temporary file storage (such as /tmp), and for directories requiring global read/write access.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dir_perms_world_writable_sticky_bits
Identifiers and References

References:  BP28(R40), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001090, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000138-GPOS-00069, OL08-00-010190, SV-248551r779219_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Get all world-writable directories with no sticky bits set
  shell: |
    set -o pipefail
    df --local -P | awk '{if (NR!=1) print $6}' | xargs -I '{}' find '{}' -xdev -type d \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null
  register: dir_output
  tags:
  - DISA-STIG-OL08-00-010190
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - dir_perms_world_writable_sticky_bits
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure sticky bit is set
  file:
    path: '{{ item }}'
    mode: a+t
  with_items:
  - '{{ dir_output.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010190
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - dir_perms_world_writable_sticky_bits
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

df --local -P | awk '{if (NR!=1) print $6}' \
| xargs -I '$6' find '$6' -xdev -type d \
\( -perm -0002 -a ! -perm -1000 \) 2>/dev/null \
-exec chmod a+t {} +

Rule   Ensure All World-Writable Directories Are Owned by a System Account   [ref]

All directories in local partitions which are world-writable should be owned by root or another system account. If any world-writable directories are not owned by a system account, this should be investigated. Following this, the files should be deleted or assigned to an appropriate owner.
Rationale:
Allowing a user account to own a world-writable directory is undesirable because it allows the owner of that directory to remove or replace any files that may be placed in the directory by other users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dir_perms_world_writable_system_owned
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-000366, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000480-GPOS-00227, OL08-00-010700, SV-248636r779474_rule

Rule   Ensure All World-Writable Directories Are Group Owned by a System Account   [ref]

All directories in local partitions which are world-writable should be group owned by root or another system account. If any world-writable directories are not group owned by a system account, this should be investigated. Following this, the files should be deleted or assigned to an appropriate group.
Rationale:
Allowing a user account to group own a world-writable directory is undesirable because it allows the owner of that directory to remove or replace any files that may be placed in the directory by other users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dir_perms_world_writable_system_owned_group
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-000366, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000480-GPOS-00227, OL08-00-010710, SV-248637r779477_rule

Rule   Verify Permissions on /etc/audit/auditd.conf   [ref]

To properly set the permissions of /etc/audit/auditd.conf, run the command:
$ sudo chmod 0640 /etc/audit/auditd.conf
Rationale:
Without the capability to restrict the roles and individuals that can select which events are audited, unauthorized personnel may be able to prevent the auditing of critical events. Misconfigured audits may degrade the system's performance by overwhelming the audit log. Misconfigured audits may also make it more difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_etc_audit_auditd
Identifiers and References

References:  CCI-000171, AU-12(b), SRG-OS-000063-GPOS-00032, OL08-00-030610, SV-248806r779984_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Test for existence /etc/audit/auditd.conf
  stat:
    path: /etc/audit/auditd.conf
  register: file_exists
  tags:
  - DISA-STIG-OL08-00-030610
  - NIST-800-53-AU-12(b)
  - configure_strategy
  - file_permissions_etc_audit_auditd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-xs,g-xws,o-xwrt on /etc/audit/auditd.conf
  file:
    path: /etc/audit/auditd.conf
    mode: u-xs,g-xws,o-xwrt
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - DISA-STIG-OL08-00-030610
  - NIST-800-53-AU-12(b)
  - configure_strategy
  - file_permissions_etc_audit_auditd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure





chmod u-xs,g-xws,o-xwrt /etc/audit/auditd.conf

Rule   Verify Permissions on /etc/audit/rules.d/*.rules   [ref]

To properly set the permissions of /etc/audit/rules.d/*.rules, run the command:
$ sudo chmod 0640 /etc/audit/rules.d/*.rules
Rationale:
Without the capability to restrict the roles and individuals that can select which events are audited, unauthorized personnel may be able to prevent the auditing of critical events. Misconfigured audits may degrade the system's performance by overwhelming the audit log. Misconfigured audits may also make it more difficult to establish, correlate, and investigate the events relating to an incident or identify those responsible for one.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_etc_audit_rulesd
Identifiers and References

References:  CCI-000171, AU-12(b), SRG-OS-000063-GPOS-00032, OL08-00-030610, SV-248806r779984_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Find /etc/audit/rules.d/ file(s)
  command: find -H /etc/audit/rules.d/ -maxdepth 1 -perm /u+xs,g+xws,o+xwrt  -type
    f -regex "^.*rules$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - DISA-STIG-OL08-00-030610
  - NIST-800-53-AU-12(b)
  - configure_strategy
  - file_permissions_etc_audit_rulesd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /etc/audit/rules.d/ file(s)
  file:
    path: '{{ item }}'
    mode: u-xs,g-xws,o-xwrt
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-030610
  - NIST-800-53-AU-12(b)
  - configure_strategy
  - file_permissions_etc_audit_rulesd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure





find -H /etc/audit/rules.d/ -maxdepth 1 -perm /u+xs,g+xws,o+xwrt  -type f -regex '^.*rules$' -exec chmod u-xs,g-xws,o-xwrt {} \;

Rule   Ensure All Files Are Owned by a Group   [ref]

If any files are not owned by a group, then the cause of their lack of group-ownership should be investigated. Following this, the files should be deleted or assigned to an appropriate group. The following command will discover and print any files on local partitions which do not belong to a valid group:
$ df --local -P | awk '{if (NR!=1) print $6}' | sudo xargs -I '{}' find '{}' -xdev -nogroup
To search all filesystems on a system including network mounted filesystems the following command can be run manually for each partition:
$ sudo find PARTITION -xdev -nogroup
Warning:  This rule only considers local groups. If you have your groups defined outside /etc/group, the rule won't consider those.
Rationale:
Unowned files do not directly imply a security problem, but they are generally a sign that something is amiss. They may be caused by an intruder, by incorrect software installation or draft software removal, or by failure to remove all files belonging to a deleted account. The files should be repaired so they will not cause problems when accounts are created in the future, and the cause should be discovered and addressed.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_ungroupowned
Identifiers and References

References:  BP28(R55), 1, 11, 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.02, DSS06.03, DSS06.06, DSS06.10, CCI-000366, CCI-002165, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.18.1.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.DS-5, PR.PT-3, 2.2.6, SRG-OS-000480-GPOS-00227, OL08-00-010790, SV-248647r779507_rule

Rule   Ensure All Files Are Owned by a User   [ref]

If any files are not owned by a user, then the cause of their lack of ownership should be investigated. Following this, the files should be deleted or assigned to an appropriate user. The following command will discover and print any files on local partitions which do not belong to a valid user:
$ df --local -P | awk {'if (NR!=1) print $6'} | sudo xargs -I '{}' find '{}' -xdev -nouser
To search all filesystems on a system including network mounted filesystems the following command can be run manually for each partition:
$ sudo find PARTITION -xdev -nouser
Warning:  For this rule to evaluate centralized user accounts, getent must be working properly so that running the command
getent passwd
returns a list of all users in your organization. If using the System Security Services Daemon (SSSD),
enumerate = true
must be configured in your organization's domain to return a complete list of users
Warning:  Enabling this rule will result in slower scan times depending on the size of your organization and number of centralized users.
Rationale:
Unowned files do not directly imply a security problem, but they are generally a sign that something is amiss. They may be caused by an intruder, by incorrect software installation or draft software removal, or by failure to remove all files belonging to a deleted account. The files should be repaired so they will not cause problems when accounts are created in the future, and the cause should be discovered and addressed.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_no_files_unowned_by_user
Identifiers and References

References:  BP28(R55), 11, 12, 13, 14, 15, 16, 18, 3, 5, 9, APO01.06, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, CCI-000366, CCI-002165, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 5.2, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), PR.AC-4, PR.AC-6, PR.DS-5, PR.IP-1, PR.PT-3, 2.2.6, SRG-OS-000480-GPOS-00227, OL08-00-010780, SV-248646r779504_rule

Group   Restrict Dynamic Mounting and Unmounting of Filesystems   Group contains 3 rules
[ref]   Linux includes a number of facilities for the automated addition and removal of filesystems on a running system. These facilities may be necessary in many environments, but this capability also carries some risk -- whether direct risk from allowing users to introduce arbitrary filesystems, or risk that software flaws in the automated mount facility itself could allow an attacker to compromise the system.

This command can be used to list the types of filesystems that are available to the currently executing kernel:
$ find /lib/modules/`uname -r`/kernel/fs -type f -name '*.ko'
If these filesystems are not required then they can be explicitly disabled in a configuratio file in /etc/modprobe.d.

Rule   Disable the Automounter   [ref]

The autofs daemon mounts and unmounts filesystems, such as user home directories shared via NFS, on demand. In addition, autofs can be used to handle removable media, and the default configuration provides the cdrom device as /misc/cd. However, this method of providing access to removable media is not common, so autofs can almost always be disabled if NFS is not in use. Even if NFS is required, it may be possible to configure filesystem mounts statically by editing /etc/fstab rather than relying on the automounter.

The autofs service can be disabled with the following command:
$ sudo systemctl mask --now autofs.service
Rationale:
Disabling the automounter permits the administrator to statically control filesystem mounting through /etc/fstab.

Additionally, automatically mounting filesystems permits easy introduction of unknown devices, thereby facilitating malicious activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_autofs_disabled
Identifiers and References

References:  1, 12, 15, 16, 5, APO13.01, DSS01.04, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.4.6, CCI-000366, CCI-000778, CCI-001958, 164.308(a)(3)(i), 164.308(a)(3)(ii)(A), 164.310(d)(1), 164.310(d)(2), 164.312(a)(1), 164.312(a)(2)(iv), 164.312(b), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.6, A.11.2.6, A.13.1.1, A.13.2.1, A.18.1.4, A.6.2.1, A.6.2.2, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, CM-7(a), CM-7(b), CM-6(a), MP-7, PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, SRG-OS-000114-GPOS-00059, SRG-OS-000378-GPOS-00163, SRG-OS-000480-GPOS-00227, OL08-00-040070, SV-248836r780074_rule



[customizations.services]
disabled = ["autofs"]

Complexity:low
Disruption:low
Strategy:enable
include disable_autofs

class disable_autofs {
  service {'autofs':
    enable => false,
    ensure => 'stopped',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Block Disable service autofs
  block:

  - name: Disable service autofs
    block:

    - name: Disable service autofs
      systemd:
        name: autofs.service
        enabled: 'no'
        state: stopped
        masked: 'yes'
    rescue:

    - name: Intentionally ignored previous 'Disable service autofs' failure, service
        was already disabled
      meta: noop
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040070
  - NIST-800-171-3.4.6
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_autofs_disabled

- name: Unit Socket Exists - autofs.socket
  command: systemctl list-unit-files autofs.socket
  register: socket_file_exists
  changed_when: false
  failed_when: socket_file_exists.rc not in [0, 1]
  check_mode: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040070
  - NIST-800-171-3.4.6
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_autofs_disabled

- name: Disable socket autofs
  systemd:
    name: autofs.socket
    enabled: 'no'
    state: stopped
    masked: 'yes'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"autofs.socket" in socket_file_exists.stdout_lines[1]'
  tags:
  - DISA-STIG-OL08-00-040070
  - NIST-800-171-3.4.6
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_autofs_disabled

Complexity:low
Disruption:low
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" stop 'autofs.service'
"$SYSTEMCTL_EXEC" disable 'autofs.service'
"$SYSTEMCTL_EXEC" mask 'autofs.service'
# Disable socket activation if we have a unit file for it
if "$SYSTEMCTL_EXEC" -q list-unit-files autofs.socket; then
    "$SYSTEMCTL_EXEC" stop 'autofs.socket'
    "$SYSTEMCTL_EXEC" mask 'autofs.socket'
fi
# The service may not be running because it has been started and failed,
# so let's reset the state so OVAL checks pass.
# Service should be 'inactive', not 'failed' after reboot though.
"$SYSTEMCTL_EXEC" reset-failed 'autofs.service' || true

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Mounting of cramfs   [ref]

To configure the system to prevent the cramfs kernel module from being loaded, add the following line to the file /etc/modprobe.d/cramfs.conf:
install cramfs /bin/true
To configure the system to prevent the cramfs from being used, add the following line to file /etc/modprobe.d/cramfs.conf:
blacklist cramfs
This effectively prevents usage of this uncommon filesystem. The cramfs filesystem type is a compressed read-only Linux filesystem embedded in small footprint systems. A cramfs image can be used without having to first decompress the image.
Rationale:
Removing support for unneeded filesystem types reduces the local attack surface of the server.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_cramfs_disabled
Identifiers and References

References:  11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, 3.4.6, CCI-000381, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, SRG-OS-000095-GPOS-00049, OL08-00-040025, SV-248833r780065_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'cramfs' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/cramfs.conf
    regexp: install\s+cramfs
    line: install cramfs /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040025
  - NIST-800-171-3.4.6
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - kernel_module_cramfs_disabled
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required

- name: Ensure kernel module 'cramfs' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/cramfs.conf
    regexp: ^blacklist cramfs$
    line: blacklist cramfs
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040025
  - NIST-800-171-3.4.6
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - kernel_module_cramfs_disabled
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install cramfs" /etc/modprobe.d/cramfs.conf ; then
	
	sed -i 's#^install cramfs.*#install cramfs /bin/true#g' /etc/modprobe.d/cramfs.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/cramfs.conf
	echo "install cramfs /bin/true" >> /etc/modprobe.d/cramfs.conf
fi

if ! LC_ALL=C grep -q -m 1 "^blacklist cramfs$" /etc/modprobe.d/cramfs.conf ; then
	echo "blacklist cramfs" >> /etc/modprobe.d/cramfs.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Modprobe Loading of USB Storage Driver   [ref]

To prevent USB storage devices from being used, configure the kernel module loading system to prevent automatic loading of the USB storage driver. To configure the system to prevent the usb-storage kernel module from being loaded, add the following line to the file /etc/modprobe.d/usb-storage.conf:
install usb-storage /bin/true
To configure the system to prevent the usb-storage from being used, add the following line to file /etc/modprobe.d/usb-storage.conf:
blacklist usb-storage
This will prevent the modprobe program from loading the usb-storage module, but will not prevent an administrator (or another program) from using the insmod program to load the module manually.
Rationale:
USB storage devices such as thumb drives can be used to introduce malicious software.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_usb-storage_disabled
Identifiers and References

References:  1, 12, 15, 16, 5, APO13.01, DSS01.04, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.1.21, CCI-000366, CCI-000778, CCI-001958, 164.308(a)(3)(i), 164.308(a)(3)(ii)(A), 164.310(d)(1), 164.310(d)(2), 164.312(a)(1), 164.312(a)(2)(iv), 164.312(b), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.6, A.11.2.6, A.13.1.1, A.13.2.1, A.18.1.4, A.6.2.1, A.6.2.2, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, CM-7(a), CM-7(b), CM-6(a), MP-7, PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, SRG-OS-000114-GPOS-00059, SRG-OS-000378-GPOS-00163, SRG-OS-000480-GPOS-00227, OL08-00-040080, SV-248837r818697_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'usb-storage' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/usb-storage.conf
    regexp: install\s+usb-storage
    line: install usb-storage /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040080
  - 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

- name: Ensure kernel module 'usb-storage' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/usb-storage.conf
    regexp: ^blacklist usb-storage$
    line: blacklist usb-storage
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040080
  - 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

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install usb-storage" /etc/modprobe.d/usb-storage.conf ; then
	
	sed -i 's#^install usb-storage.*#install usb-storage /bin/true#g' /etc/modprobe.d/usb-storage.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/usb-storage.conf
	echo "install usb-storage /bin/true" >> /etc/modprobe.d/usb-storage.conf
fi

if ! LC_ALL=C grep -q -m 1 "^blacklist usb-storage$" /etc/modprobe.d/usb-storage.conf ; then
	echo "blacklist usb-storage" >> /etc/modprobe.d/usb-storage.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Restrict Partition Mount Options   Group contains 23 rules
[ref]   System partitions can be mounted with certain options that limit what files on those partitions can do. These options are set in the /etc/fstab configuration file, and can be used to make certain types of malicious behavior more difficult.

Rule   Add nosuid Option to /boot/efi   [ref]

The nosuid mount option can be used to prevent execution of setuid programs in /boot/efi. The SUID and SGID permissions should not be required on the boot partition. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /boot/efi.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from boot partitions.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_boot_efi_nosuid
Identifiers and References

References:  CCI-000366, CM-6(b), CM-6.1(iv), SRG-OS-000480-GPOS-00227, OL08-00-010572, SV-248618r818648_rule


Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nosuid Option to /boot/efi: Check information associated to mountpoint'
  command: findmnt --fstab '/boot/efi'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and "/boot/efi" in ansible_mounts | map(attribute="mount") | list
    )
  tags:
  - DISA-STIG-OL08-00-010572
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_efi_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /boot/efi: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/boot/efi" in ansible_mounts | map(attribute="mount") | list )
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-010572
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_efi_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /boot/efi: If /boot/efi not mounted, craft mount_info
    manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /boot/efi
    - ''
    - ''
    - defaults
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/boot/efi" in ansible_mounts | map(attribute="mount") | list )
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-010572
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_efi_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /boot/efi: Make sure nosuid option is part of the to
    /boot/efi options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid''
      }) }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/boot/efi" in ansible_mounts | map(attribute="mount") | list )
  - mount_info is defined and "nosuid" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-010572
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_efi_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /boot/efi: Ensure /boot/efi is mounted with nosuid option'
  mount:
    path: /boot/efi
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/boot/efi" in ansible_mounts | map(attribute="mount") | list )
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-010572
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-6.1(iv)
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_efi_nosuid
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && [ -d /sys/firmware/efi ] ); then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/boot/efi")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/boot/efi' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /boot/efi in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /boot/efi)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /boot/efi  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi


    if mkdir -p "/boot/efi"; then
        if mountpoint -q "/boot/efi"; then
            mount -o remount --target "/boot/efi"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nosuid Option to /boot   [ref]

The nosuid mount option can be used to prevent execution of setuid programs in /boot. The SUID and SGID permissions should not be required on the boot partition. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /boot.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from boot partitions.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_boot_nosuid
Identifiers and References

References:  BP28(R12), CCI-000366, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, SRG-OS-000480-GPOS-00227, OL08-00-010571, SV-248617r779417_rule


Complexity:low
Disruption:high
Strategy:enable

part /boot --mountoptions="nosuid"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nosuid Option to /boot: Check information associated to mountpoint'
  command: findmnt --fstab '/boot'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010571
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /boot: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-010571
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /boot: If /boot not mounted, craft mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /boot
    - ''
    - ''
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-010571
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /boot: Make sure nosuid option is part of the to /boot
    options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "nosuid" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-010571
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /boot: Ensure /boot is mounted with nosuid option'
  mount:
    path: /boot
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-010571
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_boot_nosuid
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/boot")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/boot' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /boot in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /boot)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /boot  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi


    if mkdir -p "/boot"; then
        if mountpoint -q "/boot"; then
            mount -o remount --target "/boot"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nodev Option to /dev/shm   [ref]

The nodev mount option can be used to prevent creation of device files in /dev/shm. Legitimate character and block devices should not exist within temporary directories like /dev/shm. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /dev/shm.
Rationale:
The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_dev_shm_nodev
Identifiers and References

References:  11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-001764, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040120, SV-248844r853853_rule


Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nodev Option to /dev/shm: Check information associated to mountpoint'
  command: findmnt  '/dev/shm'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040120
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /dev/shm: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040120
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /dev/shm: If /dev/shm not mounted, craft mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /dev/shm
    - tmpfs
    - tmpfs
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040120
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /dev/shm: Make sure nodev option is part of the to /dev/shm
    options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "nodev" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040120
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /dev/shm: Ensure /dev/shm is mounted with nodev option'
  mount:
    path: /dev/shm
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("" |
    length == 0)
  tags:
  - DISA-STIG-OL08-00-040120
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nodev
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /dev/shm)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nodev"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
    fi


    if mkdir -p "/dev/shm"; then
        if mountpoint -q "/dev/shm"; then
            mount -o remount --target "/dev/shm"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add noexec Option to /dev/shm   [ref]

The noexec mount option can be used to prevent binaries from being executed out of /dev/shm. It can be dangerous to allow the execution of binaries from world-writable temporary storage directories such as /dev/shm. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /dev/shm.
Rationale:
Allowing users to execute binaries from world-writable directories such as /dev/shm can expose the system to potential compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_dev_shm_noexec
Identifiers and References

References:  11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-001764, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040122, SV-248846r853855_rule


Complexity:low
Disruption:high
Strategy:configure
- name: 'Add noexec Option to /dev/shm: Check information associated to mountpoint'
  command: findmnt  '/dev/shm'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040122
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /dev/shm: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040122
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /dev/shm: If /dev/shm not mounted, craft mount_info
    manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /dev/shm
    - tmpfs
    - tmpfs
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040122
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /dev/shm: Make sure noexec option is part of the to
    /dev/shm options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "noexec" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040122
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /dev/shm: Ensure /dev/shm is mounted with noexec option'
  mount:
    path: /dev/shm
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("" |
    length == 0)
  tags:
  - DISA-STIG-OL08-00-040122
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_noexec
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /dev/shm)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "noexec"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
    fi


    if mkdir -p "/dev/shm"; then
        if mountpoint -q "/dev/shm"; then
            mount -o remount --target "/dev/shm"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nosuid Option to /dev/shm   [ref]

The nosuid mount option can be used to prevent execution of setuid programs in /dev/shm. The SUID and SGID permissions should not be required in these world-writable directories. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /dev/shm.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from temporary storage partitions.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_dev_shm_nosuid
Identifiers and References

References:  11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-001764, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040121, SV-248845r853854_rule


Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nosuid Option to /dev/shm: Check information associated to mountpoint'
  command: findmnt  '/dev/shm'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040121
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /dev/shm: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040121
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /dev/shm: If /dev/shm not mounted, craft mount_info
    manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /dev/shm
    - tmpfs
    - tmpfs
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040121
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /dev/shm: Make sure nosuid option is part of the to
    /dev/shm options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "nosuid" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040121
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /dev/shm: Ensure /dev/shm is mounted with nosuid option'
  mount:
    path: /dev/shm
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("" |
    length == 0)
  tags:
  - DISA-STIG-OL08-00-040121
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_dev_shm_nosuid
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /dev/shm)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi


    if mkdir -p "/dev/shm"; then
        if mountpoint -q "/dev/shm"; then
            mount -o remount --target "/dev/shm"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add noexec Option to /home   [ref]

The noexec mount option can be used to prevent binaries from being executed out of /home. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /home.
Warning:  OVAL looks for partitions whose mount point is a substring of any interactive user's home directory and validates that noexec option is there. Because of this, there could be false negatives when several partitions share a base substring. For example, if there is a home directory in /var/tmp/user1 and there are partitions mounted in /var and /var/tmp. The noexec option is only expected in /var/tmp, but OVAL will check both.
Bash remediation uses the df command to find out the partition where the home directory is mounted. However, if the directory doesn't exist the remediation won't be applied.
Rationale:
The /home directory contains data of individual users. Binaries in this directory should not be considered as trusted and users should not be able to execute them.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_home_noexec
Identifiers and References

References:  BP28(R12), CCI-000366, CM-6(b), SRG-OS-000480-GPOS-00227, OL08-00-010590, SV-248620r779426_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation (){

    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" $1)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " $1  defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "noexec"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
    fi

    if mkdir -p "$1"; then
        if mountpoint -q "$1"; then
            mount -o remount --target "$1"
        fi
    fi
}

readarray -t home_directories  < \
    <(awk -F':' '{if ($3>=1000 && $3!= 65534) print $6}' /etc/passwd )


for home_directory in "${home_directories[@]}"
do
    if [ -d $home_directory ]; then
        fstab_mount_point=$(df $home_directory | awk '/^\/dev/ {print $6}')
        if  ! grep -qP "^/$|^/lib$|^/opt$|^/usr$|^/bin$|^/sbin$|^/boot$|^/dev$|^/proc$" <<< $fstab_mount_point
        then
            perform_remediation "$fstab_mount_point"
        fi
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nosuid Option to /home   [ref]

The nosuid mount option can be used to prevent execution of setuid programs in /home. The SUID and SGID permissions should not be required in these user data directories. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /home.
Warning:  OVAL looks for partitions whose mount point is a substring of any interactive user's home directory and validates that noexec option is there. Because of this, there could be false negatives when several partitions share a base substring. For example, if there is a home directory in /var/tmp/user1 and there are partitions mounted in /var and /var/tmp. The noexec option is only expected in /var/tmp, but OVAL will check both.
Bash remediation uses the df command to find out the partition where the home directory is mounted. However, if the directory doesn't exist the remediation won't be applied.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from user home directory partitions.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_home_nosuid
Identifiers and References

References:  BP28(R12), 11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, SRG-OS-000480-GPOS-00227, OL08-00-010570, SV-248616r779414_rule


# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation (){

    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" $1)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " $1  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi

    if mkdir -p "$1"; then
        if mountpoint -q "$1"; then
            mount -o remount --target "$1"
        fi
    fi
}

readarray -t home_directories  < \
    <(awk -F':' '{if ($3>=1000 && $3!= 65534) print $6}' /etc/passwd )


for home_directory in "${home_directories[@]}"
do
    if [ -d $home_directory ]; then
        fstab_mount_point=$(df $home_directory | awk '/^\/dev/ {print $6}')
        if  ! grep -qP "^/$|^/lib$|^/opt$|^/usr$|^/bin$|^/sbin$|^/boot$|^/dev$|^/proc$" <<< $fstab_mount_point
        then
            perform_remediation "$fstab_mount_point"
        fi
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nodev Option to Non-Root Local Partitions   [ref]

The nodev mount option prevents files from being interpreted as character or block devices. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of any non-root local partitions.
Rationale:
The nodev mount option prevents files from being interpreted as character or block devices. The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails, for which it is not advised to set nodev on these filesystems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_nodev_nonroot_local_partitions
Identifiers and References

References:  BP28(R12), 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-3, SRG-OS-000368-GPOS-00154, SRG-OS-000480-GPOS-00227, OL08-00-010580, SV-248619r779423_rule


Complexity:low
Disruption:high
Strategy:configure
- name: Ensure non-root local partitions are mounted with nodev option
  mount:
    path: '{{ item.mount }}'
    src: '{{ item.device }}'
    opts: '{{ item.options }},nodev'
    state: mounted
    fstype: '{{ item.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - item.mount is match('/\w')
  - item.options is not search('nodev')
  with_items:
  - '{{ ansible_facts.mounts }}'
  tags:
  - DISA-STIG-OL08-00-010580
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_nodev_nonroot_local_partitions
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

MOUNT_OPTION="nodev"
# Create array of local non-root partitions
readarray -t partitions_records < <(findmnt --mtab --raw --evaluate | grep "^/\w" | grep "\s/dev/\w")

# Create array of polyinstantiated directories, in case one of them is found in mtab
readarray -t polyinstantiated_dirs < \
    <(grep -oP "^\s*[^#\s]+\s+\S+" /etc/security/namespace.conf | grep -oP "(?<=\s)\S+?(?=/?\$)")


for partition_record in "${partitions_records[@]}"; do
    # Get all important information for fstab
    mount_point="$(echo ${partition_record} | cut -d " " -f1)"
    device="$(echo ${partition_record} | cut -d " " -f2)"
    device_type="$(echo ${partition_record} | cut -d " " -f3)"
    if ! printf '%s\0' "${polyinstantiated_dirs[@]}" | grep -qxzF "$mount_point"; then
        # device and device_type will be used only in case when the device doesn't have fstab record
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" $mount_point)"

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if ! grep "$mount_point_match_regexp" /etc/fstab; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|$MOUNT_OPTION)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            echo "$device $mount_point $device_type defaults,${previous_mount_opts}$MOUNT_OPTION 0 0" >> /etc/fstab
        # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
        elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "$MOUNT_OPTION"; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,$MOUNT_OPTION|" /etc/fstab
        fi
        if mkdir -p "$mount_point"; then
            if mountpoint -q "$mount_point"; then
                mount -o remount --target "$mount_point"
            fi
        fi
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nodev Option to Removable Media Partitions   [ref]

The nodev mount option prevents files from being interpreted as character or block devices. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of any removable media partitions.
Rationale:
The only legitimate location for device files is the /dev directory located on the root partition. An exception to this is chroot jails, and it is not advised to set nodev on partitions which contain their root filesystems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_nodev_removable_partitions
Identifiers and References

References:  11, 12, 13, 14, 16, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.06, DSS05.07, DSS06.03, DSS06.06, CCI-000366, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.7.1.1, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, A.9.2.1, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.AC-3, PR.AC-6, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-010600, SV-248621r779429_rule


Complexity:low
Disruption:high
Strategy:configure
- name: XCCDF Value var_removable_partition # promote to variable
  set_fact:
    var_removable_partition: !!str (N/A)
  tags:
    - always

- name: Ensure permission nodev are set on var_removable_partition
  lineinfile:
    path: /etc/fstab
    regexp: ^\s*({{ var_removable_partition }})\s+([^\s]*)\s+([^\s]*)\s+([^\s]*)(.*)$
    backrefs: true
    line: \1 \2 \3 \4,nodev \5
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010600
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_nodev_removable_partitions
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_removable_partition='(N/A)'


device_regex="^\s*$var_removable_partition\s\+"
mount_option="nodev"

if grep -q $device_regex /etc/fstab ; then
    previous_opts=$(grep $device_regex /etc/fstab | awk '{print $4}')
    sed -i "s|\($device_regex.*$previous_opts\)|\1,$mount_option|" /etc/fstab
else
    echo "Not remediating, because there is no record of $var_removable_partition in /etc/fstab" >&2
    return 1
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add noexec Option to Removable Media Partitions   [ref]

The noexec mount option prevents the direct execution of binaries on the mounted filesystem. Preventing the direct execution of binaries from removable media (such as a USB key) provides a defense against malicious software that may be present on such untrusted media. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of any removable media partitions.
Rationale:
Allowing users to execute binaries from removable media such as USB keys exposes the system to potential compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_noexec_removable_partitions
Identifiers and References

References:  11, 12, 13, 14, 16, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.06, DSS05.07, DSS06.03, DSS06.06, CCI-000087, CCI-000366, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.7.1.1, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, A.9.2.1, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.AC-3, PR.AC-6, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-010610, SV-248622r779432_rule


Complexity:low
Disruption:high
Strategy:configure
- name: XCCDF Value var_removable_partition # promote to variable
  set_fact:
    var_removable_partition: !!str (N/A)
  tags:
    - always

- name: Ensure permission noexec are set on var_removable_partition
  lineinfile:
    path: /etc/fstab
    regexp: ^\s*({{ var_removable_partition }})\s+([^\s]*)\s+([^\s]*)\s+([^\s]*)(.*)$
    backrefs: true
    line: \1 \2 \3 \4,noexec \5
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010610
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_noexec_removable_partitions
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_removable_partition='(N/A)'


device_regex="^\s*$var_removable_partition\s\+"
mount_option="noexec"

if grep -q $device_regex /etc/fstab ; then
    previous_opts=$(grep $device_regex /etc/fstab | awk '{print $4}')
    sed -i "s|\($device_regex.*$previous_opts\)|\1,$mount_option|" /etc/fstab
else
    echo "Not remediating, because there is no record of $var_removable_partition in /etc/fstab" >&2
    return 1
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nosuid Option to Removable Media Partitions   [ref]

The nosuid mount option prevents set-user-identifier (SUID) and set-group-identifier (SGID) permissions from taking effect. These permissions allow users to execute binaries with the same permissions as the owner and group of the file respectively. Users should not be allowed to introduce SUID and SGID files into the system via partitions mounted from removeable media. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of any removable media partitions.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Allowing users to introduce SUID or SGID binaries from partitions mounted off of removable media would allow them to introduce their own highly-privileged programs.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_nosuid_removable_partitions
Identifiers and References

References:  11, 12, 13, 14, 15, 16, 18, 3, 5, 8, 9, APO01.06, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.06, DSS05.07, DSS06.02, DSS06.03, DSS06.06, CCI-000366, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 5.2, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.11.2.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.AC-3, PR.AC-4, PR.AC-6, PR.DS-5, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-010620, SV-248623r779435_rule


Complexity:low
Disruption:high
Strategy:configure
- name: XCCDF Value var_removable_partition # promote to variable
  set_fact:
    var_removable_partition: !!str (N/A)
  tags:
    - always

- name: Ensure permission nosuid are set on var_removable_partition
  lineinfile:
    path: /etc/fstab
    regexp: ^\s*({{ var_removable_partition }})\s+([^\s]*)\s+([^\s]*)\s+([^\s]*)(.*)$
    backrefs: true
    line: \1 \2 \3 \4,nosuid \5
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010620
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_nosuid_removable_partitions
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_removable_partition='(N/A)'


device_regex="^\s*$var_removable_partition\s\+"
mount_option="nosuid"

if grep -q $device_regex /etc/fstab ; then
    previous_opts=$(grep $device_regex /etc/fstab | awk '{print $4}')
    sed -i "s|\($device_regex.*$previous_opts\)|\1,$mount_option|" /etc/fstab
else
    echo "Not remediating, because there is no record of $var_removable_partition in /etc/fstab" >&2
    return 1
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nodev Option to /tmp   [ref]

The nodev mount option can be used to prevent device files from being created in /tmp. Legitimate character and block devices should not exist within temporary directories like /tmp. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /tmp.
Rationale:
The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_tmp_nodev
Identifiers and References

References:  BP28(R12), 11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-001764, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040123, SV-248847r853856_rule


Complexity:low
Disruption:high
Strategy:enable

part /tmp --mountoptions="nodev"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nodev Option to /tmp: Check information associated to mountpoint'
  command: findmnt --fstab '/tmp'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  tags:
  - DISA-STIG-OL08-00-040123
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /tmp: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040123
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /tmp: If /tmp not mounted, craft mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /tmp
    - ''
    - ''
    - defaults
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040123
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /tmp: Make sure nodev option is part of the to /tmp options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev''
      }) }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - mount_info is defined and "nodev" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040123
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /tmp: Ensure /tmp is mounted with nodev option'
  mount:
    path: /tmp
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040123
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nodev
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && findmnt --kernel "/tmp" > /dev/null || findmnt --fstab "/tmp" > /dev/null ); then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/tmp")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /tmp in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /tmp)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /tmp  defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nodev"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
    fi


    if mkdir -p "/tmp"; then
        if mountpoint -q "/tmp"; then
            mount -o remount --target "/tmp"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add noexec Option to /tmp   [ref]

The noexec mount option can be used to prevent binaries from being executed out of /tmp. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /tmp.
Rationale:
Allowing users to execute binaries from world-writable directories such as /tmp should never be necessary in normal operation and can expose the system to potential compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_tmp_noexec
Identifiers and References

References:  BP28(R12), 11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-001764, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040125, SV-248849r853858_rule


Complexity:low
Disruption:high
Strategy:enable

part /tmp --mountoptions="noexec"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add noexec Option to /tmp: Check information associated to mountpoint'
  command: findmnt --fstab '/tmp'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  tags:
  - DISA-STIG-OL08-00-040125
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /tmp: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040125
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /tmp: If /tmp not mounted, craft mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /tmp
    - ''
    - ''
    - defaults
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040125
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /tmp: Make sure noexec option is part of the to /tmp
    options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec''
      }) }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - mount_info is defined and "noexec" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040125
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /tmp: Ensure /tmp is mounted with noexec option'
  mount:
    path: /tmp
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040125
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_noexec
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && findmnt --kernel "/tmp" > /dev/null || findmnt --fstab "/tmp" > /dev/null ); then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/tmp")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /tmp in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /tmp)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /tmp  defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "noexec"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
    fi


    if mkdir -p "/tmp"; then
        if mountpoint -q "/tmp"; then
            mount -o remount --target "/tmp"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nosuid Option to /tmp   [ref]

The nosuid mount option can be used to prevent execution of setuid programs in /tmp. The SUID and SGID permissions should not be required in these world-writable directories. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /tmp.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from temporary storage partitions.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_tmp_nosuid
Identifiers and References

References:  BP28(R12), 11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-001764, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040124, SV-248848r853857_rule


Complexity:low
Disruption:high
Strategy:enable

part /tmp --mountoptions="nosuid"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nosuid Option to /tmp: Check information associated to mountpoint'
  command: findmnt --fstab '/tmp'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  tags:
  - DISA-STIG-OL08-00-040124
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /tmp: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040124
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /tmp: If /tmp not mounted, craft mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /tmp
    - ''
    - ''
    - defaults
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040124
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /tmp: Make sure nosuid option is part of the to /tmp
    options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid''
      }) }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - mount_info is defined and "nosuid" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040124
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /tmp: Ensure /tmp is mounted with nosuid option'
  mount:
    path: /tmp
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/tmp" in ansible_mounts | map(attribute="mount") | list )
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040124
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_tmp_nosuid
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && findmnt --kernel "/tmp" > /dev/null || findmnt --fstab "/tmp" > /dev/null ); then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/tmp")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /tmp in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /tmp)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /tmp  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi


    if mkdir -p "/tmp"; then
        if mountpoint -q "/tmp"; then
            mount -o remount --target "/tmp"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nodev Option to /var/log/audit   [ref]

The nodev mount option can be used to prevent device files from being created in /var/log/audit. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /var/log/audit.
Rationale:
The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_log_audit_nodev
Identifiers and References

References:  CCI-001764, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040129, SV-248853r853862_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/log/audit --mountoptions="nodev"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nodev Option to /var/log/audit: Check information associated to mountpoint'
  command: findmnt --fstab '/var/log/audit'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040129
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/log/audit: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040129
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/log/audit: If /var/log/audit not mounted, craft
    mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/log/audit
    - ''
    - ''
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040129
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/log/audit: Make sure nodev option is part of the
    to /var/log/audit options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "nodev" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040129
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/log/audit: Ensure /var/log/audit is mounted with
    nodev option'
  mount:
    path: /var/log/audit
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040129
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nodev
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/log/audit")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/log/audit' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/log/audit in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/log/audit)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/log/audit  defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nodev"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
    fi


    if mkdir -p "/var/log/audit"; then
        if mountpoint -q "/var/log/audit"; then
            mount -o remount --target "/var/log/audit"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add noexec Option to /var/log/audit   [ref]

The noexec mount option can be used to prevent binaries from being executed out of /var/log/audit. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /var/log/audit.
Rationale:
Allowing users to execute binaries from directories containing audit log files such as /var/log/audit should never be necessary in normal operation and can expose the system to potential compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_log_audit_noexec
Identifiers and References

References:  CCI-001764, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040131, SV-248855r853864_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/log/audit --mountoptions="noexec"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add noexec Option to /var/log/audit: Check information associated to mountpoint'
  command: findmnt --fstab '/var/log/audit'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040131
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/log/audit: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040131
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/log/audit: If /var/log/audit not mounted, craft
    mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/log/audit
    - ''
    - ''
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040131
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/log/audit: Make sure noexec option is part of the
    to /var/log/audit options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "noexec" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040131
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/log/audit: Ensure /var/log/audit is mounted with
    noexec option'
  mount:
    path: /var/log/audit
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040131
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_noexec
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/log/audit")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/log/audit' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/log/audit in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/log/audit)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/log/audit  defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "noexec"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
    fi


    if mkdir -p "/var/log/audit"; then
        if mountpoint -q "/var/log/audit"; then
            mount -o remount --target "/var/log/audit"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nosuid Option to /var/log/audit   [ref]

The nosuid mount option can be used to prevent execution of setuid programs in /var/log/audit. The SUID and SGID permissions should not be required in directories containing audit log files. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /var/log/audit.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from partitions designated for audit log files.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_log_audit_nosuid
Identifiers and References

References:  CCI-001764, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040130, SV-248854r853863_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/log/audit --mountoptions="nosuid"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nosuid Option to /var/log/audit: Check information associated to mountpoint'
  command: findmnt --fstab '/var/log/audit'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040130
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/log/audit: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040130
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/log/audit: If /var/log/audit not mounted, craft
    mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/log/audit
    - ''
    - ''
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040130
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/log/audit: Make sure nosuid option is part of the
    to /var/log/audit options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "nosuid" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040130
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/log/audit: Ensure /var/log/audit is mounted with
    nosuid option'
  mount:
    path: /var/log/audit
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040130
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_audit_nosuid
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/log/audit")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/log/audit' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/log/audit in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/log/audit)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/log/audit  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi


    if mkdir -p "/var/log/audit"; then
        if mountpoint -q "/var/log/audit"; then
            mount -o remount --target "/var/log/audit"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nodev Option to /var/log   [ref]

The nodev mount option can be used to prevent device files from being created in /var/log. Legitimate character and block devices should exist only in the /dev directory on the root partition or within chroot jails built for system services. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /var/log.
Rationale:
The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_log_nodev
Identifiers and References

References:  CCI-001764, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040126, SV-248850r853859_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/log --mountoptions="nodev"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nodev Option to /var/log: Check information associated to mountpoint'
  command: findmnt --fstab '/var/log'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040126
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/log: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040126
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/log: If /var/log not mounted, craft mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/log
    - ''
    - ''
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040126
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/log: Make sure nodev option is part of the to /var/log
    options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "nodev" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040126
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/log: Ensure /var/log is mounted with nodev option'
  mount:
    path: /var/log
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040126
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nodev
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/log")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/log' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/log in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/log)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/log  defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nodev"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
    fi


    if mkdir -p "/var/log"; then
        if mountpoint -q "/var/log"; then
            mount -o remount --target "/var/log"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add noexec Option to /var/log   [ref]

The noexec mount option can be used to prevent binaries from being executed out of /var/log. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /var/log.
Rationale:
Allowing users to execute binaries from directories containing log files such as /var/log should never be necessary in normal operation and can expose the system to potential compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_log_noexec
Identifiers and References

References:  BP28(R12), CCI-001764, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040128, SV-248852r853861_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/log --mountoptions="noexec"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add noexec Option to /var/log: Check information associated to mountpoint'
  command: findmnt --fstab '/var/log'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040128
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/log: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040128
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/log: If /var/log not mounted, craft mount_info
    manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/log
    - ''
    - ''
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040128
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/log: Make sure noexec option is part of the to
    /var/log options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "noexec" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040128
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/log: Ensure /var/log is mounted with noexec option'
  mount:
    path: /var/log
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040128
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_noexec
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/log")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/log' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/log in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/log)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/log  defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "noexec"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
    fi


    if mkdir -p "/var/log"; then
        if mountpoint -q "/var/log"; then
            mount -o remount --target "/var/log"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nosuid Option to /var/log   [ref]

The nosuid mount option can be used to prevent execution of setuid programs in /var/log. The SUID and SGID permissions should not be required in directories containing log files. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /var/log.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from partitions designated for log files.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_log_nosuid
Identifiers and References

References:  BP28(R12), CCI-001764, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL08-00-040127, SV-248851r853860_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/log --mountoptions="nosuid"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nosuid Option to /var/log: Check information associated to mountpoint'
  command: findmnt --fstab '/var/log'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040127
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/log: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040127
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/log: If /var/log not mounted, craft mount_info
    manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/log
    - ''
    - ''
    - defaults
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040127
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/log: Make sure nosuid option is part of the to
    /var/log options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid''
      }) }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - mount_info is defined and "nosuid" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040127
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/log: Ensure /var/log is mounted with nosuid option'
  mount:
    path: /var/log
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040127
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-MP-7
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_log_nosuid
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/log")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/log' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/log in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/log)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/log  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi


    if mkdir -p "/var/log"; then
        if mountpoint -q "/var/log"; then
            mount -o remount --target "/var/log"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nodev Option to /var/tmp   [ref]

The nodev mount option can be used to prevent device files from being created in /var/tmp. Legitimate character and block devices should not exist within temporary directories like /var/tmp. Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of /var/tmp.
Rationale:
The only legitimate location for device files is the /dev directory located on the root partition. The only exception to this is chroot jails.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_tmp_nodev
Identifiers and References

References:  BP28(R12), CCI-001764, SRG-OS-000368-GPOS-00154, OL08-00-040132, SV-248856r853865_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/tmp --mountoptions="nodev"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nodev Option to /var/tmp: Check information associated to mountpoint'
  command: findmnt --fstab '/var/tmp'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and "/var/tmp" in ansible_mounts | map(attribute="mount") | list
    )
  tags:
  - DISA-STIG-OL08-00-040132
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/tmp: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040132
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/tmp: If /var/tmp not mounted, craft mount_info manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/tmp
    - ''
    - ''
    - defaults
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040132
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/tmp: Make sure nodev option is part of the to /var/tmp
    options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev''
      }) }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - mount_info is defined and "nodev" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040132
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nodev
  - no_reboot_needed

- name: 'Add nodev Option to /var/tmp: Ensure /var/tmp is mounted with nodev option'
  mount:
    path: /var/tmp
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040132
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nodev
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && findmnt --kernel "/var/tmp" > /dev/null || findmnt --fstab "/var/tmp" > /dev/null ); then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/tmp")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/tmp in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/tmp)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/tmp  defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nodev"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
    fi


    if mkdir -p "/var/tmp"; then
        if mountpoint -q "/var/tmp"; then
            mount -o remount --target "/var/tmp"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add noexec Option to /var/tmp   [ref]

The noexec mount option can be used to prevent binaries from being executed out of /var/tmp. Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of /var/tmp.
Rationale:
Allowing users to execute binaries from world-writable directories such as /var/tmp should never be necessary in normal operation and can expose the system to potential compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_tmp_noexec
Identifiers and References

References:  BP28(R12), CCI-001764, SRG-OS-000368-GPOS-00154, OL08-00-040134, SV-248858r853867_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/tmp --mountoptions="noexec"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add noexec Option to /var/tmp: Check information associated to mountpoint'
  command: findmnt --fstab '/var/tmp'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and "/var/tmp" in ansible_mounts | map(attribute="mount") | list
    )
  tags:
  - DISA-STIG-OL08-00-040134
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/tmp: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040134
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/tmp: If /var/tmp not mounted, craft mount_info
    manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/tmp
    - ''
    - ''
    - defaults
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040134
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/tmp: Make sure noexec option is part of the to
    /var/tmp options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec''
      }) }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - mount_info is defined and "noexec" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040134
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_noexec
  - no_reboot_needed

- name: 'Add noexec Option to /var/tmp: Ensure /var/tmp is mounted with noexec option'
  mount:
    path: /var/tmp
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040134
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_noexec
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && findmnt --kernel "/var/tmp" > /dev/null || findmnt --fstab "/var/tmp" > /dev/null ); then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/tmp")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/tmp in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/tmp)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/tmp  defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "noexec"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
    fi


    if mkdir -p "/var/tmp"; then
        if mountpoint -q "/var/tmp"; then
            mount -o remount --target "/var/tmp"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Add nosuid Option to /var/tmp   [ref]

The nosuid mount option can be used to prevent execution of setuid programs in /var/tmp. The SUID and SGID permissions should not be required in these world-writable directories. Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of /var/tmp.
Rationale:
The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from temporary storage partitions.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_var_tmp_nosuid
Identifiers and References

References:  BP28(R12), CCI-001764, SRG-OS-000368-GPOS-00154, OL08-00-040133, SV-248857r853866_rule


Complexity:low
Disruption:high
Strategy:enable

part /var/tmp --mountoptions="nosuid"

Complexity:low
Disruption:high
Strategy:configure
- name: 'Add nosuid Option to /var/tmp: Check information associated to mountpoint'
  command: findmnt --fstab '/var/tmp'
  register: device_name
  failed_when: device_name.rc > 1
  changed_when: false
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and "/var/tmp" in ansible_mounts | map(attribute="mount") | list
    )
  tags:
  - DISA-STIG-OL08-00-040133
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/tmp: Create mount_info dictionary variable'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - '{{ device_name.stdout_lines[0].split() | list | lower }}'
  - '{{ device_name.stdout_lines[1].split() | list }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - device_name.stdout is defined and device_name.stdout_lines is defined
  - (device_name.stdout | length > 0)
  tags:
  - DISA-STIG-OL08-00-040133
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/tmp: If /var/tmp not mounted, craft mount_info
    manually'
  set_fact:
    mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
  with_together:
  - - target
    - source
    - fstype
    - options
  - - /var/tmp
    - ''
    - ''
    - defaults
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - ("--fstab" | length == 0)
  - (device_name.stdout | length == 0)
  tags:
  - DISA-STIG-OL08-00-040133
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/tmp: Make sure nosuid option is part of the to
    /var/tmp options'
  set_fact:
    mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid''
      }) }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - mount_info is defined and "nosuid" not in mount_info.options
  tags:
  - DISA-STIG-OL08-00-040133
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nosuid
  - no_reboot_needed

- name: 'Add nosuid Option to /var/tmp: Ensure /var/tmp is mounted with nosuid option'
  mount:
    path: /var/tmp
    src: '{{ mount_info.source }}'
    opts: '{{ mount_info.options }}'
    state: mounted
    fstype: '{{ mount_info.fstype }}'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and "/var/tmp" in ansible_mounts | map(attribute="mount") | list )
  - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
    | length == 0)
  tags:
  - DISA-STIG-OL08-00-040133
  - configure_strategy
  - high_disruption
  - low_complexity
  - medium_severity
  - mount_option_var_tmp_nosuid
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && findmnt --kernel "/var/tmp" > /dev/null || findmnt --fstab "/var/tmp" > /dev/null ); then

function perform_remediation {
    
        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" "/var/tmp")"

    grep "$mount_point_match_regexp" -q /etc/fstab \
        || { echo "The mount point '/var/tmp' is not even in /etc/fstab, so we can't set up mount options" >&2;
                echo "Not remediating, because there is no record of /var/tmp in /etc/fstab" >&2; return 1; }
    


    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /var/tmp)"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " /var/tmp  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi


    if mkdir -p "/var/tmp"; then
        if mountpoint -q "/var/tmp"; then
            mount -o remount --target "/var/tmp"
        fi
    fi
}

perform_remediation

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Restrict Programs from Dangerous Execution Patterns   Group contains 4 groups and 18 rules
[ref]   The recommendations in this section are designed to ensure that the system's features to protect against potentially dangerous program execution are activated. These protections are applied at the system initialization or kernel level, and defend against certain types of badly-configured or compromised programs.
Group   Disable Core Dumps   Group contains 4 rules
[ref]   A core dump file is the memory image of an executable program when it was terminated by the operating system due to errant behavior. In most cases, only software developers legitimately need to access these files. The core dump files may also contain sensitive information, or unnecessarily occupy large amounts of disk space.

Once a hard limit is set in /etc/security/limits.conf, or to a file within the /etc/security/limits.d/ directory, a user cannot increase that limit within his or her own session. If access to core dumps is required, consider restricting them to only certain users or groups. See the limits.conf man page for more information.

The core dumps of setuid programs are further protected. The sysctl variable fs.suid_dumpable controls whether the kernel allows core dumps from these programs at all. The default value of 0 is recommended.

Rule   Disable acquiring, saving, and processing core dumps   [ref]

The systemd-coredump.socket unit is a socket activation of the systemd-coredump@.service which processes core dumps. By masking the unit, core dump processing is disabled.
Rationale:
A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers trying to debug problems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_systemd-coredump_disabled
Identifiers and References

References:  CCI-000366, SC-7(10), FMT_SMF_EXT.1, SRG-OS-000480-GPOS-00227, OL08-00-010672, SV-248630r779456_rule



[customizations.services]
disabled = ["systemd-coredump"]

Complexity:low
Disruption:low
Strategy:enable
include disable_systemd-coredump

class disable_systemd-coredump {
  service {'systemd-coredump':
    enable => false,
    ensure => 'stopped',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Block Disable service systemd-coredump
  block:

  - name: Disable service systemd-coredump
    block:

    - name: Disable service systemd-coredump
      systemd:
        name: systemd-coredump.service
        enabled: 'no'
        state: stopped
        masked: 'yes'
    rescue:

    - name: Intentionally ignored previous 'Disable service systemd-coredump' failure,
        service was already disabled
      meta: noop
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010672
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_systemd-coredump_disabled

- name: Unit Socket Exists - systemd-coredump.socket
  command: systemctl list-unit-files systemd-coredump.socket
  register: socket_file_exists
  changed_when: false
  failed_when: socket_file_exists.rc not in [0, 1]
  check_mode: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010672
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_systemd-coredump_disabled

- name: Disable socket systemd-coredump
  systemd:
    name: systemd-coredump.socket
    enabled: 'no'
    state: stopped
    masked: 'yes'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"systemd-coredump.socket" in socket_file_exists.stdout_lines[1]'
  tags:
  - DISA-STIG-OL08-00-010672
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_systemd-coredump_disabled

Complexity:low
Disruption:low
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" stop 'systemd-coredump.service'
"$SYSTEMCTL_EXEC" disable 'systemd-coredump.service'
"$SYSTEMCTL_EXEC" mask 'systemd-coredump.service'
# Disable socket activation if we have a unit file for it
if "$SYSTEMCTL_EXEC" -q list-unit-files systemd-coredump.socket; then
    "$SYSTEMCTL_EXEC" stop 'systemd-coredump.socket'
    "$SYSTEMCTL_EXEC" mask 'systemd-coredump.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 'systemd-coredump.service' || true

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable core dump backtraces   [ref]

The ProcessSizeMax option in [Coredump] section of /etc/systemd/coredump.conf specifies the maximum size in bytes of a core which will be processed. Core dumps exceeding this size may be stored, but the backtrace will not be generated.
Warning:  If the /etc/systemd/coredump.conf file does not already contain the [Coredump] section, the value will not be configured correctly.
Rationale:
A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers or system operators trying to debug problems. Enabling core dumps on production systems is not recommended, however there may be overriding operational requirements to enable advanced debuging. Permitting temporary enablement of core dumps during such situations should be reviewed through local needs and policy.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_coredump_disable_backtraces
Identifiers and References

References:  CCI-000366, CM-6, FMT_SMF_EXT.1, Req-3.2, 3.3.1.1, 3.3.1.2, 3.3.1.3, SRG-OS-000480-GPOS-00227, OL08-00-010675, SV-248633r779465_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010675
  - NIST-800-53-CM-6
  - PCI-DSS-Req-3.2
  - PCI-DSSv4-3.3.1.1
  - PCI-DSSv4-3.3.1.2
  - PCI-DSSv4-3.3.1.3
  - coredump_disable_backtraces
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Disable core dump backtraces
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/systemd/coredump.conf
      create: false
      regexp: ^\s*ProcessSizeMax\s*=\s*
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/systemd/coredump.conf
    lineinfile:
      path: /etc/systemd/coredump.conf
      create: false
      regexp: ^\s*ProcessSizeMax\s*=\s*
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/systemd/coredump.conf
    lineinfile:
      path: /etc/systemd/coredump.conf
      create: false
      regexp: ^\s*ProcessSizeMax\s*=\s*
      line: ProcessSizeMax=0
      state: present
  when: '"systemd" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010675
  - NIST-800-53-CM-6
  - PCI-DSS-Req-3.2
  - PCI-DSSv4-3.3.1.1
  - PCI-DSSv4-3.3.1.2
  - PCI-DSSv4-3.3.1.3
  - coredump_disable_backtraces
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q systemd; then

if [ -e "/etc/systemd/coredump.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*ProcessSizeMax\s*=\s*/Id" "/etc/systemd/coredump.conf"
else
    touch "/etc/systemd/coredump.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/systemd/coredump.conf"

cp "/etc/systemd/coredump.conf" "/etc/systemd/coredump.conf.bak"
# Insert at the end of the file
printf '%s\n' "ProcessSizeMax=0" >> "/etc/systemd/coredump.conf"
# Clean up after ourselves.
rm "/etc/systemd/coredump.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable storing core dump   [ref]

The Storage option in [Coredump] section of /etc/systemd/coredump.conf can be set to none to disable storing core dumps permanently.
Warning:  If the /etc/systemd/coredump.conf file does not already contain the [Coredump] section, the value will not be configured correctly.
Rationale:
A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers or system operators trying to debug problems. Enabling core dumps on production systems is not recommended, however there may be overriding operational requirements to enable advanced debuging. Permitting temporary enablement of core dumps during such situations should be reviewed through local needs and policy.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_coredump_disable_storage
Identifiers and References

References:  CCI-000366, CM-6, FMT_SMF_EXT.1, Req-3.2, 3.3.1.1, 3.3.1.2, 3.3.1.3, SRG-OS-000480-GPOS-00227, OL08-00-010674, SV-248632r779462_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010674
  - NIST-800-53-CM-6
  - PCI-DSS-Req-3.2
  - PCI-DSSv4-3.3.1.1
  - PCI-DSSv4-3.3.1.2
  - PCI-DSSv4-3.3.1.3
  - coredump_disable_storage
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Disable storing core dump
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/systemd/coredump.conf
      create: false
      regexp: ^\s*Storage\s*=\s*
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/systemd/coredump.conf
    lineinfile:
      path: /etc/systemd/coredump.conf
      create: false
      regexp: ^\s*Storage\s*=\s*
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/systemd/coredump.conf
    lineinfile:
      path: /etc/systemd/coredump.conf
      create: false
      regexp: ^\s*Storage\s*=\s*
      line: Storage=none
      state: present
  when: '"systemd" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010674
  - NIST-800-53-CM-6
  - PCI-DSS-Req-3.2
  - PCI-DSSv4-3.3.1.1
  - PCI-DSSv4-3.3.1.2
  - PCI-DSSv4-3.3.1.3
  - coredump_disable_storage
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q systemd; then

if [ -e "/etc/systemd/coredump.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*Storage\s*=\s*/Id" "/etc/systemd/coredump.conf"
else
    touch "/etc/systemd/coredump.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/systemd/coredump.conf"

cp "/etc/systemd/coredump.conf" "/etc/systemd/coredump.conf.bak"
# Insert at the end of the file
printf '%s\n' "Storage=none" >> "/etc/systemd/coredump.conf"
# Clean up after ourselves.
rm "/etc/systemd/coredump.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Core Dumps for All Users   [ref]

To disable core dumps for all users, add the following line to /etc/security/limits.conf, or to a file within the /etc/security/limits.d/ directory:
*     hard   core    0
Rationale:
A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers trying to debug problems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_disable_users_coredumps
Identifiers and References

References:  1, 12, 13, 15, 16, 2, 7, 8, APO13.01, BAI04.04, DSS01.03, DSS03.05, DSS05.07, CCI-000366, SR 6.2, SR 7.1, SR 7.2, A.12.1.3, A.17.2.1, CM-6, SC-7(10), DE.CM-1, PR.DS-4, 3.3.1.1, 3.3.1.2, 3.3.1.3, SRG-OS-000480-GPOS-00227, OL08-00-010673, SV-248631r779459_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010673
  - NIST-800-53-CM-6
  - NIST-800-53-SC-7(10)
  - PCI-DSSv4-3.3.1.1
  - PCI-DSSv4-3.3.1.2
  - PCI-DSSv4-3.3.1.3
  - disable_users_coredumps
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Disable core dumps with limits
  lineinfile:
    dest: /etc/security/limits.conf
    regexp: ^[^#].*core
    line: '*        hard       core      0'
    create: true
  when: '"pam" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010673
  - NIST-800-53-CM-6
  - NIST-800-53-SC-7(10)
  - PCI-DSSv4-3.3.1.1
  - PCI-DSSv4-3.3.1.2
  - PCI-DSSv4-3.3.1.3
  - disable_users_coredumps
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

SECURITY_LIMITS_FILE="/etc/security/limits.conf"

if grep -qE '^\s*\*\s+hard\s+core' $SECURITY_LIMITS_FILE; then
        sed -ri 's/(hard\s+core\s+)[[:digit:]]+/\1 0/' $SECURITY_LIMITS_FILE
else
        echo "*     hard   core    0" >> $SECURITY_LIMITS_FILE
fi

if ls /etc/security/limits.d/*.conf > /dev/null; then
        sed -ri '/^\s*\*\s+hard\s+core/d' /etc/security/limits.d/*.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Enable ExecShield   Group contains 2 rules
[ref]   ExecShield describes kernel features that provide protection against exploitation of memory corruption errors such as buffer overflows. These features include random placement of the stack and other memory regions, prevention of execution in memory that should only hold data, and special handling of text buffers. These protections are enabled by default on 32-bit systems and controlled through sysctl variables kernel.exec-shield and kernel.randomize_va_space. On the latest 64-bit systems, kernel.exec-shield cannot be enabled or disabled with sysctl.

Rule   Restrict Exposed Kernel Pointer Addresses Access   [ref]

To set the runtime status of the kernel.kptr_restrict kernel parameter, run the following command:
$ sudo sysctl -w kernel.kptr_restrict=1
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
kernel.kptr_restrict = 1
Rationale:
Exposing kernel pointers (through procfs or seq_printf()) exposes kernel writeable structures which may contain functions pointers. If a write vulnerability occurs in the kernel, allowing write access to any of this structure, the kernel can be compromised. This option disallow any program without the CAP_SYSLOG capability to get the addresses of kernel pointers by replacing them with 0.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_kernel_kptr_restrict
Identifiers and References

References:  BP28(R23), CCI-002824, CCI-000366, CIP-002-5 R1.1, CIP-002-5 R1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 4.1, CIP-004-6 4.2, CIP-004-6 R2.2.3, CIP-004-6 R2.2.4, CIP-004-6 R2.3, CIP-004-6 R4, CIP-005-6 R1, CIP-005-6 R1.1, CIP-005-6 R1.2, CIP-007-3 R3, CIP-007-3 R3.1, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, CIP-007-3 R8.4, CIP-009-6 R.1.1, CIP-009-6 R4, SC-30, SC-30(2), SC-30(5), CM-6(a), SRG-OS-000132-GPOS-00067, SRG-OS-000433-GPOS-00192, SRG-OS-000480-GPOS-00227, OL08-00-040283, SV-248891r858685_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*kernel.kptr_restrict.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040283
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-30
  - NIST-800-53-SC-30(2)
  - NIST-800-53-SC-30(5)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_kptr_restrict

- name: Comment out any occurrences of kernel.kptr_restrict from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*kernel.kptr_restrict
    replace: '#kernel.kptr_restrict'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040283
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-30
  - NIST-800-53-SC-30(2)
  - NIST-800-53-SC-30(5)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_kptr_restrict
- name: XCCDF Value sysctl_kernel_kptr_restrict_value # promote to variable
  set_fact:
    sysctl_kernel_kptr_restrict_value: !!str 1
  tags:
    - always

- name: Ensure sysctl kernel.kptr_restrict is set
  sysctl:
    name: kernel.kptr_restrict
    value: '{{ sysctl_kernel_kptr_restrict_value }}'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040283
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-30
  - NIST-800-53-SC-30(2)
  - NIST-800-53-SC-30(5)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_kptr_restrict

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.kptr_restrict from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*kernel.kptr_restrict.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "kernel.kptr_restrict" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"

sysctl_kernel_kptr_restrict_value='1'


#
# Set runtime for kernel.kptr_restrict
#
/sbin/sysctl -q -n -w kernel.kptr_restrict="$sysctl_kernel_kptr_restrict_value"

#
# If kernel.kptr_restrict present in /etc/sysctl.conf, change value to appropriate value
#	else, add "kernel.kptr_restrict = value" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.kptr_restrict")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_kernel_kptr_restrict_value"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^kernel.kptr_restrict\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^kernel.kptr_restrict\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable Randomized Layout of Virtual Address Space   [ref]

To set the runtime status of the kernel.randomize_va_space kernel parameter, run the following command:
$ sudo sysctl -w kernel.randomize_va_space=2
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
kernel.randomize_va_space = 2
Rationale:
Address space layout randomization (ASLR) makes it more difficult for an attacker to predict the location of attack code they have introduced into a process's address space during an attempt at exploitation. Additionally, ASLR makes it more difficult for an attacker to know the location of existing code in order to re-purpose it using return oriented programming (ROP) techniques.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_kernel_randomize_va_space
Identifiers and References

References:  BP28(R23), 3.1.7, CCI-000366, CCI-002824, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), CIP-002-5 R1.1, CIP-002-5 R1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 4.1, CIP-004-6 4.2, CIP-004-6 R2.2.3, CIP-004-6 R2.2.4, CIP-004-6 R2.3, CIP-004-6 R4, CIP-005-6 R1, CIP-005-6 R1.1, CIP-005-6 R1.2, CIP-007-3 R3, CIP-007-3 R3.1, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, CIP-007-3 R8.4, CIP-009-6 R.1.1, CIP-009-6 R4, SC-30, SC-30(2), CM-6(a), Req-2.2.1, 2.2.3, SRG-OS-000433-GPOS-00193, SRG-OS-000480-GPOS-00227, OL08-00-010430, SV-248594r860918_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*kernel.randomize_va_space.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010430
  - NIST-800-171-3.1.7
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-30
  - NIST-800-53-SC-30(2)
  - PCI-DSS-Req-2.2.1
  - PCI-DSSv4-2.2.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_randomize_va_space

- name: Comment out any occurrences of kernel.randomize_va_space from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*kernel.randomize_va_space
    replace: '#kernel.randomize_va_space'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010430
  - NIST-800-171-3.1.7
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-30
  - NIST-800-53-SC-30(2)
  - PCI-DSS-Req-2.2.1
  - PCI-DSSv4-2.2.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_randomize_va_space

- name: Ensure sysctl kernel.randomize_va_space is set to 2
  sysctl:
    name: kernel.randomize_va_space
    value: '2'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010430
  - NIST-800-171-3.1.7
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-30
  - NIST-800-53-SC-30(2)
  - PCI-DSS-Req-2.2.1
  - PCI-DSSv4-2.2.3
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_randomize_va_space

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.randomize_va_space from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*kernel.randomize_va_space.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "kernel.randomize_va_space" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for kernel.randomize_va_space
#
/sbin/sysctl -q -n -w kernel.randomize_va_space="2"

#
# If kernel.randomize_va_space present in /etc/sysctl.conf, change value to "2"
#	else, add "kernel.randomize_va_space = 2" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.randomize_va_space")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "2"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^kernel.randomize_va_space\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^kernel.randomize_va_space\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Enable Execute Disable (XD) or No Execute (NX) Support on x86 Systems   Group contains 1 rule
[ref]   Recent processors in the x86 family support the ability to prevent code execution on a per memory page basis. Generically and on AMD processors, this ability is called No Execute (NX), while on Intel processors it is called Execute Disable (XD). This ability can help prevent exploitation of buffer overflow vulnerabilities and should be activated whenever possible. Extra steps must be taken to ensure that this protection is enabled, particularly on 32-bit x86 systems. Other processors, such as Itanium and POWER, have included such support since inception and the standard kernel for those platforms supports the feature. This is enabled by default on the latest Oracle Linux, Red Hat and Fedora systems if supported by the hardware.

Rule   Enable NX or XD Support in the BIOS   [ref]

Reboot the system and enter the BIOS or Setup configuration menu. Navigate the BIOS configuration menu and make sure that the option is enabled. The setting may be located under a Security section. Look for Execute Disable (XD) on Intel-based systems and No Execute (NX) on AMD-based systems.
Rationale:
Computers with the ability to prevent this type of code execution frequently put an option in the BIOS that will allow users to turn the feature on or off at will.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_bios_enable_execution_restrictions
Identifiers and References

References:  11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.7, CCI-002824, 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, SC-39, CM-6(a), PR.IP-1, 2.2.1, SRG-OS-000433-GPOS-00192, OL08-00-010420, SV-248589r853770_rule

Group   Memory Poisoning   Group contains 2 rules
[ref]   Memory Poisoning consists of writing a special value to uninitialized or freed memory. Poisoning can be used as a mechanism to prevent leak of information and detection of corrupted memory.

Rule   Enable page allocator poisoning   [ref]

To enable poisoning of free pages, add the argument page_poison=1 to the default GRUB 2 command line for the Linux operating system. To ensure that page_poison=1 is added as a kernel command line argument to newly installed kernels, add page_poison=1 to the default Grub2 command line for Linux operating systems. Modify the line within /etc/default/grub as shown below:
GRUB_CMDLINE_LINUX="... page_poison=1 ..."
Run the following command to update command line for already installed kernels:
# grubby --update-kernel=ALL --args="page_poison=1"
Rationale:
Poisoning writes an arbitrary value to freed pages, so any modification or reference to that page after being freed or before being initialized will be detected and prevented. This prevents many types of use-after-free vulnerabilities at little performance cost. Also prevents leak of data and detection of corrupted memory.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_page_poison_argument
Identifiers and References

References:  BP28(R8), CCI-001084, CM-6(a), SRG-OS-000480-GPOS-00227, SRG-OS-000134-GPOS-00068, OL08-00-010421, SV-248590r779336_rule


[customizations.kernel]
append = "page_poison=1"

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010421
  - NIST-800-53-CM-6(a)
  - grub2_page_poison_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="page_poison=1"
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"grub2-common" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010421
  - NIST-800-53-CM-6(a)
  - grub2_page_poison_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q grub2-common; }; then

grubby --update-kernel=ALL --args=page_poison=1 --env=/boot/grub2/grubenv

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable SLUB/SLAB allocator poisoning   [ref]

To enable poisoning of SLUB/SLAB objects, add the argument slub_debug=P to the default GRUB 2 command line for the Linux operating system. To ensure that slub_debug=P is added as a kernel command line argument to newly installed kernels, add slub_debug=P to the default Grub2 command line for Linux operating systems. Modify the line within /etc/default/grub as shown below:
GRUB_CMDLINE_LINUX="... slub_debug=P ..."
Run the following command to update command line for already installed kernels:
# grubby --update-kernel=ALL --args="slub_debug=P"
Rationale:
Poisoning writes an arbitrary value to freed objects, so any modification or reference to that object after being freed or before being initialized will be detected and prevented. This prevents many types of use-after-free vulnerabilities at little performance cost. Also prevents leak of data and detection of corrupted memory.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_slub_debug_argument
Identifiers and References

References:  BP28(R8), CCI-001084, CM-6(a), SRG-OS-000433-GPOS-00192, SRG-OS-000134-GPOS-00068, OL08-00-010423, SV-248592r779342_rule


[customizations.kernel]
append = "slub_debug=P"

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010423
  - NIST-800-53-CM-6(a)
  - grub2_slub_debug_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy
- name: XCCDF Value var_slub_debug_options # promote to variable
  set_fact:
    var_slub_debug_options: !!str P
  tags:
    - always

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="slub_debug={{ var_slub_debug_options
    }}"
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"grub2-common" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010423
  - NIST-800-53-CM-6(a)
  - grub2_slub_debug_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q grub2-common; }; then

var_slub_debug_options='P'



grubby --update-kernel=ALL --args=slub_debug=$var_slub_debug_options --env=/boot/grub2/grubenv

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable the uvcvideo module   [ref]

If the device contains a camera it should be covered or disabled when not in use.
Rationale:
Failing to disconnect from collaborative computing devices (i.e., cameras) can result in subsequent compromises of organizational information. Providing easy methods to physically disconnect from such devices after a collaborative computing session helps to ensure participants actually carry out the disconnect activity without having to go through complex and tedious procedures.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_module_uvcvideo_disabled
Identifiers and References

References:  CCI-000381, CM-7 (a), CM-7 (5) (b), SRG-OS-000095-GPOS-00049, SRG-OS-000370-GPOS-00155, OL08-00-040020, SV-248828r818691_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: Ensure kernel module 'uvcvideo' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/uvcvideo.conf
    regexp: install\s+uvcvideo
    line: install uvcvideo /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040020
  - NIST-800-53-CM-7 (5) (b)
  - NIST-800-53-CM-7 (a)
  - disable_strategy
  - kernel_module_uvcvideo_disabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required

- name: Ensure kernel module 'uvcvideo' is blacklisted
  lineinfile:
    create: true
    dest: /etc/modprobe.d/uvcvideo.conf
    regexp: ^blacklist uvcvideo$
    line: blacklist uvcvideo
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040020
  - NIST-800-53-CM-7 (5) (b)
  - NIST-800-53-CM-7 (a)
  - disable_strategy
  - kernel_module_uvcvideo_disabled
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install uvcvideo" /etc/modprobe.d/uvcvideo.conf ; then
	
	sed -i 's#^install uvcvideo.*#install uvcvideo /bin/true#g' /etc/modprobe.d/uvcvideo.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/uvcvideo.conf
	echo "install uvcvideo /bin/true" >> /etc/modprobe.d/uvcvideo.conf
fi

if ! LC_ALL=C grep -q -m 1 "^blacklist uvcvideo$" /etc/modprobe.d/uvcvideo.conf ; then
	echo "blacklist uvcvideo" >> /etc/modprobe.d/uvcvideo.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable storing core dumps   [ref]

To set the runtime status of the kernel.core_pattern kernel parameter, run the following command:
$ sudo sysctl -w kernel.core_pattern=|/bin/false
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
kernel.core_pattern = |/bin/false
Rationale:
A core dump includes a memory image taken at the time the operating system terminates an application. The memory image could contain sensitive data and is generally useful only for developers trying to debug problems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_kernel_core_pattern
Identifiers and References

References:  CCI-000366, SC-7(10), FMT_SMF_EXT.1, SRG-OS-000480-GPOS-00227, OL08-00-010671, SV-248629r858629_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*kernel.core_pattern.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010671
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_core_pattern

- name: Comment out any occurrences of kernel.core_pattern from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*kernel.core_pattern
    replace: '#kernel.core_pattern'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010671
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_core_pattern

- name: Ensure sysctl kernel.core_pattern is set to |/bin/false
  sysctl:
    name: kernel.core_pattern
    value: '|/bin/false'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010671
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_core_pattern

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.core_pattern from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*kernel.core_pattern.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "kernel.core_pattern" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for kernel.core_pattern
#
/sbin/sysctl -q -n -w kernel.core_pattern="|/bin/false"

#
# If kernel.core_pattern present in /etc/sysctl.conf, change value to "|/bin/false"
#	else, add "kernel.core_pattern = |/bin/false" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.core_pattern")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "|/bin/false"

# 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.core_pattern\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^kernel.core_pattern\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Restrict Access to Kernel Message Buffer   [ref]

To set the runtime status of the kernel.dmesg_restrict kernel parameter, run the following command:
$ sudo sysctl -w kernel.dmesg_restrict=1
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
kernel.dmesg_restrict = 1
Rationale:
Unprivileged access to the kernel syslog can expose sensitive kernel address information.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_kernel_dmesg_restrict
Identifiers and References

References:  BP28(R23), 3.1.5, CCI-001090, CCI-001314, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), SI-11(a), SI-11(b), SRG-OS-000132-GPOS-00067, SRG-OS-000138-GPOS-00069, OL08-00-010375, SV-248579r858617_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*kernel.dmesg_restrict.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010375
  - NIST-800-171-3.1.5
  - NIST-800-53-SI-11(a)
  - NIST-800-53-SI-11(b)
  - disable_strategy
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required
  - sysctl_kernel_dmesg_restrict

- name: Comment out any occurrences of kernel.dmesg_restrict from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*kernel.dmesg_restrict
    replace: '#kernel.dmesg_restrict'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010375
  - NIST-800-171-3.1.5
  - NIST-800-53-SI-11(a)
  - NIST-800-53-SI-11(b)
  - disable_strategy
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required
  - sysctl_kernel_dmesg_restrict

- name: Ensure sysctl kernel.dmesg_restrict is set to 1
  sysctl:
    name: kernel.dmesg_restrict
    value: '1'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010375
  - NIST-800-171-3.1.5
  - NIST-800-53-SI-11(a)
  - NIST-800-53-SI-11(b)
  - disable_strategy
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required
  - sysctl_kernel_dmesg_restrict

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.dmesg_restrict from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*kernel.dmesg_restrict.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "kernel.dmesg_restrict" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for kernel.dmesg_restrict
#
/sbin/sysctl -q -n -w kernel.dmesg_restrict="1"

#
# If kernel.dmesg_restrict present in /etc/sysctl.conf, change value to "1"
#	else, add "kernel.dmesg_restrict = 1" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.dmesg_restrict")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "1"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^kernel.dmesg_restrict\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^kernel.dmesg_restrict\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kernel Image Loading   [ref]

To set the runtime status of the kernel.kexec_load_disabled kernel parameter, run the following command:
$ sudo sysctl -w kernel.kexec_load_disabled=1
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
kernel.kexec_load_disabled = 1
Rationale:
Disabling kexec_load allows greater control of the kernel memory. It makes it impossible to load another kernel image after it has been disabled.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_kernel_kexec_load_disabled
Identifiers and References

References:  CCI-001749, CM-6, SRG-OS-000480-GPOS-00227, SRG-OS-000366-GPOS-00153, OL08-00-010372, SV-248576r877463_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*kernel.kexec_load_disabled.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010372
  - NIST-800-53-CM-6
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_kexec_load_disabled

- name: Comment out any occurrences of kernel.kexec_load_disabled from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*kernel.kexec_load_disabled
    replace: '#kernel.kexec_load_disabled'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010372
  - NIST-800-53-CM-6
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_kexec_load_disabled

- name: Ensure sysctl kernel.kexec_load_disabled is set to 1
  sysctl:
    name: kernel.kexec_load_disabled
    value: '1'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010372
  - NIST-800-53-CM-6
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_kexec_load_disabled

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.kexec_load_disabled from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*kernel.kexec_load_disabled.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "kernel.kexec_load_disabled" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for kernel.kexec_load_disabled
#
/sbin/sysctl -q -n -w kernel.kexec_load_disabled="1"

#
# If kernel.kexec_load_disabled present in /etc/sysctl.conf, change value to "1"
#	else, add "kernel.kexec_load_disabled = 1" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.kexec_load_disabled")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "1"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^kernel.kexec_load_disabled\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^kernel.kexec_load_disabled\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disallow kernel profiling by unprivileged users   [ref]

To set the runtime status of the kernel.perf_event_paranoid kernel parameter, run the following command:
$ sudo sysctl -w kernel.perf_event_paranoid=2
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
kernel.perf_event_paranoid = 2
Rationale:
Kernel profiling can reveal sensitive information about kernel behaviour.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_kernel_perf_event_paranoid
Identifiers and References

References:  BP28(R23), CCI-001090, AC-6, FMT_SMF_EXT.1, SRG-OS-000132-GPOS-00067, SRG-OS-000138-GPOS-00069, OL08-00-010376, SV-248580r858619_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*kernel.perf_event_paranoid.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010376
  - NIST-800-53-AC-6
  - disable_strategy
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required
  - sysctl_kernel_perf_event_paranoid

- name: Comment out any occurrences of kernel.perf_event_paranoid from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*kernel.perf_event_paranoid
    replace: '#kernel.perf_event_paranoid'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010376
  - NIST-800-53-AC-6
  - disable_strategy
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required
  - sysctl_kernel_perf_event_paranoid

- name: Ensure sysctl kernel.perf_event_paranoid is set to 2
  sysctl:
    name: kernel.perf_event_paranoid
    value: '2'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010376
  - NIST-800-53-AC-6
  - disable_strategy
  - low_complexity
  - low_severity
  - medium_disruption
  - reboot_required
  - sysctl_kernel_perf_event_paranoid

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.perf_event_paranoid from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*kernel.perf_event_paranoid.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "kernel.perf_event_paranoid" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for kernel.perf_event_paranoid
#
/sbin/sysctl -q -n -w kernel.perf_event_paranoid="2"

#
# If kernel.perf_event_paranoid present in /etc/sysctl.conf, change value to "2"
#	else, add "kernel.perf_event_paranoid = 2" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.perf_event_paranoid")

# 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.perf_event_paranoid\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^kernel.perf_event_paranoid\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Access to Network bpf() Syscall From Unprivileged Processes   [ref]

To set the runtime status of the kernel.unprivileged_bpf_disabled kernel parameter, run the following command:
$ sudo sysctl -w kernel.unprivileged_bpf_disabled=1
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
kernel.unprivileged_bpf_disabled = 1
Rationale:
Loading and accessing the packet filters programs and maps using the bpf() syscall has the potential of revealing sensitive information about the kernel state.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_kernel_unprivileged_bpf_disabled
Identifiers and References

References:  BP28(R9), CCI-000366, AC-6, SC-7(10), FMT_SMF_EXT.1, SRG-OS-000132-GPOS-00067, SRG-OS-000480-GPOS-00227, OL08-00-040281, SV-248889r858681_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*kernel.unprivileged_bpf_disabled.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040281
  - NIST-800-53-AC-6
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_unprivileged_bpf_disabled

- name: Comment out any occurrences of kernel.unprivileged_bpf_disabled from config
    files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*kernel.unprivileged_bpf_disabled
    replace: '#kernel.unprivileged_bpf_disabled'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040281
  - NIST-800-53-AC-6
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_unprivileged_bpf_disabled

- name: Ensure sysctl kernel.unprivileged_bpf_disabled is set to 1
  sysctl:
    name: kernel.unprivileged_bpf_disabled
    value: '1'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040281
  - NIST-800-53-AC-6
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_unprivileged_bpf_disabled

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.unprivileged_bpf_disabled from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*kernel.unprivileged_bpf_disabled.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "kernel.unprivileged_bpf_disabled" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for kernel.unprivileged_bpf_disabled
#
/sbin/sysctl -q -n -w kernel.unprivileged_bpf_disabled="1"

#
# If kernel.unprivileged_bpf_disabled present in /etc/sysctl.conf, change value to "1"
#	else, add "kernel.unprivileged_bpf_disabled = 1" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.unprivileged_bpf_disabled")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "1"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^kernel.unprivileged_bpf_disabled\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^kernel.unprivileged_bpf_disabled\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Restrict usage of ptrace to descendant processes   [ref]

To set the runtime status of the kernel.yama.ptrace_scope kernel parameter, run the following command:
$ sudo sysctl -w kernel.yama.ptrace_scope=1
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
kernel.yama.ptrace_scope = 1
Rationale:
Unrestricted usage of ptrace allows compromised binaries to run ptrace on another processes of the user. Like this, the attacker can steal sensitive information from the target processes (e.g. SSH sessions, web browser, ...) without any additional assistance from the user (i.e. without resorting to phishing).
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_kernel_yama_ptrace_scope
Identifiers and References

References:  BP28(R25), CCI-000366, SC-7(10), SRG-OS-000132-GPOS-00067, SRG-OS-000480-GPOS-00227, OL08-00-040282, SV-248890r858683_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*kernel.yama.ptrace_scope.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040282
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_yama_ptrace_scope

- name: Comment out any occurrences of kernel.yama.ptrace_scope from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*kernel.yama.ptrace_scope
    replace: '#kernel.yama.ptrace_scope'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040282
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_yama_ptrace_scope

- name: Ensure sysctl kernel.yama.ptrace_scope is set to 1
  sysctl:
    name: kernel.yama.ptrace_scope
    value: '1'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040282
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_kernel_yama_ptrace_scope

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.yama.ptrace_scope from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*kernel.yama.ptrace_scope.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "kernel.yama.ptrace_scope" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for kernel.yama.ptrace_scope
#
/sbin/sysctl -q -n -w kernel.yama.ptrace_scope="1"

#
# If kernel.yama.ptrace_scope present in /etc/sysctl.conf, change value to "1"
#	else, add "kernel.yama.ptrace_scope = 1" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.yama.ptrace_scope")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "1"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^kernel.yama.ptrace_scope\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^kernel.yama.ptrace_scope\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Harden the operation of the BPF just-in-time compiler   [ref]

To set the runtime status of the net.core.bpf_jit_harden kernel parameter, run the following command:
$ sudo sysctl -w net.core.bpf_jit_harden=2
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
net.core.bpf_jit_harden = 2
Rationale:
When hardened, the extended Berkeley Packet Filter just-in-time compiler will randomize any kernel addresses in the BPF programs and maps, and will not expose the JIT addresses in /proc/kallsyms.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_net_core_bpf_jit_harden
Identifiers and References

References:  BP28(R12), CCI-000366, CM-6, SC-7(10), FMT_SMF_EXT.1, SRG-OS-000480-GPOS-00227, OL08-00-040286, SV-248894r858691_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*net.core.bpf_jit_harden.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040286
  - NIST-800-53-CM-6
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_core_bpf_jit_harden

- name: Comment out any occurrences of net.core.bpf_jit_harden from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*net.core.bpf_jit_harden
    replace: '#net.core.bpf_jit_harden'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040286
  - NIST-800-53-CM-6
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_core_bpf_jit_harden

- name: Ensure sysctl net.core.bpf_jit_harden is set to 2
  sysctl:
    name: net.core.bpf_jit_harden
    value: '2'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040286
  - NIST-800-53-CM-6
  - NIST-800-53-SC-7(10)
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_net_core_bpf_jit_harden

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of net.core.bpf_jit_harden from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*net.core.bpf_jit_harden.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "net.core.bpf_jit_harden" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for net.core.bpf_jit_harden
#
/sbin/sysctl -q -n -w net.core.bpf_jit_harden="2"

#
# If net.core.bpf_jit_harden present in /etc/sysctl.conf, change value to "2"
#	else, add "net.core.bpf_jit_harden = 2" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.core.bpf_jit_harden")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "2"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^net.core.bpf_jit_harden\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^net.core.bpf_jit_harden\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable the use of user namespaces   [ref]

To set the runtime status of the user.max_user_namespaces kernel parameter, run the following command:
$ sudo sysctl -w user.max_user_namespaces=0
To make sure that the setting is persistent, add the following line to a file in the directory /etc/sysctl.d:
user.max_user_namespaces = 0
When containers are deployed on the machine, the value should be set to large non-zero value.
Warning:  This configuration baseline was created to deploy the base operating system for general purpose workloads. When the operating system is configured for certain purposes, such as to host Linux Containers, it is expected that user.max_user_namespaces will be enabled.
Rationale:
It is detrimental for operating systems to provide, or install by default, functionality exceeding requirements or system objectives. These unnecessary capabilities or services are often overlooked and therefore may remain unsecured. They increase the risk to the platform by providing additional attack vectors. User namespaces are used primarily for Linux containers. The value 0 disallows the use of user namespaces.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sysctl_user_max_user_namespaces
Identifiers and References

References:  CCI-000366, SC-39, CM-6(a), FMT_SMF_EXT.1, SRG-OS-000480-GPOS-00227, OL08-00-040284, SV-248892r858687_rule


Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
- name: List /etc/sysctl.d/*.conf files
  find:
    paths:
    - /etc/sysctl.d/
    - /run/sysctl.d/
    - /usr/local/lib/sysctl.d/
    contains: ^[\s]*user.max_user_namespaces.*$
    patterns: '*.conf'
    file_type: any
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040284
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-39
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_user_max_user_namespaces

- name: Comment out any occurrences of user.max_user_namespaces from config files
  replace:
    path: '{{ item.path }}'
    regexp: ^[\s]*user.max_user_namespaces
    replace: '#user.max_user_namespaces'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040284
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-39
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_user_max_user_namespaces

- name: Ensure sysctl user.max_user_namespaces is set to 0
  sysctl:
    name: user.max_user_namespaces
    value: '0'
    sysctl_file: /etc/sysctl.conf
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040284
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-39
  - disable_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - reboot_required
  - sysctl_user_max_user_namespaces

Complexity:low
Disruption:medium
Reboot:true
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of user.max_user_namespaces from /etc/sysctl.d/*.conf files

for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

  matching_list=$(grep -P '^(?!#).*[\s]*user.max_user_namespaces.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
      # comment out "user.max_user_namespaces" matches to preserve user data
      sed -i "s/^${escaped_entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set sysctl config file which to save the desired value
#

SYSCONFIG_FILE="/etc/sysctl.conf"


#
# Set runtime for user.max_user_namespaces
#
/sbin/sysctl -q -n -w user.max_user_namespaces="0"

#
# If user.max_user_namespaces present in /etc/sysctl.conf, change value to "0"
#	else, add "user.max_user_namespaces = 0" to /etc/sysctl.conf
#

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^user.max_user_namespaces")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "0"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^user.max_user_namespaces\\>" "${SYSCONFIG_FILE}"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^user.max_user_namespaces\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
else
    if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
    fi
    printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   SELinux   Group contains 4 rules
[ref]   SELinux is a feature of the Linux kernel which can be used to guard against misconfigured or compromised programs. SELinux enforces the idea that programs should be limited in what files they can access and what actions they can take.

The default SELinux policy, as configured on Oracle Linux 8, has been sufficiently developed and debugged that it should be usable on almost any system with minimal configuration and a small amount of system administrator training. This policy prevents system services - including most of the common network-visible services such as mail servers, FTP servers, and DNS servers - from accessing files which those services have no valid reason to access. This action alone prevents a huge amount of possible damage from network attacks against services, from trojaned software, and so forth.

This guide recommends that SELinux be enabled using the default (targeted) policy on every Oracle Linux 8 system, unless that system has unusual requirements which make a stronger policy appropriate. For more information on SELinux, see https://docs.oracle.com/en/operating-systems/oracle-linux/selinux/.

Rule   Install policycoreutils Package   [ref]

The policycoreutils package can be installed with the following command:
$ sudo yum install policycoreutils
Rationale:
Security-enhanced Linux is a feature of the Linux kernel and a number of utilities with enhanced security functionality designed to add mandatory access controls to Linux. The Security-enhanced Linux kernel contains new architectural components originally developed to improve security of the Flask operating system. These architectural components provide general support for the enforcement of many kinds of mandatory access control policies, including those based on the concepts of Type Enforcement, Role-based Access Control, and Multi-level Security. policycoreutils contains the policy core utilities that are required for basic operation of an SELinux-enabled system. These utilities include load_policy to load SELinux policies, setfiles to label filesystems, newrole to switch roles, and so on.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_package_policycoreutils_installed
Identifiers and References

References:  CCI-001084, SRG-OS-000480-GPOS-00227, SRG-OS-000134-GPOS-00068, OL08-00-010171, SV-248549r779213_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=policycoreutils


[[packages]]
name = "policycoreutils"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_policycoreutils

class install_policycoreutils {
  package { 'policycoreutils':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure policycoreutils is installed
  package:
    name: policycoreutils
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010171
  - enable_strategy
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - package_policycoreutils_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "policycoreutils" ; then
    yum install -y "policycoreutils"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure SELinux Policy   [ref]

The SELinux targeted policy is appropriate for general-purpose desktops and servers, as well as systems in many other roles. To configure the system to use this policy, add or correct the following line in /etc/selinux/config:
SELINUXTYPE=targeted
Other policies, such as mls, provide additional security labeling and greater confinement but are not compatible with many general-purpose use cases.
Rationale:
Setting the SELinux policy to targeted or a more specialized policy ensures the system will confine processes that are likely to be targeted for exploitation, such as network or system services.

Note: During the development or debugging of SELinux modules, it is common to temporarily place non-production systems in permissive mode. In such temporary cases, SELinux policies should be developed, and once work is completed, the system should be reconfigured to targeted.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_selinux_policytype
Identifiers and References

References:  BP28(R66), 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, APO01.06, APO11.04, APO13.01, BAI03.05, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, MEA02.01, 3.1.2, 3.7.2, CCI-002165, CCI-002696, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), 4.2.3.4, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.2, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-004-6 R3.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, CIP-007-3 R6.5, AC-3, AC-3(3)(a), AU-9, SC-7(21), DE.AE-1, ID.AM-3, PR.AC-4, PR.AC-5, PR.AC-6, PR.DS-5, PR.PT-1, PR.PT-3, PR.PT-4, SRG-OS-000445-GPOS-00199, OL08-00-010450, SV-248596r853774_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: XCCDF Value var_selinux_policy_name # promote to variable
  set_fact:
    var_selinux_policy_name: !!str targeted
  tags:
    - always

- name: Configure SELinux Policy
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/selinux/config
      create: false
      regexp: ^SELINUXTYPE=
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/selinux/config
    lineinfile:
      path: /etc/selinux/config
      create: false
      regexp: ^SELINUXTYPE=
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/selinux/config
    lineinfile:
      path: /etc/selinux/config
      create: true
      regexp: ^SELINUXTYPE=
      line: SELINUXTYPE={{ var_selinux_policy_name }}
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010450
  - NIST-800-171-3.1.2
  - NIST-800-171-3.7.2
  - NIST-800-53-AC-3
  - NIST-800-53-AC-3(3)(a)
  - NIST-800-53-AU-9
  - NIST-800-53-SC-7(21)
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy
  - selinux_policytype

Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_selinux_policy_name='targeted'


if [ -e "/etc/selinux/config" ] ; then
    
    LC_ALL=C sed -i "/^SELINUXTYPE=/Id" "/etc/selinux/config"
else
    touch "/etc/selinux/config"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/selinux/config"

cp "/etc/selinux/config" "/etc/selinux/config.bak"
# Insert at the end of the file
printf '%s\n' "SELINUXTYPE=$var_selinux_policy_name" >> "/etc/selinux/config"
# Clean up after ourselves.
rm "/etc/selinux/config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure SELinux State is Enforcing   [ref]

The SELinux state should be set to enforcing at system boot time. In the file /etc/selinux/config, add or correct the following line to configure the system to boot into enforcing mode:
SELINUX=enforcing
Rationale:
Setting the SELinux state to enforcing ensures SELinux is able to confine potentially compromised processes to the security policy, which is designed to prevent them from causing damage to the system or further elevating their privileges.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_selinux_state
Identifiers and References

References:  BP28(R4), BP28(R66), 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, APO01.06, APO11.04, APO13.01, BAI03.05, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, MEA02.01, 3.1.2, 3.7.2, CCI-001084, CCI-002165, CCI-002696, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), 4.2.3.4, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.2, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-004-6 R3.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, CIP-007-3 R6.5, AC-3, AC-3(3)(a), AU-9, SC-7(21), DE.AE-1, ID.AM-3, PR.AC-4, PR.AC-5, PR.AC-6, PR.DS-5, PR.PT-1, PR.PT-3, PR.PT-4, SRG-OS-000445-GPOS-00199, SRG-OS-000134-GPOS-00068, OL08-00-010170, SV-248548r779210_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: XCCDF Value var_selinux_state # promote to variable
  set_fact:
    var_selinux_state: !!str enforcing
  tags:
    - always

- name: Ensure SELinux State is Enforcing
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/selinux/config
      create: false
      regexp: ^SELINUX=
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/selinux/config
    lineinfile:
      path: /etc/selinux/config
      create: false
      regexp: ^SELINUX=
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/selinux/config
    lineinfile:
      path: /etc/selinux/config
      create: true
      regexp: ^SELINUX=
      line: SELINUX={{ var_selinux_state }}
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010170
  - NIST-800-171-3.1.2
  - NIST-800-171-3.7.2
  - NIST-800-53-AC-3
  - NIST-800-53-AC-3(3)(a)
  - NIST-800-53-AU-9
  - NIST-800-53-SC-7(21)
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - restrict_strategy
  - selinux_state

Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_selinux_state='enforcing'


if [ -e "/etc/selinux/config" ] ; then
    
    LC_ALL=C sed -i "/^SELINUX=/Id" "/etc/selinux/config"
else
    touch "/etc/selinux/config"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/selinux/config"

cp "/etc/selinux/config" "/etc/selinux/config.bak"
# Insert at the end of the file
printf '%s\n' "SELINUX=$var_selinux_state" >> "/etc/selinux/config"
# Clean up after ourselves.
rm "/etc/selinux/config.bak"

fixfiles onboot
fixfiles -f relabel

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Services   Group contains 25 groups and 58 rules
[ref]   The best protection against vulnerable software is running less software. This section describes how to review the software which Oracle Linux 8 installs on a system and disable software which is not needed. It then enumerates the software packages installed on a default Oracle Linux 8 system and provides guidance about which ones can be safely disabled.

Oracle Linux 8 provides a convenient minimal install option that essentially installs the bare necessities for a functional system. When building Oracle Linux 8 systems, it is highly recommended to select the minimal packages and then build up the system from there.
Group   Base Services   Group contains 2 rules
[ref]   This section addresses the base services that are installed on a Oracle Linux 8 default installation which are not covered in other sections. Some of these services listen on the network and should be treated with particular discretion. Other services are local system utilities that may or may not be extraneous. In general, system services should be disabled if not required.

Rule   Uninstall Automatic Bug Reporting Tool (abrt)   [ref]

The Automatic Bug Reporting Tool (abrt) collects and reports crash data when an application crash is detected. Using a variety of plugins, abrt can email crash reports to system administrators, log crash reports to files, or forward crash reports to a centralized issue tracking system such as RHTSupport. The abrt package can be removed with the following command:
$ sudo yum erase abrt
Rationale:
Mishandling crash data could expose sensitive information about vulnerabilities in software executing on the system, as well as sensitive information from within a process's address space or registers.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_abrt_removed
Identifiers and References

References:  CCI-000381, SRG-OS-000095-GPOS-00049, OL08-00-040001, SV-248824r780038_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=abrt

Complexity:low
Disruption:low
Strategy:disable
include remove_abrt

class remove_abrt {
  package { 'abrt':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure abrt is removed
  package:
    name: abrt
    state: absent
  tags:
  - DISA-STIG-OL08-00-040001
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_abrt_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove abrt
#	   from the system, and may remove any packages
#	   that depend on abrt. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "abrt" ; then

    yum remove -y "abrt"

fi

Rule   Disable KDump Kernel Crash Analyzer (kdump)   [ref]

The kdump service provides a kernel crash dump analyzer. It uses the kexec system call to boot a secondary kernel ("capture" kernel) following a system crash, which can load information from the crashed kernel for analysis. The kdump service can be disabled with the following command:
$ sudo systemctl mask --now kdump.service
Rationale:
Kernel core dumps may contain the full contents of system memory at the time of the crash. Kernel core dumps consume a considerable amount of disk space and may result in denial of service by exhausting the available space on the target file system partition. Unless the system is used for kernel development or testing, there is little need to run the kdump service.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_kdump_disabled
Identifiers and References

References:  11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000366, CCI-001665, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, FMT_SMF_EXT.1.1, SRG-OS-000269-GPOS-00103, SRG-OS-000480-GPOS-00227, OL08-00-010670, SV-248628r779450_rule



kdump --disable


[customizations.services]
disabled = ["kdump"]

Complexity:low
Disruption:low
Strategy:enable
include disable_kdump

class disable_kdump {
  service {'kdump':
    enable => false,
    ensure => 'stopped',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Block Disable service kdump
  block:

  - name: Disable service kdump
    block:

    - name: Disable service kdump
      systemd:
        name: kdump.service
        enabled: 'no'
        state: stopped
        masked: 'yes'
    rescue:

    - name: Intentionally ignored previous 'Disable service kdump' failure, service
        was already disabled
      meta: noop
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010670
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_kdump_disabled

- name: Unit Socket Exists - kdump.socket
  command: systemctl list-unit-files kdump.socket
  register: socket_file_exists
  changed_when: false
  failed_when: socket_file_exists.rc not in [0, 1]
  check_mode: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010670
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_kdump_disabled

- name: Disable socket kdump
  systemd:
    name: kdump.socket
    enabled: 'no'
    state: stopped
    masked: 'yes'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '"kdump.socket" in socket_file_exists.stdout_lines[1]'
  tags:
  - DISA-STIG-OL08-00-010670
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_kdump_disabled

Complexity:low
Disruption:low
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" stop 'kdump.service'
"$SYSTEMCTL_EXEC" disable 'kdump.service'
"$SYSTEMCTL_EXEC" mask 'kdump.service'
# Disable socket activation if we have a unit file for it
if "$SYSTEMCTL_EXEC" -q list-unit-files kdump.socket; then
    "$SYSTEMCTL_EXEC" stop 'kdump.socket'
    "$SYSTEMCTL_EXEC" mask 'kdump.socket'
fi
# The service may not be running because it has been started and failed,
# so let's reset the state so OVAL checks pass.
# Service should be 'inactive', not 'failed' after reboot though.
"$SYSTEMCTL_EXEC" reset-failed 'kdump.service' || true

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Application Whitelisting Daemon   Group contains 3 rules
[ref]   Fapolicyd (File Access Policy Daemon) implements application whitelisting to decide file access rights. Applications that are known via a reputation source are allowed access while unknown applications are not. The daemon makes use of the kernel's fanotify interface to determine file access rights.

Rule   Install fapolicyd Package   [ref]

The fapolicyd package can be installed with the following command:
$ sudo yum install fapolicyd
Rationale:
fapolicyd (File Access Policy Daemon) implements application whitelisting to decide file access rights.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_fapolicyd_installed
Identifiers and References

References:  CCI-001764, CCI-001774, CM-6(a), SI-4(22), SRG-OS-000370-GPOS-00155, SRG-OS-000368-GPOS-00154, SRG-OS-000480-GPOS-00230, OL08-00-040135, SV-248859r853868_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=fapolicyd


[[packages]]
name = "fapolicyd"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_fapolicyd

class install_fapolicyd {
  package { 'fapolicyd':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure fapolicyd is installed
  package:
    name: fapolicyd
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040135
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-4(22)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_fapolicyd_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "fapolicyd" ; then
    yum install -y "fapolicyd"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable the File Access Policy Service   [ref]

The File Access Policy service should be enabled. The fapolicyd service can be enabled with the following command:
$ sudo systemctl enable fapolicyd.service
Rationale:
The fapolicyd service (File Access Policy Daemon) implements application whitelisting to decide file access rights.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_fapolicyd_enabled
Identifiers and References

References:  CCI-001764, CCI-001774, CM-6(a), SI-4(22), FMT_SMF_EXT.1, SRG-OS-000370-GPOS-00155, SRG-OS-000368-GPOS-00154, SRG-OS-000480-GPOS-00230, OL08-00-040136, SV-248860r853869_rule



[customizations.services]
enabled = ["fapolicyd"]

Complexity:low
Disruption:low
Strategy:enable
include enable_fapolicyd

class enable_fapolicyd {
  service {'fapolicyd':
    enable => true,
    ensure => 'running',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Enable service fapolicyd
  block:

  - name: Gather the package facts
    package_facts:
      manager: auto

  - name: Enable service fapolicyd
    systemd:
      name: fapolicyd
      enabled: 'yes'
      state: started
      masked: 'no'
    when:
    - '"fapolicyd" in ansible_facts.packages'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040136
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-4(22)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_fapolicyd_enabled

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'fapolicyd.service'
"$SYSTEMCTL_EXEC" start 'fapolicyd.service'
"$SYSTEMCTL_EXEC" enable 'fapolicyd.service'

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure Fapolicy Module to Employ a Deny-all, Permit-by-exception Policy to Allow the Execution of Authorized Software Programs.   [ref]

The Fapolicy module must be configured to employ a deny-all, permit-by-exception policy to allow the execution of authorized software programs and to prevent unauthorized software from running.
Warning:  This rule doesn't come with a remediation. Before remediating the system administrator needs to create an allowlist of authorized software.
Rationale:
Utilizing a whitelist provides a configuration management method for allowing the execution of only authorized software. Using only authorized software decreases risk by limiting the number of potential vulnerabilities. Verification of whitelisted software occurs prior to execution or at system startup. Proceed with caution with enforcing the use of this daemon. Improper configuration may render the system non-functional. The "fapolicyd" API is not namespace aware and can cause issues when launching or running containers.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_fapolicy_default_deny
Identifiers and References

References:  CCI-001764, CM-7 (2), CM-7 (5) (b), CM-6 b, SRG-OS-000368-GPOS-00154, SRG-OS-000370-GPOS-00155, SRG-OS-000480-GPOS-00232, OL08-00-040137, SV-248861r860922_rule

Group   FTP Server   Group contains 1 group and 1 rule
[ref]   FTP is a common method for allowing remote access to files. Like telnet, the FTP protocol is unencrypted, which means that passwords and other data transmitted during the session can be captured and that the session is vulnerable to hijacking. Therefore, running the FTP server software is not recommended.

However, there are some FTP server configurations which may be appropriate for some environments, particularly those which allow only read-only anonymous access as a means of downloading data available to the public.
Group   Disable vsftpd if Possible   Group contains 1 rule
[ref]   To minimize attack surface, disable vsftpd if at all possible.

Rule   Uninstall vsftpd Package   [ref]

The vsftpd package can be removed with the following command:
 $ sudo yum erase vsftpd
Rationale:
Removing the vsftpd package decreases the risk of its accidental activation.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_package_vsftpd_removed
Identifiers and References

References:  11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000197, CCI-000366, CCI-000381, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), IA-5(1).1(v), CM-7, CM-7.1(ii), PR.IP-1, PR.PT-3, 2.2.4, SRG-OS-000074-GPOS-00042, SRG-OS-000095-GPOS-00049, SRG-OS-000480-GPOS-00227, OL08-00-040360, SV-248903r780275_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=vsftpd

Complexity:low
Disruption:low
Strategy:disable
include remove_vsftpd

class remove_vsftpd {
  package { 'vsftpd':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure vsftpd is removed
  package:
    name: vsftpd
    state: absent
  tags:
  - DISA-STIG-OL08-00-040360
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-CM-7.1(ii)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(1).1(v)
  - PCI-DSSv4-2.2.4
  - disable_strategy
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - package_vsftpd_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove vsftpd
#	   from the system, and may remove any packages
#	   that depend on vsftpd. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "vsftpd" ; then

    yum remove -y "vsftpd"

fi
Group   Kerberos   Group contains 2 rules
[ref]   The Kerberos protocol is used for authentication across non-secure network. Authentication can happen between various types of principals -- users, service, or hosts. Their identity and encryption keys can be stored in keytab files.

Rule   Remove the Kerberos Server Package   [ref]

The krb5-server package should be removed if not in use. Is this system the Kerberos server? If not, remove the package. The krb5-server package can be removed with the following command:
$ sudo yum erase krb5-server
The krb5-server RPM is not installed by default on a Oracle Linux 8 system. It is needed only by the Kerberos servers, not by the clients which use Kerberos for authentication. If the system is not intended for use as a Kerberos Server it should be removed.
Rationale:
Unnecessary packages should not be installed to decrease the attack surface of the system. While this software is clearly essential on an KDC server, it is not necessary on typical desktop or workstation systems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_krb5-server_removed
Identifiers and References

References:  CCI-000803, IA-7, IA-7.1, SRG-OS-000120-GPOS-00061, OL08-00-010163, SV-248547r779207_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=krb5-server

Complexity:low
Disruption:low
Strategy:disable
include remove_krb5-server

class remove_krb5-server {
  package { 'krb5-server':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure krb5-server is removed
  package:
    name: krb5-server
    state: absent
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010163
  - NIST-800-53-IA-7
  - NIST-800-53-IA-7.1
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_krb5-server_removed

Complexity:low
Disruption:low
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# CAUTION: This remediation script will remove krb5-server
#	   from the system, and may remove any packages
#	   that depend on krb5-server. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "krb5-server" ; then

    yum remove -y "krb5-server"

fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable Kerberos by removing host keytab   [ref]

Kerberos is not an approved key distribution method for Common Criteria. To prevent using Kerberos by system daemons, remove the Kerberos keytab files, especially /etc/krb5.keytab.
Rationale:
The key derivation function (KDF) in Kerberos is not FIPS compatible.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kerberos_disable_no_keytab
Identifiers and References

References:  CCI-000803, 0418, 1055, 1402, FTP_ITC_EXT.1, SRG-OS-000120-GPOS-00061, OL08-00-010161, SV-248545r779201_rule


Complexity:low
Disruption:low
Strategy:disable
- name: Find keytab files
  find:
    paths: /etc/
    patterns: '*.keytab'
  register: keytab_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010161
  - disable_strategy
  - kerberos_disable_no_keytab
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Remove keytab files
  file:
    path: '{{ item.path }}'
    state: absent
  with_items: '{{ keytab_files.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010161
  - disable_strategy
  - kerberos_disable_no_keytab
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

rm -f /etc/*.keytab

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Mail Server Software   Group contains 4 groups and 4 rules
[ref]   Mail servers are used to send and receive email over the network. Mail is a very common service, and Mail Transfer Agents (MTAs) are obvious targets of network attack. Ensure that systems are not running MTAs unnecessarily, and configure needed MTAs as defensively as possible.

Very few systems at any site should be configured to directly receive email over the network. Users should instead use mail client programs to retrieve email from a central server that supports protocols such as IMAP or POP3. However, it is normal for most systems to be independently capable of sending email, for instance so that cron jobs can report output to an administrator. Most MTAs, including Postfix, support a submission-only mode in which mail can be sent from the local system to a central site MTA (or directly delivered to a local account), but the system still cannot receive mail directly over a network.

The alternatives program in Oracle Linux 8 permits selection of other mail server software (such as Sendmail), but Postfix is the default and is preferred. Postfix was coded with security in mind and can also be more effectively contained by SELinux as its modular design has resulted in separate processes performing specific actions. More information is available on its website, http://www.postfix.org.
Group   Configure SMTP For Mail Clients   Group contains 1 rule
[ref]   This section discusses settings for Postfix in a submission-only e-mail configuration.

Rule   Configure System to Forward All Mail From Postmaster to The Root Account   [ref]

Verify the administrators are notified in the event of an audit processing failure. Check that the "/etc/aliases" file has a defined value for "root".
$ sudo grep "postmaster:\s*root$" /etc/aliases

postmaster: root
Rationale:
It is critical for the appropriate personnel to be aware if a system is at risk of failing to process audit logs as required. Without this notification, the security personnel may be unaware of an impending failure of the audit capability, and system operation may be adversely affected. Audit processing failures include software/hardware errors, failures in the audit capturing mechanisms, and audit storage capacity being reached or exceeded.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_postfix_client_configure_mail_alias_postmaster
Identifiers and References

References:  CCI-000139, AU-5(a), AU-5.1(ii), SRG-OS-000046-GPOS-00022, OL08-00-030030, SV-248725r779741_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Configure System to Forward All Mail From Postmaster to The Root Account
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/aliases
      create: false
      regexp: ^\s*postmaster\s*:\s*
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/aliases
    lineinfile:
      path: /etc/aliases
      create: false
      regexp: ^\s*postmaster\s*:\s*
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/aliases
    lineinfile:
      path: /etc/aliases
      create: true
      regexp: ^\s*postmaster\s*:\s*
      line: 'postmaster: root'
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030030
  - NIST-800-53-AU-5(a)
  - NIST-800-53-AU-5.1(ii)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - postfix_client_configure_mail_alias_postmaster

- name: Check if newaliases command is available
  ansible.builtin.stat:
    path: /usr/bin/newaliases
  register: result_newaliases_present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030030
  - NIST-800-53-AU-5(a)
  - NIST-800-53-AU-5.1(ii)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - postfix_client_configure_mail_alias_postmaster

- name: Update postfix aliases
  ansible.builtin.command:
    cmd: newaliases
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - result_newaliases_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-030030
  - NIST-800-53-AU-5(a)
  - NIST-800-53-AU-5.1(ii)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - postfix_client_configure_mail_alias_postmaster

Complexity:low
Disruption:low
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if [ -e "/etc/aliases" ] ; then
    
    LC_ALL=C sed -i "/^\s*postmaster\s*:\s*/Id" "/etc/aliases"
else
    touch "/etc/aliases"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/aliases"

cp "/etc/aliases" "/etc/aliases.bak"
# Insert at the end of the file
printf '%s\n' "postmaster: root" >> "/etc/aliases"
# Clean up after ourselves.
rm "/etc/aliases.bak"

if [ -f /usr/bin/newaliases ]; then
    newaliases
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Configure Operating System to Protect Mail Server   Group contains 2 groups and 1 rule
[ref]   The guidance in this section is appropriate for any host which is operating as a site MTA, whether the mail server runs using Sendmail, Postfix, or some other software.
Group   Configure Postfix if Necessary   Group contains 1 group and 1 rule
[ref]   Postfix stores its configuration files in the directory /etc/postfix by default. The primary configuration file is /etc/postfix/main.cf.
Group   Control Mail Relaying   Group contains 1 rule
[ref]   Postfix's mail relay controls are implemented with the help of the smtpd recipient restrictions option, which controls the restrictions placed on the SMTP dialogue once the sender and recipient envelope addresses are known. The guidance in the following sections should be applied to all systems. If there are systems which must be allowed to relay mail, but which cannot be trusted to relay unconditionally, configure SMTP AUTH with SSL support.

Rule   Prevent Unrestricted Mail Relaying   [ref]

Modify the
/etc/postfix/main.cf
file to restrict client connections to the local network with the following command:
$ sudo postconf -e 'smtpd_client_restrictions = permit_mynetworks,reject'
Rationale:
If unrestricted mail relaying is permitted, unauthorized senders could use this host as a mail relay for the purpose of sending spam or other unauthorized activity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_postfix_prevent_unrestricted_relay
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-040290, SV-248895r780251_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-040290
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - postfix_prevent_unrestricted_relay
  - restrict_strategy

- name: Prevent Unrestricted Mail Relaying
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/postfix/main.cf
      create: false
      regexp: ^[ \t]*smtpd_client_restrictions\s*=\s*
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/postfix/main.cf
    lineinfile:
      path: /etc/postfix/main.cf
      create: false
      regexp: ^[ \t]*smtpd_client_restrictions\s*=\s*
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/postfix/main.cf
    lineinfile:
      path: /etc/postfix/main.cf
      create: true
      regexp: ^[ \t]*smtpd_client_restrictions\s*=\s*
      line: smtpd_client_restrictions = permit_mynetworks,reject
      state: present
  when:
  - '"postfix" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040290
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - postfix_prevent_unrestricted_relay
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q postfix; then

if ! grep -q ^smtpd_client_restrictions /etc/postfix/main.cf; then
	echo "smtpd_client_restrictions = permit_mynetworks,reject" >> /etc/postfix/main.cf
else
	sed -i "s/^smtpd_client_restrictions.*/smtpd_client_restrictions = permit_mynetworks,reject/g" /etc/postfix/main.cf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   The mailx Package Is Installed   [ref]

A mail server is required for sending emails. The mailx package can be installed with the following command:
$ sudo yum install mailx
Rationale:
Emails can be used to notify designated personnel about important system events such as failures or warnings.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_mailx_installed
Identifiers and References

References:  CCI-001744, CM-3(5), SRG-OS-000363-GPOS-00150, OL08-00-010358, SV-256979r902803_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=mailx


[[packages]]
name = "mailx"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_mailx

class install_mailx {
  package { 'mailx':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure mailx is installed
  package:
    name: mailx
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010358
  - NIST-800-53-CM-3(5)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_mailx_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "mailx" ; then
    yum install -y "mailx"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Uninstall Sendmail Package   [ref]

Sendmail is not the default mail transfer agent and is not installed by default. The sendmail package can be removed with the following command:
$ sudo yum erase sendmail
Rationale:
The sendmail software was not developed with security in mind and its design prevents it from being effectively contained by SELinux. Postfix should be used instead.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_sendmail_removed
Identifiers and References

References:  BP28(R1), 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000381, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, SRG-OS-000480-GPOS-00227, SRG-OS-000095-GPOS-00049, OL08-00-040002, SV-248825r780041_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=sendmail

Complexity:low
Disruption:low
Strategy:disable
include remove_sendmail

class remove_sendmail {
  package { 'sendmail':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure sendmail is removed
  package:
    name: sendmail
    state: absent
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040002
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_sendmail_removed

Complexity:low
Disruption:low
Strategy:disable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# CAUTION: This remediation script will remove sendmail
#	   from the system, and may remove any packages
#	   that depend on sendmail. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "sendmail" ; then

    yum remove -y "sendmail"

fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   NFS and RPC   Group contains 2 groups and 3 rules
[ref]   The Network File System is a popular distributed filesystem for the Unix environment, and is very widely deployed. This section discusses the circumstances under which it is possible to disable NFS and its dependencies, and then details steps which should be taken to secure NFS's configuration. This section is relevant to systems operating as NFS clients, as well as to those operating as NFS servers.
Group   Configure NFS Clients   Group contains 1 group and 3 rules
[ref]   The steps in this section are appropriate for systems which operate as NFS clients.
Group   Mount Remote Filesystems with Restrictive Options   Group contains 3 rules
[ref]   Edit the file /etc/fstab. For each filesystem whose type (column 3) is nfs or nfs4, add the text ,nodev,nosuid to the list of mount options in column 4. If appropriate, also add ,noexec.

See the section titled "Restrict Partition Mount Options" for a description of the effects of these options. In general, execution of files mounted via NFS should be considered risky because of the possibility that an adversary could intercept the request and substitute a malicious file. Allowing setuid files to be executed from remote servers is particularly risky, both for this reason and because it requires the clients to extend root-level trust to the NFS server.

Rule   Mount Remote Filesystems with nodev   [ref]

Add the nodev option to the fourth column of /etc/fstab for the line which controls mounting of any NFS mounts.
Rationale:
Legitimate device files should only exist in the /dev directory. NFS mounts should not present device files to users.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_nodev_remote_filesystems
Identifiers and References

References:  11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.11.2.9, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, CM-6(a), MP-2, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000480-GPOS-00227, OL08-00-010640, SV-248625r779441_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Get nfs and nfs4 mount points, that don't have nodev
  command: findmnt --fstab --types nfs,nfs4 -O nonodev -n -P
  register: points_register
  check_mode: false
  changed_when: false
  failed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010640
  - NIST-800-53-CM-6(a)
  - NIST-800-53-MP-2
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - mount_option_nodev_remote_filesystems
  - no_reboot_needed

- name: Add nodev to nfs and nfs4 mount points
  mount:
    path: '{{ item | regex_search(''TARGET="([^"]+)"'',''\1'') | first }}'
    src: '{{ item | regex_search(''SOURCE="([^"]+)"'',''\1'') | first }}'
    fstype: '{{ item | regex_search(''FSTYPE="([^"]+)"'',''\1'') | first }}'
    state: present
    opts: '{{ item | regex_search(''OPTIONS="([^"]+)"'',''\1'') | first }},nodev'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (points_register.stdout | length > 0) and '\\x09' not in item
  with_items: '{{ points_register.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010640
  - NIST-800-53-CM-6(a)
  - NIST-800-53-MP-2
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - mount_option_nodev_remote_filesystems
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

vfstype_points=()
readarray -t vfstype_points < <(grep -E "[[:space:]]nfs[4]?[[:space:]]" /etc/fstab | awk '{print $2}')

for vfstype_point in "${vfstype_points[@]}"
do
    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" ${vfstype_point//\\/\\\\})"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " ${vfstype_point//\\/\\\\} nfs4 defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nodev"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Mount Remote Filesystems with noexec   [ref]

Add the noexec option to the fourth column of /etc/fstab for the line which controls mounting of any NFS mounts.
Rationale:
The noexec mount option causes the system not to execute binary files. This option must be used for mounting any file system not containing approved binary files as they may be incompatible. Executing files from untrusted file systems increases the opportunity for unprivileged users to attain unauthorized administrative access.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_noexec_remote_filesystems
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-000366, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, AC-6, AC-6(8), AC-6(10), CM-6(a), PR.AC-4, PR.DS-5, SRG-OS-000480-GPOS-00227, OL08-00-010630, SV-248624r779438_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Get nfs and nfs4 mount points, that don't have noexec
  command: findmnt --fstab --types nfs,nfs4 -O nonoexec -n -P
  register: points_register
  check_mode: false
  changed_when: false
  failed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010630
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(10)
  - NIST-800-53-AC-6(8)
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - mount_option_noexec_remote_filesystems
  - no_reboot_needed

- name: Add noexec to nfs and nfs4 mount points
  mount:
    path: '{{ item | regex_search(''TARGET="([^"]+)"'',''\1'') | first }}'
    src: '{{ item | regex_search(''SOURCE="([^"]+)"'',''\1'') | first }}'
    fstype: '{{ item | regex_search(''FSTYPE="([^"]+)"'',''\1'') | first }}'
    state: present
    opts: '{{ item | regex_search(''OPTIONS="([^"]+)"'',''\1'') | first }},noexec'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (points_register.stdout | length > 0) and '\\x09' not in item
  with_items: '{{ points_register.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010630
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(10)
  - NIST-800-53-AC-6(8)
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - mount_option_noexec_remote_filesystems
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

vfstype_points=()
readarray -t vfstype_points < <(grep -E "[[:space:]]nfs[4]?[[:space:]]" /etc/fstab | awk '{print $2}')

for vfstype_point in "${vfstype_points[@]}"
do
    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" ${vfstype_point//\\/\\\\})"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " ${vfstype_point//\\/\\\\} nfs4 defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "noexec"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Mount Remote Filesystems with nosuid   [ref]

Add the nosuid option to the fourth column of /etc/fstab for the line which controls mounting of any NFS mounts.
Rationale:
NFS mounts should not present suid binaries to users. Only vendor-supplied suid executables should be installed to their default location on the local filesystem.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_mount_option_nosuid_remote_filesystems
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-000366, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, AC-6, AC-6(1), CM6(a), PR.AC-4, PR.DS-5, SRG-OS-000480-GPOS-00227, OL08-00-010650, SV-248626r779444_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Get nfs and nfs4 mount points, that don't have nosuid
  command: findmnt --fstab --types nfs,nfs4 -O nonosuid -n -P
  register: points_register
  check_mode: false
  changed_when: false
  failed_when: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010650
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM6(a)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - mount_option_nosuid_remote_filesystems
  - no_reboot_needed

- name: Add nosuid to nfs and nfs4 mount points
  mount:
    path: '{{ item | regex_search(''TARGET="([^"]+)"'',''\1'') | first }}'
    src: '{{ item | regex_search(''SOURCE="([^"]+)"'',''\1'') | first }}'
    fstype: '{{ item | regex_search(''FSTYPE="([^"]+)"'',''\1'') | first }}'
    state: present
    opts: '{{ item | regex_search(''OPTIONS="([^"]+)"'',''\1'') | first }},nosuid'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - (points_register.stdout | length > 0) and '\\x09' not in item
  with_items: '{{ points_register.stdout_lines }}'
  tags:
  - DISA-STIG-OL08-00-010650
  - NIST-800-53-AC-6
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM6(a)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - mount_option_nosuid_remote_filesystems
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

vfstype_points=()
readarray -t vfstype_points < <(grep -E "[[:space:]]nfs[4]?[[:space:]]" /etc/fstab | awk '{print $2}')

for vfstype_point in "${vfstype_points[@]}"
do
    mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" ${vfstype_point//\\/\\\\})"

    # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
    if ! grep "$mount_point_match_regexp" /etc/fstab; then
        # runtime opts without some automatic kernel/userspace-added defaults
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                    | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
        [ "$previous_mount_opts" ] && previous_mount_opts+=","
        echo " ${vfstype_point//\\/\\\\} nfs4 defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
    # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
    elif ! grep "$mount_point_match_regexp" /etc/fstab | grep "nosuid"; then
        previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
        sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Network Time Protocol   Group contains 4 rules
[ref]   The Network Time Protocol is used to manage the system clock over a network. Computer clocks are not very accurate, so time will drift unpredictably on unmanaged systems. Central time protocols can be used both to ensure that time is consistent among a network of systems, and that their time is consistent with the outside world.

If every system on a network reliably reports the same time, then it is much easier to correlate log messages in case of an attack. In addition, a number of cryptographic protocols (such as Kerberos) use timestamps to prevent certain types of attacks. If your network does not have synchronized time, these protocols may be unreliable or even unusable.

Depending on the specifics of the network, global time accuracy may be just as important as local synchronization, or not very important at all. If your network is connected to the Internet, using a public timeserver (or one provided by your enterprise) provides globally accurate timestamps which may be essential in investigating or responding to an attack which originated outside of your network.

A typical network setup involves a small number of internal systems operating as NTP servers, and the remainder obtaining time information from those internal servers.

There is a choice between the daemons ntpd and chronyd, which are available from the repositories in the ntp and chrony packages respectively.

The default chronyd daemon can work well when external time references are only intermittently accesible, can perform well even when the network is congested for longer periods of time, can usually synchronize the clock faster and with better time accuracy, and quickly adapts to sudden changes in the rate of the clock, for example, due to changes in the temperature of the crystal oscillator. Chronyd should be considered for all systems which are frequently suspended or otherwise intermittently disconnected and reconnected to a network. Mobile and virtual systems for example.

The ntpd NTP daemon fully supports NTP protocol version 4 (RFC 5905), including broadcast, multicast, manycast clients and servers, and the orphan mode. It also supports extra authentication schemes based on public-key cryptography (RFC 5906). The NTP daemon (ntpd) should be considered for systems which are normally kept permanently on. Systems which are required to use broadcast or multicast IP, or to perform authentication of packets with the Autokey protocol, should consider using ntpd.

Refer to https://docs.oracle.com/en/operating-systems/oracle-linux/8/network/network-ConfiguringNetworkTime.html#ol-nettime for more detailed comparison of features of chronyd and ntpd daemon features respectively, and for further guidance how to choose between the two NTP daemons.

The upstream manual pages at http://chrony.tuxfamily.org/manual.html for chronyd and http://www.ntp.org for ntpd provide additional information on the capabilities and configuration of each of the NTP daemons.

Rule   Disable chrony daemon from acting as server   [ref]

The port option in /etc/chrony.conf can be set to 0 to make chrony daemon to never open any listening port for server operation and to operate strictly in a client-only mode.
Rationale:
In order to prevent unauthorized connection of devices, unauthorized transfer of information, or unauthorized tunneling (i.e., embedding of data types within data types), organizations must disable or restrict unused or unnecessary physical and logical ports/protocols on information systems. Operating systems are capable of providing a wide variety of functions and services. Some of the functions and services provided by default may not be necessary to support essential organizational operations. Additionally, it is sometimes convenient to provide multiple services from a single component (e.g., VPN and IPS); however, doing so increases risk over limiting the services provided by any one component. To support the requirements and principles of least functionality, the operating system must support the organizational requirements, providing only essential capabilities and limiting the use of ports, protocols, and/or services to only those required, authorized, and approved to conduct official business or to address authorized quality of life issues.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_chronyd_client_only
Identifiers and References

References:  CCI-000381, AU-8(1), AU-12(1), FMT_SMF_EXT.1, SRG-OS-000096-GPOS-00050, SRG-OS-000095-GPOS-00049, OL08-00-030741, SV-248821r780029_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Disable chrony daemon from acting as server
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/chrony.conf
      create: false
      regexp: ^\s*port\s+
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/chrony.conf
    lineinfile:
      path: /etc/chrony.conf
      create: false
      regexp: ^\s*port\s+
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/chrony.conf
    lineinfile:
      path: /etc/chrony.conf
      create: true
      regexp: ^\s*port\s+
      line: port 0
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030741
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)
  - chronyd_client_only
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^port")

# shellcheck disable=SC2059
printf -v formatted_output "%s %s" "$stripped_key" "0"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^port\\>" "/etc/chrony.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^port\\>.*/$escaped_formatted_output/gi" "/etc/chrony.conf"
else
    if [[ -s "/etc/chrony.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/chrony.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/chrony.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/chrony.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Disable network management of chrony daemon   [ref]

The cmdport option in /etc/chrony.conf can be set to 0 to stop chrony daemon from listening on the UDP port 323 for management connections made by chronyc.
Rationale:
Minimizing the exposure of the server functionality of the chrony daemon diminishes the attack surface.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_chronyd_no_chronyc_network
Identifiers and References

References:  CCI-000381, CM-7(1), FMT_SMF_EXT.1, SRG-OS-000096-GPOS-00050, SRG-OS-000095-GPOS-00049, OL08-00-030742, SV-248822r780032_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Disable network management of chrony daemon
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/chrony.conf
      create: false
      regexp: ^\s*cmdport\s+
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/chrony.conf
    lineinfile:
      path: /etc/chrony.conf
      create: false
      regexp: ^\s*cmdport\s+
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/chrony.conf
    lineinfile:
      path: /etc/chrony.conf
      create: true
      regexp: ^\s*cmdport\s+
      line: cmdport 0
      state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-030742
  - NIST-800-53-CM-7(1)
  - chronyd_no_chronyc_network
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^cmdport")

# shellcheck disable=SC2059
printf -v formatted_output "%s %s" "$stripped_key" "0"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^cmdport\\>" "/etc/chrony.conf"; then
    escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
    LC_ALL=C sed -i --follow-symlinks "s/^cmdport\\>.*/$escaped_formatted_output/gi" "/etc/chrony.conf"
else
    if [[ -s "/etc/chrony.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/chrony.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/chrony.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/chrony.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Configure Time Service Maxpoll Interval   [ref]

The maxpoll should be configured to 16 in /etc/ntp.conf or /etc/chrony.conf to continuously poll time servers. To configure maxpoll in /etc/ntp.conf or /etc/chrony.conf add the following after each `server`, `pool` or `peer` entry:
maxpoll 16
to
server
directives. If using chrony any
pool
directives should be configured too. If no server or pool directives are configured, the rule evaluates to pass.
Rationale:
Inaccurate time stamps make it more difficult to correlate events and can lead to an inaccurate analysis. Determining the correct time a particular event occurred on a system is critical when conducting forensic analysis and investigating system events. Sources outside the configured acceptable allowance (drift) may be inaccurate. Synchronizing internal information system clocks provides uniformity of time stamps for information systems with multiple system clocks and systems connected over a network. Organizations should consider endpoints that may not have regular access to the authoritative time server (e.g., mobile, teleworking, and tactical endpoints).
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_chronyd_or_ntpd_set_maxpoll
Identifiers and References

References:  1, 14, 15, 16, 3, 5, 6, APO11.04, BAI03.05, DSS05.04, DSS05.07, MEA02.01, CCI-001891, CCI-002046, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, CM-6(a), AU-8(1)(b), AU-12(1), PR.PT-1, SRG-OS-000355-GPOS-00143, SRG-OS-000356-GPOS-00144, SRG-OS-000359-GPOS-00146, OL08-00-030740, SV-248820r877038_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-030740
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)(b)
  - NIST-800-53-CM-6(a)
  - chronyd_or_ntpd_set_maxpoll
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_time_service_set_maxpoll # promote to variable
  set_fact:
    var_time_service_set_maxpoll: !!str 16
  tags:
    - always

- name: Check that /etc/ntp.conf exist
  stat:
    path: /etc/ntp.conf
  register: ntp_conf_exist_result
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages )
  tags:
  - DISA-STIG-OL08-00-030740
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)(b)
  - NIST-800-53-CM-6(a)
  - chronyd_or_ntpd_set_maxpoll
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Update the maxpoll values in /etc/ntp.conf
  replace:
    path: /etc/ntp.conf
    regexp: ^(server.*maxpoll)[ ]+[0-9]+(.*)$
    replace: \1 {{ var_time_service_set_maxpoll }}\2
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages )
  - ntp_conf_exist_result.stat.exists
  tags:
  - DISA-STIG-OL08-00-030740
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)(b)
  - NIST-800-53-CM-6(a)
  - chronyd_or_ntpd_set_maxpoll
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set the maxpoll values in /etc/ntp.conf
  replace:
    path: /etc/ntp.conf
    regexp: (^server\s+((?!maxpoll).)*)$
    replace: \1 maxpoll {{ var_time_service_set_maxpoll }}\n
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages )
  - ntp_conf_exist_result.stat.exists
  tags:
  - DISA-STIG-OL08-00-030740
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)(b)
  - NIST-800-53-CM-6(a)
  - chronyd_or_ntpd_set_maxpoll
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Check that /etc/chrony.conf exist
  stat:
    path: /etc/chrony.conf
  register: chrony_conf_exist_result
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages )
  tags:
  - DISA-STIG-OL08-00-030740
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)(b)
  - NIST-800-53-CM-6(a)
  - chronyd_or_ntpd_set_maxpoll
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Get get conf files from /etc/chrony.conf
  shell: |
    set -o pipefail
    CHRONY_NAME=/etc/chrony.conf
    CHRONY_PATH=${CHRONY_NAME%%.*}
    find ${CHRONY_PATH}.* -type f -name '*.conf'
  register: update_chrony_files
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages )
  - chrony_conf_exist_result.stat.exists
  changed_when: false
  tags:
  - DISA-STIG-OL08-00-030740
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)(b)
  - NIST-800-53-CM-6(a)
  - chronyd_or_ntpd_set_maxpoll
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Update the maxpoll values in /etc/chrony.conf
  replace:
    path: '{{ item }}'
    regexp: ^((?:server|pool|peer).*maxpoll)[ ]+[0-9]+(.*)$
    replace: \1 {{ var_time_service_set_maxpoll }}\2
  loop: '{{ update_chrony_files.stdout_lines|list|flatten|unique }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages )
  - chrony_conf_exist_result.stat.exists
  tags:
  - DISA-STIG-OL08-00-030740
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)(b)
  - NIST-800-53-CM-6(a)
  - chronyd_or_ntpd_set_maxpoll
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set the maxpoll values in /etc/chrony.conf
  replace:
    path: '{{ item }}'
    regexp: (^(?:server|pool|peer)\s+((?!maxpoll).)*)$
    replace: \1 maxpoll {{ var_time_service_set_maxpoll }}\n
  loop: '{{ update_chrony_files.stdout_lines|list|flatten|unique }}'
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ( "chrony" in ansible_facts.packages or "ntp" in ansible_facts.packages )
  - chrony_conf_exist_result.stat.exists
  tags:
  - DISA-STIG-OL08-00-030740
  - NIST-800-53-AU-12(1)
  - NIST-800-53-AU-8(1)(b)
  - NIST-800-53-CM-6(a)
  - chronyd_or_ntpd_set_maxpoll
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { ( rpm --quiet -q chrony || rpm --quiet -q ntp ); }; then

var_time_service_set_maxpoll='16'




pof="/usr/sbin/pidof"


CONFIG_FILES="/etc/ntp.conf"
$pof ntpd || {
    CHRONY_NAME=/etc/chrony.conf
    CHRONY_PATH=${CHRONY_NAME%%.*}
    CONFIG_FILES=$(find ${CHRONY_PATH}.* -type f -name '*.conf')
}

# get list of ntp files

for config_file in $CONFIG_FILES; do
    # Set maxpoll values to var_time_service_set_maxpoll
    sed -i "s/^\(\(server\|pool\|peer\).*maxpoll\) [0-9][0-9]*\(.*\)$/\1 $var_time_service_set_maxpoll \3/" "$config_file"
done




for config_file in $CONFIG_FILES; do
    # Add maxpoll to server, pool or peer entries without maxpoll
    grep "^\(server\|pool\|peer\)" "$config_file" | grep -v maxpoll | while read -r line ; do
        sed -i "s/$line/& maxpoll $var_time_service_set_maxpoll/" "$config_file"
    done
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Ensure Chrony is only configured with the server directive   [ref]

Check that Chrony only has time sources configured with the server directive.
Warning:  This rule doesn't come with a remediation, the time source needs to be added by the administrator.
Rationale:
Depending on the infrastructure being used the pool directive may not be supported.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_chronyd_server_directive
Identifiers and References

References:  CCI-001891, SRG-OS-000355-GPOS-00143, SRG-OS-000356-GPOS-00144, SRG-OS-000359-GPOS-00146, OL08-00-030740, SV-248820r877038_rule

Group   Obsolete Services   Group contains 3 groups and 6 rules
[ref]   This section discusses a number of network-visible services which have historically caused problems for system security, and for which disabling or severely limiting the service has been the best available guidance for some time. As a result of this, many of these services are not installed as part of Oracle Linux 8 by default.

Organizations which are running these services should switch to more secure equivalents as soon as possible. If it remains absolutely necessary to run one of these services for legacy reasons, care should be taken to restrict the service as much as possible, for instance by configuring host firewall software such as iptables to restrict access to the vulnerable service to only those remote hosts which have a known need to use it.
Group   Rlogin, Rsh, and Rexec   Group contains 3 rules
[ref]   The Berkeley r-commands are legacy services which allow cleartext remote access and have an insecure trust model.

Rule   Uninstall rsh-server Package   [ref]

The rsh-server package can be removed with the following command:
$ sudo yum erase rsh-server
Rationale:
The rsh-server service provides unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session and has very weak authentication. If a privileged user were to login using this service, the privileged user password could be compromised. The rsh-server package provides several obsolete and insecure network services. Removing it decreases the risk of those services' accidental (or intentional) activation.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_package_rsh-server_removed
Identifiers and References

References:  BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000095-GPOS-00049, OL08-00-040010, SV-248827r780047_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=rsh-server

Complexity:low
Disruption:low
Strategy:disable
include remove_rsh-server

class remove_rsh-server {
  package { 'rsh-server':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure rsh-server is removed
  package:
    name: rsh-server
    state: absent
  tags:
  - DISA-STIG-OL08-00-040010
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - NIST-800-53-IA-5(1)(c)
  - disable_strategy
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - package_rsh-server_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove rsh-server
#	   from the system, and may remove any packages
#	   that depend on rsh-server. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "rsh-server" ; then

    yum remove -y "rsh-server"

fi

Rule   Remove Host-Based Authentication Files   [ref]

The shosts.equiv file list remote hosts and users that are trusted by the local system. To remove these files, run the following command to delete them from any location:
$ sudo rm /[path]/[to]/[file]/shosts.equiv
Rationale:
The shosts.equiv files are used to configure host-based authentication for the system via SSH. Host-based authentication is not sufficient for preventing unauthorized access to the system, as it does not require interactive identification and authentication of a connection request, or for the use of two-factor authentication.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_no_host_based_files
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010460, SV-248597r779357_rule



# Identify local mounts
MOUNT_LIST=$(df --local | awk '{ print $6 }')

# Find file on each listed mount point
for cur_mount in ${MOUNT_LIST}
do
	find ${cur_mount} -xdev -type f -name "shosts.equiv" -exec rm -f {} \;
done

Rule   Remove User Host-Based Authentication Files   [ref]

The ~/.shosts (in each user's home directory) files list remote hosts and users that are trusted by the local system. To remove these files, run the following command to delete them from any location:
$ sudo find / -name '.shosts' -type f -delete
Rationale:
The .shosts files are used to configure host-based authentication for individual users or the system via SSH. Host-based authentication is not sufficient for preventing unauthorized access to the system, as it does not require interactive identification and authentication of a connection request, or for the use of two-factor authentication.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_no_user_host_based_files
Identifiers and References

References:  CCI-000366, SRG-OS-000480-GPOS-00227, OL08-00-010470, SV-248598r779360_rule



# Identify local mounts
MOUNT_LIST=$(df --local | awk '{ print $6 }')

# Find file on each listed mount point
for cur_mount in ${MOUNT_LIST}
do
	find ${cur_mount} -xdev -type f -name ".shosts" -exec rm -f {} \;
done
Group   Telnet   Group contains 1 rule
[ref]   The telnet protocol does not provide confidentiality or integrity for information transmitted on the network. This includes authentication information such as passwords. Organizations which use telnet should be actively working to migrate to a more secure protocol.

Rule   Uninstall telnet-server Package   [ref]

The telnet-server package can be removed with the following command:
$ sudo yum erase telnet-server
Rationale:
It is detrimental for operating systems to provide, or install by default, functionality exceeding requirements or mission objectives. These unnecessary capabilities are often overlooked and therefore may remain unsecure. They increase the risk to the platform by providing additional attack vectors.
The telnet service provides an unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session. If a privileged user were to login using this service, the privileged user password could be compromised.
Removing the telnet-server package decreases the risk of the telnet service's accidental (or intentional) activation.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_package_telnet-server_removed
Identifiers and References

References:  BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, Req-2.2.2, 2.2.4, SRG-OS-000095-GPOS-00049, OL08-00-040000, SV-248823r780035_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=telnet-server

Complexity:low
Disruption:low
Strategy:disable
include remove_telnet-server

class remove_telnet-server {
  package { 'telnet-server':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure telnet-server is removed
  package:
    name: telnet-server
    state: absent
  tags:
  - DISA-STIG-OL08-00-040000
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-2.2.2
  - PCI-DSSv4-2.2.4
  - disable_strategy
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - package_telnet-server_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove telnet-server
#	   from the system, and may remove any packages
#	   that depend on telnet-server. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "telnet-server" ; then

    yum remove -y "telnet-server"

fi
Group   TFTP Server   Group contains 2 rules
[ref]   TFTP is a lightweight version of the FTP protocol which has traditionally been used to configure networking equipment. However, TFTP provides little security, and modern versions of networking operating systems frequently support configuration via SSH or other more secure protocols. A TFTP server should be run only if no more secure method of supporting existing equipment can be found.

Rule   Uninstall tftp-server Package   [ref]

The tftp-server package can be removed with the following command:
 $ sudo yum erase tftp-server
Rationale:
Removing the tftp-server package decreases the risk of the accidental (or intentional) activation of tftp services.

If TFTP is required for operational support (such as transmission of router configurations), its use must be documented with the Information Systems Securty Manager (ISSM), restricted to only authorized personnel, and have access control rules established.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_package_tftp-server_removed
Identifiers and References

References:  BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000318, CCI-000366, CCI-000368, CCI-001812, CCI-001813, CCI-001814, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-040190, SV-248873r780185_rule


Complexity:low
Disruption:low
Strategy:disable

package --remove=tftp-server

Complexity:low
Disruption:low
Strategy:disable
include remove_tftp-server

class remove_tftp-server {
  package { 'tftp-server':
    ensure => 'purged',
  }
}

Complexity:low
Disruption:low
Strategy:disable
- name: Ensure tftp-server is removed
  package:
    name: tftp-server
    state: absent
  tags:
  - DISA-STIG-OL08-00-040190
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - disable_strategy
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - package_tftp-server_removed

Complexity:low
Disruption:low
Strategy:disable

# CAUTION: This remediation script will remove tftp-server
#	   from the system, and may remove any packages
#	   that depend on tftp-server. Execute this
#	   remediation AFTER testing on a non-production
#	   system!

if rpm -q --quiet "tftp-server" ; then

    yum remove -y "tftp-server"

fi

Rule   Ensure tftp Daemon Uses Secure Mode   [ref]

If running the Trivial File Transfer Protocol (TFTP) service is necessary, it should be configured to change its root directory at startup. To do so, ensure /etc/xinetd.d/tftp includes -s as a command line argument, as shown in the following example:
server_args = -s /var/lib/tftpboot
Rationale:
Using the -s option causes the TFTP service to only serve files from the given directory. Serving files from an intentionally-specified directory reduces the risk of sharing files which should remain private.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_tftpd_uses_secure_mode
Identifiers and References

References:  11, 12, 13, 14, 15, 16, 18, 3, 5, 8, 9, APO01.06, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(b), AC-6, CM-7(a), PR.AC-3, PR.AC-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-040350, SV-248902r780272_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-040350
  - NIST-800-53-AC-6
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-7(a)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - tftpd_uses_secure_mode
- name: XCCDF Value var_tftpd_secure_directory # promote to variable
  set_fact:
    var_tftpd_secure_directory: !!str /var/lib/tftpboot
  tags:
    - always

- name: Find out if the file exists and contains the line configuring server arguments
  find:
    path: /etc/xinetd.d
    patterns: tftp
    contains: ^[\s]+server_args.*$
  register: tftpd_secure_config_line
  when: '"tftp-server" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-040350
  - NIST-800-53-AC-6
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-7(a)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - tftpd_uses_secure_mode

- name: Ensure that TFTP server is configured to start with secure directory
  lineinfile:
    path: /etc/xinetd.d/tftp
    regexp: ^[\s]*(server_args[\s]+=[\s]+.*?)(-s[\s]+[/\.\w]+)*(.*)$
    line: \1 -s {{ var_tftpd_secure_directory }} \3
    state: present
    backrefs: true
  when:
  - '"tftp-server" in ansible_facts.packages'
  - tftpd_secure_config_line is defined and tftpd_secure_config_line.matched > 0
  tags:
  - DISA-STIG-OL08-00-040350
  - NIST-800-53-AC-6
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-7(a)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - tftpd_uses_secure_mode

- name: Insert correct config line to start TFTP server with secure directory
  lineinfile:
    path: /etc/xinetd.d/tftp
    line: server_args = -s {{ var_tftpd_secure_directory }}
    state: present
    create: true
  when:
  - '"tftp-server" in ansible_facts.packages'
  - tftpd_secure_config_line is defined and tftpd_secure_config_line.matched == 0
  tags:
  - DISA-STIG-OL08-00-040350
  - NIST-800-53-AC-6
  - NIST-800-53-CM-6(b)
  - NIST-800-53-CM-7(a)
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - tftpd_uses_secure_mode

# Remediation is applicable only in certain platforms
if rpm --quiet -q tftp-server; then

var_tftpd_secure_directory='/var/lib/tftpboot'


if grep -q 'server_args' /etc/xinetd.d/tftp; then
    sed -i -E "s;^([[:blank:]]*server_args[[:blank:]]+=[[:blank:]]+.*?)(-s[[:blank:]]+[[:graph:]]+)*(.*)$;\1 -s $var_tftpd_secure_directory \3;" /etc/xinetd.d/tftp
else
    echo "server_args = -s $var_tftpd_secure_directory" >> /etc/xinetd.d/tftp
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Hardware RNG Entropy Gatherer Daemon   Group contains 1 rule
[ref]   The rngd feeds random data from hardware device to kernel random device.

Rule   Enable the Hardware RNG Entropy Gatherer Service   [ref]

The Hardware RNG Entropy Gatherer service should be enabled. The rngd service can be enabled with the following command:
$ sudo systemctl enable rngd.service
Rationale:
The rngd service feeds random data from hardware device to kernel random device.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_service_rngd_enabled
Identifiers and References

References:  CCI-000366, FCS_RBG_EXT.1, SRG-OS-000480-GPOS-00227, OL08-00-010471, SV-248599r779363_rule



[customizations.services]
enabled = ["rngd"]

Complexity:low
Disruption:low
Strategy:enable
include enable_rngd

class enable_rngd {
  service {'rngd':
    enable => true,
    ensure => 'running',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Enable service rngd
  block:

  - name: Gather the package facts
    package_facts:
      manager: auto

  - name: Enable service rngd
    systemd:
      name: rngd
      enabled: 'yes'
      state: started
      masked: 'no'
    when:
    - '"rng-tools" in ansible_facts.packages'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010471
  - enable_strategy
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - service_rngd_enabled

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'rngd.service'
"$SYSTEMCTL_EXEC" start 'rngd.service'
"$SYSTEMCTL_EXEC" enable 'rngd.service'

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   SSH Server   Group contains 1 group and 21 rules
[ref]   The SSH protocol is recommended for remote login and remote file transfer. SSH provides confidentiality and integrity for data exchanged between two systems, as well as server authentication, through the use of public key cryptography. The implementation included with the system is called OpenSSH, and more detailed documentation is available from its website, https://www.openssh.com. Its server program is called sshd and provided by the RPM package openssh-server.
Group   Configure OpenSSH Server if Necessary   Group contains 16 rules
[ref]   If the system needs to act as an SSH server, then certain changes should be made to the OpenSSH daemon configuration file /etc/ssh/sshd_config. The following recommendations can be applied to this file. See the sshd_config(5) man page for more detailed information.

Rule   Set SSH Client Alive Count Max to zero   [ref]

The SSH server sends at most ClientAliveCountMax messages during a SSH session and waits for a response from the SSH client. The option ClientAliveInterval configures timeout after each ClientAliveCountMax message. If the SSH server does not receive a response from the client, then the connection is considered unresponsive and terminated. To ensure the SSH 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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_set_keepalive_0
Identifiers and References

References:  1, 12, 13, 14, 15, 16, 18, 3, 5, 7, 8, 5.5.6, APO13.01, BAI03.01, BAI03.02, BAI03.03, DSS01.03, DSS03.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.1.11, CCI-000879, CCI-001133, CCI-002361, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 6.2, A.12.4.1, A.12.4.3, A.14.1.1, A.14.2.1, A.14.2.5, A.18.1.4, A.6.1.2, A.6.1.5, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-004-6 R2.2.3, CIP-007-3 R5.1, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, AC-2(5), AC-12, AC-17(a), SC-10, CM-6(a), DE.CM-1, DE.CM-3, PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.IP-2, Req-8.1.8, 8.2.8, SRG-OS-000126-GPOS-00066, SRG-OS-000163-GPOS-00072, SRG-OS-000279-GPOS-00109, OL08-00-010200, SV-248552r860908_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-010200
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_set_keepalive_0

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*ClientAliveCountMax.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-010200
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_set_keepalive_0

- 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
  - DISA-STIG-OL08-00-010200
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_set_keepalive_0

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*ClientAliveCountMax/Id" "$included_file"
done

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

Rule   Set SSH Client Alive Interval   [ref]

SSH allows administrators to set a network responsiveness timeout interval. After this interval has passed, the unresponsive client will be automatically logged out.

To set this timeout interval, edit the following line in /etc/ssh/sshd_config as follows:
ClientAliveInterval 600


The timeout interval is given in seconds. For example, have a timeout of 10 minutes, set interval to 600.

If a shorter timeout has already been set for the login shell, that value will preempt any SSH setting made in /etc/ssh/sshd_config. Keep in mind that some processes may stop SSH from correctly detecting that the user is idle.
Warning:  SSH disconnecting unresponsive clients will not have desired effect without also configuring ClientAliveCountMax in the SSH service configuration.
Warning:  Following conditions may prevent the SSH session to time out:
  • Remote processes on the remote machine generates output. As the output has to be transferred over the network to the client, the timeout is reset every time such transfer happens.
  • Any scp or sftp activity by the same user to the host resets the timeout.
Rationale:
Terminating an idle ssh session within a short time period reduces the window of opportunity for unauthorized personnel to take control of a management session enabled on the console or console port that has been let unattended.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_set_idle_timeout
Identifiers and References

References:  BP28(R29), 1, 12, 13, 14, 15, 16, 18, 3, 5, 7, 8, 5.5.6, APO13.01, BAI03.01, BAI03.02, BAI03.03, DSS01.03, DSS03.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.1.11, CCI-000879, CCI-001133, CCI-002361, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 6.2, A.12.4.1, A.12.4.3, A.14.1.1, A.14.2.1, A.14.2.5, A.18.1.4, A.6.1.2, A.6.1.5, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-004-6 R2.2.3, CIP-007-3 R5.1, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, CM-6(a), AC-17(a), AC-2(5), AC-12, AC-17(a), SC-10, CM-6(a), DE.CM-1, DE.CM-3, PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.IP-2, Req-8.1.8, 8.2.8, SRG-OS-000126-GPOS-00066, SRG-OS-000163-GPOS-00072, SRG-OS-000279-GPOS-00109, SRG-OS-000395-GPOS-00175, OL08-00-010201, SV-248553r860909_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: XCCDF Value sshd_idle_timeout_value # promote to variable
  set_fact:
    sshd_idle_timeout_value: !!str 600
  tags:
    - always

- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-010201
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_set_idle_timeout

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*ClientAliveInterval.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-010201
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_set_idle_timeout

- name: Set SSH Client Alive 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
  - DISA-STIG-OL08-00-010201
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSS-Req-8.1.8
  - PCI-DSSv4-8.2.8
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_set_idle_timeout

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

sshd_idle_timeout_value='600'


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

Rule   Disable SSH Access via Empty Passwords   [ref]

Disallow SSH login with empty passwords. The default SSH configuration disables logins with empty passwords. The appropriate configuration is used if no value is set for PermitEmptyPasswords.
To explicitly disallow SSH login from accounts with empty passwords, add or correct the following line in /etc/ssh/sshd_config:
PermitEmptyPasswords no
Any accounts with empty passwords should be disabled immediately, and PAM configuration should prevent users from being able to assign themselves empty passwords.
Rationale:
Configuring this setting for the SSH daemon provides additional assurance that remote login via SSH will require a password, even in the event of misconfiguration elsewhere.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_sshd_disable_empty_passwords
Identifiers and References

References:  NT007(R17), 11, 12, 13, 14, 15, 16, 18, 3, 5, 9, 5.5.6, APO01.06, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, 3.1.1, 3.1.5, CCI-000366, CCI-000766, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 5.2, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.AC-4, PR.AC-6, PR.DS-5, PR.IP-1, PR.PT-3, FIA_UAU.1, Req-2.2.4, 2.2.6, SRG-OS-000106-GPOS-00053, SRG-OS-000480-GPOS-00229, SRG-OS-000480-GPOS-00227, OL08-00-020330, SV-248714r858582_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-020330
  - NIST-800-171-3.1.1
  - NIST-800-171-3.1.5
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_empty_passwords

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*PermitEmptyPasswords.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-020330
  - NIST-800-171-3.1.1
  - NIST-800-171-3.1.5
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_empty_passwords

- 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
  - DISA-STIG-OL08-00-020330
  - NIST-800-171-3.1.1
  - NIST-800-171-3.1.5
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_empty_passwords

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*PermitEmptyPasswords/Id" "$included_file"
done

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

Rule   Disable GSSAPI Authentication   [ref]

Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms like GSSAPI.
The default SSH configuration disallows authentications based on GSSAPI. The appropriate configuration is used if no value is set for GSSAPIAuthentication.
To explicitly disable GSSAPI authentication, add or correct the following line in /etc/ssh/sshd_config:
GSSAPIAuthentication no
Rationale:
GSSAPI authentication is used to provide additional authentication mechanisms to applications. Allowing GSSAPI authentication through SSH exposes the system's GSSAPI to remote hosts, increasing the attack surface of the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_disable_gssapi_auth
Identifiers and References

References:  11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000318, CCI-000368, CCI-001812, CCI-001813, CCI-001814, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, 0418, 1055, 1402, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, CM-7(a), CM-7(b), CM-6(a), AC-17(a), PR.IP-1, FTP_ITC_EXT.1, FCS_SSH_EXT.1.2, SRG-OS-000364-GPOS-00151, SRG-OS-000480-GPOS-00227, OL08-00-010522, SV-248607r858576_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010522
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_gssapi_auth

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*GSSAPIAuthentication.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010522
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_gssapi_auth

- 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:
  - DISA-STIG-OL08-00-010522
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_gssapi_auth

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*GSSAPIAuthentication/Id" "$included_file"
done

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

Rule   Disable Kerberos Authentication   [ref]

Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms like Kerberos.
The default SSH configuration disallows authentication validation through Kerberos. The appropriate configuration is used if no value is set for KerberosAuthentication.
To explicitly disable Kerberos authentication, add or correct the following line in /etc/ssh/sshd_config:
KerberosAuthentication no
Rationale:
Kerberos authentication for SSH is often implemented using GSSAPI. If Kerberos is enabled through SSH, the SSH daemon provides a means of access to the system's Kerberos implementation. Configuring these settings for the SSH daemon provides additional assurance that remote logon via SSH will not use unused methods of authentication, even in the event of misconfiguration elsewhere.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_disable_kerb_auth
Identifiers and References

References:  11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000318, CCI-000368, CCI-001812, CCI-001813, CCI-001814, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, FTP_ITC_EXT.1, FCS_SSH_EXT.1.2, SRG-OS-000364-GPOS-00151, SRG-OS-000480-GPOS-00227, OL08-00-010521, SV-248606r858574_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010521
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_kerb_auth

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*KerberosAuthentication.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010521
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_kerb_auth

- 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:
  - DISA-STIG-OL08-00-010521
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_kerb_auth

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*KerberosAuthentication/Id" "$included_file"
done

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

Rule   Disable SSH Support for User Known Hosts   [ref]

SSH can allow system users to connect to systems if a cache of the remote systems public keys is available. This should be disabled.

To ensure this behavior is disabled, add or correct the following line in /etc/ssh/sshd_config:
IgnoreUserKnownHosts yes
Rationale:
Configuring this setting for the SSH daemon provides additional assurance that remote login via SSH will require a password, even in the event of misconfiguration elsewhere.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_disable_user_known_hosts
Identifiers and References

References:  11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, FIA_UAU.1, SRG-OS-000480-GPOS-00227, OL08-00-010520, SV-248605r858572_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010520
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_user_known_hosts

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*IgnoreUserKnownHosts.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010520
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_user_known_hosts

- name: Disable SSH Support for User Known Hosts
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/ssh/sshd_config
      create: false
      regexp: (?i)^\s*IgnoreUserKnownHosts\s+
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/ssh/sshd_config
    lineinfile:
      path: /etc/ssh/sshd_config
      create: false
      regexp: (?i)^\s*IgnoreUserKnownHosts\s+
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/ssh/sshd_config
    lineinfile:
      path: /etc/ssh/sshd_config
      create: true
      regexp: (?i)^\s*IgnoreUserKnownHosts\s+
      line: IgnoreUserKnownHosts yes
      state: present
      insertbefore: ^[#\s]*Match
      validate: /usr/sbin/sshd -t -f %s
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010520
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_user_known_hosts

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*IgnoreUserKnownHosts/Id" "$included_file"
done

if [ -e "/etc/ssh/sshd_config" ] ; then
    
    LC_ALL=C sed -i "/^\s*IgnoreUserKnownHosts\s\+/Id" "/etc/ssh/sshd_config"
else
    touch "/etc/ssh/sshd_config"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/ssh/sshd_config"

cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
# Insert 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' "IgnoreUserKnownHosts yes" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "IgnoreUserKnownHosts 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

Rule   Disable X11 Forwarding   [ref]

The X11Forwarding parameter provides the ability to tunnel X11 traffic through the connection to enable remote graphic connections. SSH has the capability to encrypt remote X11 connections when SSH's X11Forwarding option is enabled.
The default SSH configuration disables X11Forwarding. The appropriate configuration is used if no value is set for X11Forwarding.
To explicitly disable X11 Forwarding, add or correct the following line in /etc/ssh/sshd_config:
X11Forwarding no
Rationale:
Disable X11 forwarding unless there is an operational requirement to use X11 applications directly. There is a small risk that the remote X11 servers of users who are logged in via SSH with X11 forwarding could be compromised by other users on the X11 server. Note that even if X11 forwarding is disabled, users can always install their own forwarders.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_disable_x11_forwarding
Identifiers and References

References:  CCI-000366, CM-6(b), 2.2.4, SRG-OS-000480-GPOS-00227, OL08-00-040340, SV-248900r858588_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040340
  - NIST-800-53-CM-6(b)
  - PCI-DSSv4-2.2.4
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_x11_forwarding

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*X11Forwarding.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040340
  - NIST-800-53-CM-6(b)
  - PCI-DSSv4-2.2.4
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_x11_forwarding

- name: Disable X11 Forwarding
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/ssh/sshd_config
      create: false
      regexp: (?i)^\s*X11Forwarding\s+
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/ssh/sshd_config
    lineinfile:
      path: /etc/ssh/sshd_config
      create: false
      regexp: (?i)^\s*X11Forwarding\s+
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/ssh/sshd_config
    lineinfile:
      path: /etc/ssh/sshd_config
      create: true
      regexp: (?i)^\s*X11Forwarding\s+
      line: X11Forwarding no
      state: present
      insertbefore: ^[#\s]*Match
      validate: /usr/sbin/sshd -t -f %s
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040340
  - NIST-800-53-CM-6(b)
  - PCI-DSSv4-2.2.4
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_disable_x11_forwarding

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*X11Forwarding/Id" "$included_file"
done

if [ -e "/etc/ssh/sshd_config" ] ; then
    
    LC_ALL=C sed -i "/^\s*X11Forwarding\s\+/Id" "/etc/ssh/sshd_config"
else
    touch "/etc/ssh/sshd_config"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/ssh/sshd_config"

cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
# Insert 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' "X11Forwarding no" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "X11Forwarding 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

Rule   Do Not Allow SSH Environment Options   [ref]

Ensure that users are not able to override environment variables of the SSH daemon.
The default SSH configuration disables environment processing. The appropriate configuration is used if no value is set for PermitUserEnvironment.
To explicitly disable Environment options, add or correct the following /etc/ssh/sshd_config:
PermitUserEnvironment no
Rationale:
SSH environment options potentially allow users to bypass access restriction in some configurations.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_sshd_do_not_permit_user_env
Identifiers and References

References:  11, 3, 9, 5.5.6, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, Req-2.2.4, 2.2.6, SRG-OS-000480-GPOS-00229, OL08-00-010830, SV-248650r877377_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-010830
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_do_not_permit_user_env

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*PermitUserEnvironment.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-010830
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_do_not_permit_user_env

- 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
  - DISA-STIG-OL08-00-010830
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_do_not_permit_user_env

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*PermitUserEnvironment/Id" "$included_file"
done

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

Rule   Enable Use of Strict Mode Checking   [ref]

SSHs StrictModes option checks file and ownership permissions in the user's home directory .ssh folder before accepting login. If world- writable permissions are found, logon is rejected.
The default SSH configuration has StrictModes enabled. The appropriate configuration is used if no value is set for StrictModes.
To explicitly enable StrictModes in SSH, add or correct the following line in /etc/ssh/sshd_config:
StrictModes yes
Rationale:
If other users have access to modify user-specific SSH configuration files, they may be able to log into the system as another user.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_enable_strictmodes
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.12, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, AC-6, AC-17(a), CM-6(a), PR.AC-4, PR.DS-5, SRG-OS-000480-GPOS-00227, OL08-00-010500, SV-248603r858568_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010500
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6
  - NIST-800-53-CM-6(a)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_enable_strictmodes

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*StrictModes.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010500
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6
  - NIST-800-53-CM-6(a)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_enable_strictmodes

- 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:
  - DISA-STIG-OL08-00-010500
  - NIST-800-171-3.1.12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6
  - NIST-800-53-CM-6(a)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_enable_strictmodes

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*StrictModes/Id" "$included_file"
done

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

Rule   Enable SSH Warning Banner   [ref]

To enable the warning banner and ensure it is consistent across the system, add or correct the following line in /etc/ssh/sshd_config:
Banner /etc/issue
Another section contains information on how to create an appropriate system-wide warning banner.
Rationale:
The warning message reinforces policy awareness during the logon process and facilitates possible legal action against attackers. Alternatively, systems whose ownership should not be obvious should ensure usage of a banner that does not provide easy attribution.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_enable_warning_banner
Identifiers and References

References:  1, 12, 15, 16, 5.5.6, DSS05.04, DSS05.10, DSS06.10, 3.1.9, CCI-000048, CCI-000050, CCI-001384, CCI-001385, CCI-001386, CCI-001387, CCI-001388, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-8(a), AC-8(c), AC-17(a), CM-6(a), PR.AC-7, FTA_TAB.1, Req-2.2.4, 2.2.6, SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088, OL08-00-010040, SV-248526r858561_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-010040
  - NIST-800-171-3.1.9
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-8(a)
  - NIST-800-53-AC-8(c)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_enable_warning_banner

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*Banner.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.6
  - DISA-STIG-OL08-00-010040
  - NIST-800-171-3.1.9
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-8(a)
  - NIST-800-53-AC-8(c)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_enable_warning_banner

- 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
  - DISA-STIG-OL08-00-010040
  - NIST-800-171-3.1.9
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-8(a)
  - NIST-800-53-AC-8(c)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_enable_warning_banner

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*Banner/Id" "$included_file"
done

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

Rule   Enable SSH Print Last Log   [ref]

Ensure that SSH will display the date and time of the last successful account logon.
The default SSH configuration enables print of the date and time of the last login. The appropriate configuration is used if no value is set for PrintLastLog.
To explicitly enable LastLog in SSH, add or correct the following line in /etc/ssh/sshd_config:
PrintLastLog yes
Rationale:
Providing users feedback on when account accesses last occurred facilitates user recognition and reporting of unauthorized account use.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_print_last_log
Identifiers and References

References:  1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, CCI-000052, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-9, AC-9(1), PR.AC-7, SRG-OS-000480-GPOS-00227, OL08-00-020350, SV-248718r858584_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020350
  - NIST-800-53-AC-9
  - NIST-800-53-AC-9(1)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_print_last_log

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*PrintLastLog.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020350
  - NIST-800-53-AC-9
  - NIST-800-53-AC-9(1)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_print_last_log

- 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:
  - DISA-STIG-OL08-00-020350
  - NIST-800-53-AC-9
  - NIST-800-53-AC-9(1)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_print_last_log

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*PrintLastLog/Id" "$included_file"
done

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

Rule   Force frequent session key renegotiation   [ref]

The RekeyLimit parameter specifies how often the session key of the is renegotiated, both in terms of amount of data that may be transmitted and the time elapsed.
To decrease the default limits, add or correct the following line in /etc/ssh/sshd_config:
RekeyLimit 1G 1h
Rationale:
By decreasing the limit based on the amount of data and enabling time-based limit, effects of potential attacks against encryption keys are limited.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_rekey_limit
Identifiers and References

References:  CCI-000068, FCS_SSH_EXT.1.8, SRG-OS-000480-GPOS-00227, SRG-OS-000033-GPOS-00014, OL08-00-040161, SV-248868r877398_rule


Complexity:low
Disruption:low
Strategy:configure
- name: XCCDF Value var_rekey_limit_size # promote to variable
  set_fact:
    var_rekey_limit_size: !!str 1G
  tags:
    - always
- name: XCCDF Value var_rekey_limit_time # promote to variable
  set_fact:
    var_rekey_limit_time: !!str 1h
  tags:
    - always

- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040161
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sshd_rekey_limit

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*RekeyLimit.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040161
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sshd_rekey_limit

- name: Force frequent session key renegotiation
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/ssh/sshd_config
      create: false
      regexp: (?i)^\s*RekeyLimit\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*RekeyLimit\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*RekeyLimit\s+
      line: RekeyLimit {{ var_rekey_limit_size }} {{ var_rekey_limit_time }}
      state: present
      insertbefore: ^[#\s]*Match
      validate: /usr/sbin/sshd -t -f %s
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040161
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - sshd_rekey_limit

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_rekey_limit_size='1G'
var_rekey_limit_time='1h'




# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*RekeyLimit/Id" "$included_file"
done

if [ -e "/etc/ssh/sshd_config" ] ; then
    
    LC_ALL=C sed -i "/^\s*RekeyLimit\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' "RekeyLimit $var_rekey_limit_size $var_rekey_limit_time" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "RekeyLimit $var_rekey_limit_size $var_rekey_limit_time" >> "/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

Rule   Use Only FIPS 140-2 Validated Key Exchange Algorithms   [ref]

Limit the key exchange algorithms to those which are FIPS-approved. Add or modify the following line in /etc/crypto-policies/back-ends/opensshserver.config
CRYPTO_POLICY='-oKexAlgorithms=ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512'
This rule ensures that only the key exchange algorithms mentioned above (or their subset) are configured for use, keeping the given order of algorithms.
Warning:  The system needs to be rebooted for these changes to take effect.
Warning:  This rule doesn't come with a remediation, automatically changing the crypto-policies may be too disruptive.
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 requirements, 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:
DoD information systems are required to use FIPS-approved key exchange algorithms. The system will attempt to use the first algorithm presented by the client that matches the server list. Listing the values "strongest to weakest" is a method to ensure the use of the strongest algorithm available to secure the SSH connection.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_use_approved_kex_ordered_stig
Identifiers and References

References:  CCI-001453, AC-17(2), SRG-OS-000250-GPOS-00093, OL08-00-040342, SV-255898r880544_rule

Rule   SSH server uses strong entropy to seed   [ref]

To set up SSH server to use entropy from a high-quality source, edit the /etc/sysconfig/sshd file. The SSH_USE_STRONG_RNG configuration value determines how many bytes of entropy to use, so make sure that the file contains line
SSH_USE_STRONG_RNG=32
Warning:  This setting can cause problems on computers without the hardware random generator, because insufficient entropy causes the connection to be blocked until enough entropy is available.
Rationale:
SSH implementation in Oracle Linux 8 uses the openssl library, which doesn't use high-entropy sources by default. Randomness is needed to generate data-encryption keys, and as plaintext padding and initialization vectors in encryption algorithms, and high-quality entropy elliminates the possibility that the output of the random number generator used by SSH would be known to potential attackers.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_sshd_use_strong_rng
Identifiers and References

References:  CCI-000366, FCS_RBG_EXT.1.2, SRG-OS-000480-GPOS-00232, SRG-OS-000480-GPOS-00227, OL08-00-010292, SV-248563r779255_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Setting unquoted shell-style assignment of 'SSH_USE_STRONG_RNG' to '32' in
    '/etc/sysconfig/sshd'
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/sysconfig/sshd
      create: false
      regexp: ^\s*SSH_USE_STRONG_RNG=
      state: absent
    check_mode: true
    changed_when: false
    register: dupes

  - name: Deduplicate values from /etc/sysconfig/sshd
    lineinfile:
      path: /etc/sysconfig/sshd
      create: false
      regexp: ^\s*SSH_USE_STRONG_RNG=
      state: absent
    when: dupes.found is defined and dupes.found > 1

  - name: Insert correct line to /etc/sysconfig/sshd
    lineinfile:
      path: /etc/sysconfig/sshd
      create: true
      regexp: ^\s*SSH_USE_STRONG_RNG=
      line: SSH_USE_STRONG_RNG=32
      state: present
      insertbefore: ^# SSH_USE_STRONG_RNG
      validate: /usr/bin/bash -n %s
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010292
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_use_strong_rng

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if [ -e "/etc/sysconfig/sshd" ] ; then
    
    LC_ALL=C sed -i "/^\s*SSH_USE_STRONG_RNG\s*=\s*/d" "/etc/sysconfig/sshd"
else
    touch "/etc/sysconfig/sshd"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/sysconfig/sshd"

cp "/etc/sysconfig/sshd" "/etc/sysconfig/sshd.bak"
# Insert before the line matching the regex '^#\s*SSH_USE_STRONG_RNG'.
line_number="$(LC_ALL=C grep -n "^#\s*SSH_USE_STRONG_RNG" "/etc/sysconfig/sshd.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^#\s*SSH_USE_STRONG_RNG', insert at
    # the end of the file.
    printf '%s\n' "SSH_USE_STRONG_RNG=32" >> "/etc/sysconfig/sshd"
else
    head -n "$(( line_number - 1 ))" "/etc/sysconfig/sshd.bak" > "/etc/sysconfig/sshd"
    printf '%s\n' "SSH_USE_STRONG_RNG=32" >> "/etc/sysconfig/sshd"
    tail -n "+$(( line_number ))" "/etc/sysconfig/sshd.bak" >> "/etc/sysconfig/sshd"
fi
# Clean up after ourselves.
rm "/etc/sysconfig/sshd.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Prevent remote hosts from connecting to the proxy display   [ref]

The SSH daemon should prevent remote hosts from connecting to the proxy display.
The default SSH configuration for X11UseLocalhost is yes, which prevents remote hosts from connecting to the proxy display.
To explicitly prevent remote connections to the proxy display, add or correct the following line in /etc/ssh/sshd_config: X11UseLocalhost yes
Rationale:
When X11 forwarding is enabled, there may be additional exposure to the server and client displays if the sshd proxy display is configured to listen on the wildcard address. By default, sshd binds the forwarding server to the loopback address and sets the hostname part of the DISPLAY environment variable to localhost. This prevents remote hosts from connecting to the proxy display.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sshd_x11_use_localhost
Identifiers and References

References:  CCI-000366, CM-6(b), SRG-OS-000480-GPOS-00227, OL08-00-040341, SV-248901r858590_rule


Complexity:low
Disruption:low
Strategy:restrict
- name: Find sshd_config included files
  shell: |-
    included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
    [[ -n $included_files ]] && ls $included_files || true
  register: sshd_config_included_files
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040341
  - NIST-800-53-CM-6(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_x11_use_localhost

- name: Comment conf from included files
  replace:
    path: '{{ item }}'
    regexp: ^(\s*X11UseLocalhost.*)$
    replace: '# \1'
  loop: '{{ sshd_config_included_files.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040341
  - NIST-800-53-CM-6(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_x11_use_localhost

- name: Prevent remote hosts from connecting to the proxy display
  block:

  - name: Check for duplicate values
    lineinfile:
      path: /etc/ssh/sshd_config
      create: false
      regexp: (?i)^\s*X11UseLocalhost\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*X11UseLocalhost\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*X11UseLocalhost\s+
      line: X11UseLocalhost 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:
  - DISA-STIG-OL08-00-040341
  - NIST-800-53-CM-6(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sshd_x11_use_localhost

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Find the include keyword, extract from the line the glob expression representing included files.
# And if it is a relative path prepend '/etc/ssh/'
included_files=$(grep -oP "^\s*(?i)include.*" /etc/ssh/sshd_config | sed -e 's/Include\s*//i' | sed -e 's|^[^/]|/etc/ssh/&|')
for included_file in ${included_files} ; do
    
    LC_ALL=C sed -i "/^\s*X11UseLocalhost/Id" "$included_file"
done

if [ -e "/etc/ssh/sshd_config" ] ; then
    
    LC_ALL=C sed -i "/^\s*X11UseLocalhost\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' "X11UseLocalhost yes" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "X11UseLocalhost 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

Rule   Install the OpenSSH Server Package   [ref]

The openssh-server package should be installed. The openssh-server package can be installed with the following command:
$ sudo yum install openssh-server
Rationale:
Without protection of the transmitted information, confidentiality, and integrity may be compromised because unprotected communications can be intercepted and either read or altered.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_openssh-server_installed
Identifiers and References

References:  13, 14, APO01.06, DSS05.02, DSS05.04, DSS05.07, DSS06.02, DSS06.06, CCI-002418, CCI-002420, CCI-002421, CCI-002422, SR 3.1, SR 3.8, SR 4.1, SR 4.2, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), PR.DS-2, PR.DS-5, FIA_UAU.5, FTP_ITC_EXT.1, FCS_SSH_EXT.1, FCS_SSHS_EXT.1, SRG-OS-000423-GPOS-00187, SRG-OS-000424-GPOS-00188, SRG-OS-000425-GPOS-00189, SRG-OS-000426-GPOS-00190, OL08-00-040159, SV-248866r853875_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=openssh-server


[[packages]]
name = "openssh-server"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_openssh-server

class install_openssh-server {
  package { 'openssh-server':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure openssh-server is installed
  package:
    name: openssh-server
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040159
  - NIST-800-53-CM-6(a)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_openssh-server_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if ! rpm -q --quiet "openssh-server" ; then
    yum install -y "openssh-server"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable the OpenSSH Service   [ref]

The SSH server service, sshd, is commonly needed. The sshd service can be enabled with the following command:
$ sudo systemctl enable sshd.service
Rationale:
Without protection of the transmitted information, confidentiality, and integrity may be compromised because unprotected communications can be intercepted and either read or altered.

This checklist item applies to both internal and external networks and all types of information system components from which information can be transmitted (e.g., servers, mobile devices, notebook computers, printers, copiers, scanners, etc). Communication paths outside the physical protection of a controlled boundary are exposed to the possibility of interception and modification.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_sshd_enabled
Identifiers and References

References:  13, 14, APO01.06, DSS05.02, DSS05.04, DSS05.07, DSS06.02, DSS06.06, 3.1.13, 3.5.4, 3.13.8, CCI-002418, CCI-002420, CCI-002421, CCI-002422, SR 3.1, SR 3.8, SR 4.1, SR 4.2, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), SC-8, SC-8(1), SC-8(2), SC-8(3), SC-8(4), PR.DS-2, PR.DS-5, SRG-OS-000423-GPOS-00187, SRG-OS-000424-GPOS-00188, SRG-OS-000425-GPOS-00189, SRG-OS-000426-GPOS-00190, OL08-00-040160, SV-248867r853876_rule



[customizations.services]
enabled = ["sshd"]

Complexity:low
Disruption:low
Strategy:enable
include enable_sshd

class enable_sshd {
  service {'sshd':
    enable => true,
    ensure => 'running',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Enable service sshd
  block:

  - name: Gather the package facts
    package_facts:
      manager: auto

  - name: Enable service sshd
    systemd:
      name: sshd
      enabled: 'yes'
      state: started
      masked: 'no'
    when:
    - '"openssh-server" in ansible_facts.packages'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040160
  - NIST-800-171-3.1.13
  - NIST-800-171-3.13.8
  - NIST-800-171-3.5.4
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-8
  - NIST-800-53-SC-8(1)
  - NIST-800-53-SC-8(2)
  - NIST-800-53-SC-8(3)
  - NIST-800-53-SC-8(4)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_sshd_enabled

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'sshd.service'
"$SYSTEMCTL_EXEC" start 'sshd.service'
"$SYSTEMCTL_EXEC" enable 'sshd.service'

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Verify Permissions on SSH Server Private *_key Key Files   [ref]

SSH server private keys - files that match the /etc/ssh/*_key glob, have to have restricted permissions. If those files are owned by the root user and the root group, they have to have the 0600 permission or stricter. If they are owned by the root user, but by a dedicated group ssh_keys, they can have the 0640 permission or stricter.
Rationale:
If an unauthorized user obtains the private SSH host key file, the host could be impersonated.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_sshd_private_key
Identifiers and References

References:  BP28(R36), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.13, 3.13.10, CCI-000366, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, AC-17(a), CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, Req-2.2.4, 2.2.6, SRG-OS-000480-GPOS-00227, OL08-00-010490, SV-248602r880541_rule


include ssh_private_key_perms

class ssh_private_key_perms {
  exec { 'sshd_priv_key':
    command => "chmod 0640 /etc/ssh/*_key",
    path    => '/bin:/usr/bin'
  }
}

Complexity:low
Disruption:low
Strategy:configure
- name: Find root:root-owned keys
  ansible.builtin.command: find -H /etc/ssh/ -maxdepth 1 -user root -regex ".*_key$"
    -type f -group root -perm /u+xs,g+xwrs,o+xwrt
  register: root_owned_keys
  changed_when: false
  failed_when: false
  check_mode: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010490
  - NIST-800-171-3.1.13
  - NIST-800-171-3.13.10
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_sshd_private_key
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for root:root-owned keys
  ansible.builtin.file:
    path: '{{ item }}'
    mode: u-xs,g-xwrs,o-xwrt
    state: file
  with_items:
  - '{{ root_owned_keys.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010490
  - NIST-800-171-3.1.13
  - NIST-800-171-3.13.10
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_sshd_private_key
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Find root:ssh_keys-owned keys
  ansible.builtin.command: find -H /etc/ssh/ -maxdepth 1 -user root -regex ".*_key$"
    -type f -group ssh_keys -perm /u+xs,g+xws,o+xwrt
  register: dedicated_group_owned_keys
  changed_when: false
  failed_when: false
  check_mode: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010490
  - NIST-800-171-3.1.13
  - NIST-800-171-3.13.10
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_sshd_private_key
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for root:ssh_keys-owned keys
  ansible.builtin.file:
    path: '{{ item }}'
    mode: u-xs,g-xws,o-xwrt
    state: file
  with_items:
  - '{{ dedicated_group_owned_keys.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010490
  - NIST-800-171-3.1.13
  - NIST-800-171-3.13.10
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_sshd_private_key
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

for keyfile in /etc/ssh/*_key; do
    test -f "$keyfile" || continue
    if test root:root = "$(stat -c "%U:%G" "$keyfile")"; then
    
	chmod u-xs,g-xwrs,o-xwrt "$keyfile"
    
    elif test root:ssh_keys = "$(stat -c "%U:%G" "$keyfile")"; then
	chmod u-xs,g-xws,o-xwrt "$keyfile"
    else
        echo "Key-like file '$keyfile' is owned by an unexpected user:group combination"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Verify Permissions on SSH Server Public *.pub Key Files   [ref]

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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_sshd_pub_key
Identifiers and References

References:  12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.13, 3.13.10, CCI-000366, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, AC-17(a), CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, Req-2.2.4, 2.2.6, SRG-OS-000480-GPOS-00227, OL08-00-010480, SV-248601r779369_rule


include ssh_public_key_perms

class ssh_public_key_perms {
  exec { 'sshd_pub_key':
    command => "chmod 0644 /etc/ssh/*.pub",
    path    => '/bin:/usr/bin'
  }
}

Complexity:low
Disruption:low
Strategy:configure
- name: Find /etc/ssh/ file(s)
  command: find -H /etc/ssh/ -maxdepth 1 -perm /u+xs,g+xws,o+xwt  -type f -regex "^.*\.pub$"
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010480
  - NIST-800-171-3.1.13
  - NIST-800-171-3.13.10
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_sshd_pub_key
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /etc/ssh/ file(s)
  file:
    path: '{{ item }}'
    mode: u-xs,g-xws,o-xwt
    state: file
  with_items:
  - '{{ files_found.stdout_lines }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-010480
  - NIST-800-171-3.1.13
  - NIST-800-171-3.13.10
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-2.2.4
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_sshd_pub_key
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

find -H /etc/ssh/ -maxdepth 1 -perm /u+xs,g+xws,o+xwt  -type f -regex '^.*\.pub$' -exec chmod u-xs,g-xws,o-xwt {} \;

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   OpenSSH Service Must Use Passcode for Their Private Keys   [ref]

Verify the SSH private key files have a passcode. For each private key stored on the system, use the following command:
$ sudo ssh-keygen -y -f /path/to/file
If the contents of the key are displayed, without asking a passphrase this is a finding.
Rationale:
If an unauthorized user obtains access to a private key without a passcode, that user would have unauthorized access to any system where the associated public key has been installed.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_ssh_private_keys_have_passcode
Identifiers and References

References:  CCI-000186, IA-5(2), IA-5(2).1, SRG-OS-000067-GPOS-00035, OL08-00-010100, SV-248532r779162_rule

Group   System Security Services Daemon   Group contains 5 rules
[ref]   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 https://docs.oracle.com/en/operating-systems/oracle-linux/8/userauth/userauth-UsingtheSystemSecurityServicesDaemon.html#sssd-auth

Rule   Certificate status checking in SSSD   [ref]

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. Configuring certificate_verification to ocsp_dgst=sha1 ensures that certificates for multifactor solutions are checked via Online Certificate Status Protocol (OCSP).
Rationale:
Ensuring that multifactor solutions certificates are checked via Online Certificate Status Protocol (OCSP) ensures the security of the system.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sssd_certificate_verification
Identifiers and References

References:  CCI-001948, CCI-001954, IA-2(11), SRG-OS-000375-GPOS-00160, SRG-OS-000377-GPOS-00162, OL08-00-010400, SV-248587r860917_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-010400
  - NIST-800-53-IA-2(11)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_certificate_verification
- name: XCCDF Value var_sssd_certificate_verification_digest_function # promote to variable
  set_fact:
    var_sssd_certificate_verification_digest_function: !!str sha1
  tags:
    - always

- name: Ensure that "certificate_verification" is not set in /etc/sssd/sssd.conf
  ini_file:
    path: /etc/sssd/sssd.conf
    section: sssd
    option: certificate_verification
    state: absent
  when: '"sssd-common" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010400
  - NIST-800-53-IA-2(11)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_certificate_verification

- name: Ensure that "certificate_verification" is not set in  /etc/sssd/conf.d/*.conf
  ini_file:
    path: /etc/sssd/conf.d/*.conf
    section: sssd
    option: certificate_verification
    state: absent
  when: '"sssd-common" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010400
  - NIST-800-53-IA-2(11)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_certificate_verification

- name: Ensure that "certificate_verification" is set
  ini_file:
    path: /etc/sssd/conf.d/certificate_verification.conf
    section: sssd
    option: certificate_verification
    value: ocsp_dgst = {{ var_sssd_certificate_verification_digest_function }}
    state: present
  when: '"sssd-common" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-010400
  - NIST-800-53-IA-2(11)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_certificate_verification

Complexity:low
Disruption:medium
Strategy:configure
# Remediation is applicable only in certain platforms
if rpm --quiet -q sssd-common; then

var_sssd_certificate_verification_digest_function='sha1'


MAIN_CONF="/etc/sssd/conf.d/certificate_verification.conf"

found=false

# set value in all files if they contain section or key
for f in $(echo -n "$MAIN_CONF /etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf"); do
    if [ ! -e "$f" ]; then
        continue
    fi

    # find key in section and change value
    if grep -qzosP "[[:space:]]*\[sssd\]([^\n\[]*\n+)+?[[:space:]]*certificate_verification" "$f"; then
            sed -i "s/certificate_verification[^(\n)]*/certificate_verification = ocsp_dgst = $var_sssd_certificate_verification_digest_function/" "$f"
            found=true

    # find section and add key = value to it
    elif grep -qs "[[:space:]]*\[sssd\]" "$f"; then
            sed -i "/[[:space:]]*\[sssd\]/a certificate_verification = ocsp_dgst = $var_sssd_certificate_verification_digest_function" "$f"
            found=true
    fi
done

# if section not in any file, append section with key = value to FIRST file in files parameter
if ! $found ; then
    file=$(echo "$MAIN_CONF /etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf" | cut -f1 -d ' ')
    mkdir -p "$(dirname "$file")"
    echo -e "[sssd]\ncertificate_verification = ocsp_dgst = $var_sssd_certificate_verification_digest_function" >> "$file"
fi

if [ -e "$MAIN_CONF" ]; then
    chown root:root "$MAIN_CONF"
	chmod 600 "$MAIN_CONF"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable Certmap in SSSD   [ref]

SSSD should be configured to verify the certificate of the user or group. To set this up ensure that section like certmap/testing.test/rule_name is setup in /etc/sssd/sssd.conf. For example
[certmap/testing.test/rule_name]
matchrule =<SAN>.*EDIPI@mil
maprule = (userCertificate;binary={cert!bin})
domains = testing.test
Warning:  Automatic remediation of this control is not available, since all of the settings in in the certmap need to be customized.
Rationale:
Without mapping the certificate used to authenticate to the user account, the ability to determine the identity of the individual user or group will not be available for forensic analysis.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sssd_enable_certmap
Identifiers and References

References:  CCI-000187, IA-5 (2) (c), SRG-OS-000068-GPOS-00036, OL08-00-020090, SV-248685r858606_rule

Rule   Enable Smartcards in SSSD   [ref]

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
Add or update "pam_sss.so" line in auth section of "/etc/pam.d/system-auth" file to include "try_cert_auth" or "require_cert_auth" option, like in the following example:
/etc/pam.d/system-auth:auth [success=done authinfo_unavail=ignore ignore=ignore default=die] pam_sss.so try_cert_auth
Also add or update "pam_sss.so" line in auth section of "/etc/pam.d/smartcard-auth" file to include the "allow_missing_name" option, like in the following example:
/etc/pam.d/smartcard-auth:auth sufficient pam_sss.so allow_missing_name
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.

Multi-Factor Authentication (MFA) 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.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sssd_enable_smartcards
Identifiers and References

References:  CCI-001954, CCI-000765, CCI-000766, CCI-000767, CCI-000768, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, Req-8.3, 8.4, SRG-OS-000375-GPOS-00160, SRG-OS-000105-GPOS-00052, SRG-OS-000106-GPOS-00053, SRG-OS-000107-GPOS-00054, SRG-OS-000108-GPOS-00055, OL08-00-020250, SV-248702r779672_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020250
  - PCI-DSS-Req-8.3
  - PCI-DSSv4-8.4
  - 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:
  - DISA-STIG-OL08-00-020250
  - PCI-DSS-Req-8.3
  - PCI-DSSv4-8.4
  - 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:
  - DISA-STIG-OL08-00-020250
  - PCI-DSS-Req-8.3
  - PCI-DSSv4-8.4
  - 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:
  - DISA-STIG-OL08-00-020250
  - PCI-DSS-Req-8.3
  - PCI-DSSv4-8.4
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_enable_smartcards

- name: Enable Smartcards in SSSD - Check if system relies on authselect
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when:
  - '"sssd-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020250
  - PCI-DSS-Req-8.3
  - PCI-DSSv4-8.4
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_enable_smartcards

- name: Enable Smartcards in SSSD - Remediate using authselect
  block:

  - name: Enable Smartcards in SSSD - Check integrity of authselect current profile
    ansible.builtin.command:
      cmd: authselect check
    register: result_authselect_check_cmd
    changed_when: false
    failed_when: false

  - name: Enable Smartcards in SSSD - Informative message based on the authselect
      integrity check result
    ansible.builtin.assert:
      that:
      - result_authselect_check_cmd.rc == 0
      fail_msg:
      - authselect integrity check failed. Remediation aborted!
      - This remediation could not be applied because an authselect profile was not
        selected or the selected profile is not intact.
      - It is not recommended to manually edit the PAM files when authselect tool
        is available.
      - In cases where the default authselect profile does not cover a specific demand,
        a custom authselect profile is recommended.
      success_msg:
      - authselect integrity check passed

  - name: Enable Smartcards in SSSD - Get authselect current features
    ansible.builtin.shell:
      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
    register: result_authselect_features
    changed_when: false
    when:
    - result_authselect_check_cmd is success

  - name: Enable Smartcards in SSSD - Ensure "with-smartcard" feature is enabled using
      authselect tool
    ansible.builtin.command:
      cmd: authselect enable-feature with-smartcard
    register: result_authselect_enable_feature_cmd
    when:
    - result_authselect_check_cmd is success
    - result_authselect_features.stdout is not search("with-smartcard")

  - name: Enable Smartcards in SSSD - Ensure authselect changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_enable_feature_cmd is not skipped
    - result_authselect_enable_feature_cmd is success
  when:
  - '"sssd-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - result_authselect_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020250
  - PCI-DSS-Req-8.3
  - PCI-DSSv4-8.4
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_enable_smartcards

- name: Enable Smartcards in SSSD - Remediate by directly editing PAM files
  block:

  - name: Enable Smartcards in SSSD - Check if expected PAM module line is present
      in /etc/pam.d/smartcard-auth
    ansible.builtin.lineinfile:
      path: /etc/pam.d/smartcard-auth
      regexp: ^\s*auth\s+sufficient\s+pam_sss.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

  - name: Enable Smartcards in SSSD - Include or update the PAM module line in /etc/pam.d/smartcard-auth
    block:

    - name: Enable Smartcards in SSSD - Check if required PAM module line is present
        in /etc/pam.d/smartcard-auth with different control
      ansible.builtin.lineinfile:
        path: /etc/pam.d/smartcard-auth
        regexp: ^\s*auth\s+.*\s+pam_sss.so\s*
        state: absent
      check_mode: true
      changed_when: false
      register: result_pam_line_other_control_present

    - name: Enable Smartcards in SSSD - Ensure the correct control for the required
        PAM module line in /etc/pam.d/smartcard-auth
      ansible.builtin.replace:
        dest: /etc/pam.d/smartcard-auth
        regexp: ^(\s*auth\s+).*(\bpam_sss.so.*)
        replace: \1sufficient \2
      register: result_pam_module_edit
      when:
      - result_pam_line_other_control_present.found == 1

    - name: Enable Smartcards in SSSD - Ensure the required PAM module line is included
        in /etc/pam.d/smartcard-auth
      ansible.builtin.lineinfile:
        dest: /etc/pam.d/smartcard-auth
        line: auth    sufficient    pam_sss.so
      register: result_pam_module_add
      when:
      - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
        > 1

    - name: Enable Smartcards in SSSD - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present is defined
      - result_authselect_present.stat.exists
      - |-
        (result_pam_module_add is defined and result_pam_module_add.changed)
         or (result_pam_module_edit is defined and result_pam_module_edit.changed)
    when:
    - result_pam_line_present.found is defined
    - result_pam_line_present.found == 0

  - name: Enable Smartcards in SSSD - Check if the required PAM module option is present
      in /etc/pam.d/smartcard-auth
    ansible.builtin.lineinfile:
      path: /etc/pam.d/smartcard-auth
      regexp: ^\s*auth\s+sufficient\s+pam_sss.so\s*.*\sallow_missing_name\b
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_module_allow_missing_name_option_present

  - name: Enable Smartcards in SSSD - Ensure the "allow_missing_name" PAM option for
      "pam_sss.so" is included in /etc/pam.d/smartcard-auth
    ansible.builtin.lineinfile:
      path: /etc/pam.d/smartcard-auth
      backrefs: true
      regexp: ^(\s*auth\s+sufficient\s+pam_sss.so.*)
      line: \1 allow_missing_name
      state: present
    register: result_pam_allow_missing_name_add
    when:
    - result_pam_module_allow_missing_name_option_present.found == 0

  - name: Enable Smartcards in SSSD - Check if expected PAM module line is present
      in /etc/pam.d/system-auth
    ansible.builtin.lineinfile:
      path: /etc/pam.d/system-auth
      regexp: ^\s*auth\s+\[success=done authinfo_unavail=ignore ignore=ignore default=die\]\s+pam_sss.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

  - name: Enable Smartcards in SSSD - Include or update the PAM module line in /etc/pam.d/system-auth
    block:

    - name: Enable Smartcards in SSSD - Check if required PAM module line is present
        in /etc/pam.d/system-auth with different control
      ansible.builtin.lineinfile:
        path: /etc/pam.d/system-auth
        regexp: ^\s*auth\s+.*\s+pam_sss.so\s*
        state: absent
      check_mode: true
      changed_when: false
      register: result_pam_line_other_control_present

    - name: Enable Smartcards in SSSD - Ensure the correct control for the required
        PAM module line in /etc/pam.d/system-auth
      ansible.builtin.replace:
        dest: /etc/pam.d/system-auth
        regexp: ^(\s*auth\s+).*(\bpam_sss.so.*)
        replace: \1\[success=done authinfo_unavail=ignore ignore=ignore default=die\]
          \2
      register: result_pam_module_edit
      when:
      - result_pam_line_other_control_present.found == 1

    - name: Enable Smartcards in SSSD - Ensure the required PAM module line is included
        in /etc/pam.d/system-auth
      ansible.builtin.lineinfile:
        dest: /etc/pam.d/system-auth
        line: auth    \[success=done authinfo_unavail=ignore ignore=ignore default=die\]    pam_sss.so
      register: result_pam_module_add
      when:
      - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
        > 1

    - name: Enable Smartcards in SSSD - Ensure authselect changes are applied
      ansible.builtin.command:
        cmd: authselect apply-changes -b
      when:
      - result_authselect_present is defined
      - result_authselect_present.stat.exists
      - |-
        (result_pam_module_add is defined and result_pam_module_add.changed)
         or (result_pam_module_edit is defined and result_pam_module_edit.changed)
    when:
    - result_pam_line_present.found is defined
    - result_pam_line_present.found == 0

  - name: Enable Smartcards in SSSD - Check if the required PAM module option is present
      in /etc/pam.d/system-auth
    ansible.builtin.lineinfile:
      path: /etc/pam.d/system-auth
      regexp: ^\s*auth\s+\[success=done authinfo_unavail=ignore ignore=ignore default=die\]\s+pam_sss.so\s*.*\stry_cert_auth\b
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_module_try_cert_auth_option_present

  - name: Enable Smartcards in SSSD - Ensure the "try_cert_auth" PAM option for "pam_sss.so"
      is included in /etc/pam.d/system-auth
    ansible.builtin.lineinfile:
      path: /etc/pam.d/system-auth
      backrefs: true
      regexp: ^(\s*auth\s+\[success=done authinfo_unavail=ignore ignore=ignore default=die\]\s+pam_sss.so.*)
      line: \1 try_cert_auth
      state: present
    register: result_pam_try_cert_auth_add
    when:
    - result_pam_module_try_cert_auth_option_present.found == 0
  when:
  - '"sssd-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - not result_authselect_present.stat.exists
  tags:
  - DISA-STIG-OL08-00-020250
  - PCI-DSS-Req-8.3
  - PCI-DSSv4-8.4
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_enable_smartcards

Complexity:low
Disruption:medium
Strategy:configure
# Remediation is applicable only in certain platforms
if rpm --quiet -q sssd-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

found=false

# set value in all files if they contain section or key
for f in $(echo -n "/etc/sssd/sssd.conf"); do
    if [ ! -e "$f" ]; then
        continue
    fi

    # find key in section and change value
    if grep -qzosP "[[:space:]]*\[pam\]([^\n\[]*\n+)+?[[:space:]]*pam_cert_auth" "$f"; then
            sed -i "s/pam_cert_auth[^(\n)]*/pam_cert_auth = True/" "$f"
            found=true

    # find section and add key = value to it
    elif grep -qs "[[:space:]]*\[pam\]" "$f"; then
            sed -i "/[[:space:]]*\[pam\]/a pam_cert_auth = True" "$f"
            found=true
    fi
done

# if section not in any file, append section with key = value to FIRST file in files parameter
if ! $found ; then
    file=$(echo "/etc/sssd/sssd.conf" | cut -f1 -d ' ')
    mkdir -p "$(dirname "$file")"
    echo -e "[pam]\npam_cert_auth = True" >> "$file"
fi


if [ -f /usr/bin/authselect ]; then
    if authselect check; then
        if ! authselect check; then
        echo "
        authselect integrity check failed. Remediation aborted!
        This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
        It is not recommended to manually edit the PAM files when authselect tool is available.
        In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        exit 1
        fi
        authselect enable-feature with-smartcard

        authselect apply-changes -b
    fi
else
    if ! grep -qP '^\s*auth\s+'"sufficient"'\s+pam_sss.so\s*.*' "/etc/pam.d/smartcard-auth"; then
        # Line matching group + control + module was not found. Check group + module.
        if [ "$(grep -cP '^\s*auth\s+.*\s+pam_sss.so\s*' "/etc/pam.d/smartcard-auth")" -eq 1 ]; then
            # The control is updated only if one single line matches.
            sed -i -E --follow-symlinks 's/^(\s*auth\s+).*(\bpam_sss.so.*)/\1'"sufficient"' \2/' "/etc/pam.d/smartcard-auth"
        else
            echo 'auth    '"sufficient"'    pam_sss.so' >> "/etc/pam.d/smartcard-auth"
        fi
    fi
    # Check the option
    if ! grep -qP '^\s*auth\s+'"sufficient"'\s+pam_sss.so\s*.*\sallow_missing_name\b' "/etc/pam.d/smartcard-auth"; then
        sed -i -E --follow-symlinks '/\s*auth\s+'"sufficient"'\s+pam_sss.so.*/ s/$/ allow_missing_name/' "/etc/pam.d/smartcard-auth"
    fi
    if ! grep -qP '^\s*auth\s+'"\[success=done authinfo_unavail=ignore ignore=ignore default=die\]"'\s+pam_sss.so\s*.*' "/etc/pam.d/system-auth"; then
        # Line matching group + control + module was not found. Check group + module.
        if [ "$(grep -cP '^\s*auth\s+.*\s+pam_sss.so\s*' "/etc/pam.d/system-auth")" -eq 1 ]; then
            # The control is updated only if one single line matches.
            sed -i -E --follow-symlinks 's/^(\s*auth\s+).*(\bpam_sss.so.*)/\1'"\[success=done authinfo_unavail=ignore ignore=ignore default=die\]"' \2/' "/etc/pam.d/system-auth"
        else
            echo 'auth    '"\[success=done authinfo_unavail=ignore ignore=ignore default=die\]"'    pam_sss.so' >> "/etc/pam.d/system-auth"
        fi
    fi
    # Check the option
    if ! grep -qP '^\s*auth\s+'"\[success=done authinfo_unavail=ignore ignore=ignore default=die\]"'\s+pam_sss.so\s*.*\stry_cert_auth\b' "/etc/pam.d/system-auth"; then
        sed -i -E --follow-symlinks '/\s*auth\s+'"\[success=done authinfo_unavail=ignore ignore=ignore default=die\]"'\s+pam_sss.so.*/ s/$/ try_cert_auth/' "/etc/pam.d/system-auth"
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   SSSD Has a Correct Trust Anchor   [ref]

SSSD must have acceptable trust anchor present.
Warning:  Automatic remediation of this control is not available.
Rationale:
Without path validation, an informed trust decision by the relying party cannot be made when presented with any certificate not already explicitly trusted. A trust anchor is an authoritative entity represented via a public key and associated data. It is used in the context of public key infrastructures, X.509 digital certificates, and DNSSEC. When there is a chain of trust, usually the top entity to be trusted becomes the trust anchor; it can be, for example, a Certification Authority (CA). A certification path starts with the subject certificate and proceeds through a number of intermediate certificates up to a trusted root certificate, typically issued by a trusted CA. This requirement verifies that a certification path to an accepted trust anchor is used for certificate validation and that the path includes status information. Path validation is necessary for a relying party to make an informed trust decision when presented with any certificate not already explicitly trusted. Status information for certification paths includes certificate revocation lists or online certificate status protocol responses. Validation of the certificate status information is out of scope for this requirement.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sssd_has_trust_anchor
Identifiers and References

References:  CCI-000185, IA-5 (2) (a), SRG-OS-000066-GPOS-00034, SRG-OS-000384-GPOS-00167, OL08-00-010090, SV-248531r860907_rule

Rule   Configure SSSD to Expire Offline Credentials   [ref]

SSSD should be configured to expire offline credentials after 1 day. Check if SSSD allows cached authentications with the following command:
$ sudo grep cache_credentials /etc/sssd/sssd.conf
cache_credentials = true
If "cache_credentials" is set to "false" or is missing no further checks are required.
To configure SSSD to expire offline credentials, set offline_credentials_expiration to 1 under the [pam] section in /etc/sssd/sssd.conf. For example:
[pam]
offline_credentials_expiration = 1
Rationale:
If cached authentication information is out-of-date, the validity of the authentication information may be questionable.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sssd_offline_cred_expiration
Identifiers and References

References:  1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-002007, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), IA-5(13), PR.AC-1, PR.AC-6, PR.AC-7, SRG-OS-000383-GPOS-00166, OL08-00-020290, SV-248710r853793_rule


Complexity:low
Disruption:medium
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-020290
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(13)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_offline_cred_expiration

- 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:
  - DISA-STIG-OL08-00-020290
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(13)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_offline_cred_expiration

- 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:
  - DISA-STIG-OL08-00-020290
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(13)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_offline_cred_expiration

- name: Configure SSD to Expire Offline Credentials
  ini_file:
    dest: /etc/sssd/sssd.conf
    section: pam
    option: offline_credentials_expiration
    value: 1
    create: true
    mode: 384
  when:
  - '"sssd-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-020290
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(13)
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - sssd_offline_cred_expiration

Complexity:low
Disruption:medium
Strategy:configure
# Remediation is applicable only in certain platforms
if rpm --quiet -q sssd-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

found=false

# set value in all files if they contain section or key
for f in $(echo -n "/etc/sssd/sssd.conf"); do
    if [ ! -e "$f" ]; then
        continue
    fi

    # find key in section and change value
    if grep -qzosP "[[:space:]]*\[pam\]([^\n\[]*\n+)+?[[:space:]]*offline_credentials_expiration" "$f"; then
            sed -i "s/offline_credentials_expiration[^(\n)]*/offline_credentials_expiration = 1/" "$f"
            found=true

    # find section and add key = value to it
    elif grep -qs "[[:space:]]*\[pam\]" "$f"; then
            sed -i "/[[:space:]]*\[pam\]/a offline_credentials_expiration = 1" "$f"
            found=true
    fi
done

# if section not in any file, append section with key = value to FIRST file in files parameter
if ! $found ; then
    file=$(echo "/etc/sssd/sssd.conf" | cut -f1 -d ' ')
    mkdir -p "$(dirname "$file")"
    echo -e "[pam]\noffline_credentials_expiration = 1" >> "$file"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   USBGuard daemon   Group contains 4 rules
[ref]   The USBGuard daemon enforces the USB device authorization policy for all USB devices.

Rule   Install usbguard Package   [ref]

The usbguard package can be installed with the following command:
$ sudo yum install usbguard
Rationale:
usbguard is a software framework that helps to protect against rogue USB devices by implementing basic whitelisting/blacklisting capabilities based on USB device attributes.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_usbguard_installed
Identifiers and References

References:  CCI-001958, 1418, CM-8(3), IA-3, SRG-OS-000378-GPOS-00163, OL08-00-040139, SV-248862r853871_rule


Complexity:low
Disruption:low
Strategy:enable

package --add=usbguard

---
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
spec:
  config:
    ignition:
      version: 3.1.0
  extensions:
    - usbguard


[[packages]]
name = "usbguard"
version = "*"

Complexity:low
Disruption:low
Strategy:enable
include install_usbguard

class install_usbguard {
  package { 'usbguard':
    ensure => 'installed',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Ensure usbguard is installed
  package:
    name: usbguard
    state: present
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and ansible_architecture != "s390x" )
  tags:
  - DISA-STIG-OL08-00-040139
  - NIST-800-53-CM-8(3)
  - NIST-800-53-IA-3
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_usbguard_installed

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && ! grep -q s390x /proc/sys/kernel/osrelease ); then

if ! rpm -q --quiet "usbguard" ; then
    yum install -y "usbguard"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Enable the USBGuard Service   [ref]

The USBGuard service should be enabled. The usbguard service can be enabled with the following command:
$ sudo systemctl enable usbguard.service
Rationale:
The usbguard service must be running in order to enforce the USB device authorization policy for all USB devices.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_service_usbguard_enabled
Identifiers and References

References:  CCI-000416, CCI-001958, 1418, CM-8(3)(a), IA-3, FMT_SMF_EXT.1, SRG-OS-000378-GPOS-00163, OL08-00-040141, SV-248864r853873_rule


---
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
metadata:
  annotations:
    complianceascode.io/depends-on: xccdf_org.ssgproject.content_rule_package_usbguard_installed
spec:
  config:
    ignition:
      version: 3.1.0
    systemd:
      units:
      - name: usbguard.service
        enabled: true


[customizations.services]
enabled = ["usbguard"]

Complexity:low
Disruption:low
Strategy:enable
include enable_usbguard

class enable_usbguard {
  service {'usbguard':
    enable => true,
    ensure => 'running',
  }
}

Complexity:low
Disruption:low
Strategy:enable
- name: Enable service usbguard
  block:

  - name: Gather the package facts
    package_facts:
      manager: auto

  - name: Enable service usbguard
    systemd:
      name: usbguard
      enabled: 'yes'
      state: started
      masked: 'no'
    when:
    - '"usbguard" in ansible_facts.packages'
  when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
    "container"] and ansible_architecture != "s390x" )
  tags:
  - DISA-STIG-OL08-00-040141
  - NIST-800-53-CM-8(3)(a)
  - NIST-800-53-IA-3
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - service_usbguard_enabled

Complexity:low
Disruption:low
Strategy:enable
# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && ! grep -q s390x /proc/sys/kernel/osrelease ); then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'usbguard.service'
"$SYSTEMCTL_EXEC" start 'usbguard.service'
"$SYSTEMCTL_EXEC" enable 'usbguard.service'

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Log USBGuard daemon audit events using Linux Audit   [ref]

To configure USBGuard daemon to log via Linux Audit (as opposed directly to a file), AuditBackend option in /etc/usbguard/usbguard-daemon.conf needs to be set to LinuxAudit.
Rationale:
Using the Linux Audit logging allows for centralized trace of events.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_configure_usbguard_auditbackend
Identifiers and References

References:  CCI-000169, CCI-000172, AU-2, CM-8(3), IA-3, FMT_SMF_EXT.1, SRG-OS-000062-GPOS-00031, SRG-OS-000471-GPOS-00215, OL08-00-030603, SV-248805r779981_rule


---
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
metadata:
  annotations:
    complianceascode.io/depends-on: xccdf_org.ssgproject.content_rule_package_usbguard_installed
    complianceascode.io/ocp-version: '>=4.7.0'
spec:
  config:
    ignition:
      version: 3.1.0
    storage:
      files:
      - contents:
          source: data:,{{ %0A%23%0A%23%20Rule%20set%20file%20path.%0A%23%0A%23%20The%20USBGuard%20daemon%20will%20use%20this%20file%20to%20load%20the%20policy%0A%23%20rule%20set%20from%20it%20and%20to%20write%20new%20rules%20received%20via%20the%0A%23%20IPC%20interface.%0A%23%0A%23%20RuleFile%3D/path/to/rules.conf%0A%23%0ARuleFile%3D/etc/usbguard/rules.conf%0A%0A%23%0A%23%20Rule%20set%20folder%20path.%0A%23%0A%23%20The%20USBGuard%20daemon%20will%20use%20this%20folder%20to%20load%20the%20policy%0A%23%20rule%20set%20from%20it%20and%20to%20write%20new%20rules%20received%20via%20the%0A%23%20IPC%20interface.%20Usually%2C%20we%20set%20the%20option%20to%0A%23%20/etc/usbguard/rules.d/.%20The%20USBGuard%20daemon%20is%20supposed%20to%0A%23%20behave%20like%20any%20other%20standard%20Linux%20daemon%20therefore%20it%0A%23%20loads%20rule%20files%20in%20alpha-numeric%20order.%20File%20names%20inside%0A%23%20RuleFolder%20directory%20should%20start%20with%20a%20two-digit%20number%0A%23%20prefix%20indicating%20the%20position%2C%20in%20which%20the%20rules%20are%0A%23%20scanned%20by%20the%20daemon.%0A%23%0A%23%20RuleFolder%3D/path/to/rulesfolder/%0A%23%0ARuleFolder%3D/etc/usbguard/rules.d/%0A%0A%23%0A%23%20Implicit%20policy%20target.%0A%23%0A%23%20How%20to%20treat%20devices%20that%20don%27t%20match%20any%20rule%20in%20the%0A%23%20policy.%20One%20of%3A%0A%23%0A%23%20%2A%20allow%20%20-%20authorize%20the%20device%0A%23%20%2A%20block%20%20-%20block%20the%20device%0A%23%20%2A%20reject%20-%20remove%20the%20device%0A%23%0AImplicitPolicyTarget%3Dblock%0A%0A%23%0A%23%20Present%20device%20policy.%0A%23%0A%23%20How%20to%20treat%20devices%20that%20are%20already%20connected%20when%20the%0A%23%20daemon%20starts.%20One%20of%3A%0A%23%0A%23%20%2A%20allow%20%20%20%20%20%20%20%20-%20authorize%20every%20present%20device%0A%23%20%2A%20block%20%20%20%20%20%20%20%20-%20deauthorize%20every%20present%20device%0A%23%20%2A%20reject%20%20%20%20%20%20%20-%20remove%20every%20present%20device%0A%23%20%2A%20keep%20%20%20%20%20%20%20%20%20-%20just%20sync%20the%20internal%20state%20and%20leave%20it%0A%23%20%2A%20apply-policy%20-%20evaluate%20the%20ruleset%20for%20every%20present%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20device%0A%23%0APresentDevicePolicy%3Dapply-policy%0A%0A%23%0A%23%20Present%20controller%20policy.%0A%23%0A%23%20How%20to%20treat%20USB%20controllers%20that%20are%20already%20connected%0A%23%20when%20the%20daemon%20starts.%20One%20of%3A%0A%23%0A%23%20%2A%20allow%20%20%20%20%20%20%20%20-%20authorize%20every%20present%20device%0A%23%20%2A%20block%20%20%20%20%20%20%20%20-%20deauthorize%20every%20present%20device%0A%23%20%2A%20reject%20%20%20%20%20%20%20-%20remove%20every%20present%20device%0A%23%20%2A%20keep%20%20%20%20%20%20%20%20%20-%20just%20sync%20the%20internal%20state%20and%20leave%20it%0A%23%20%2A%20apply-policy%20-%20evaluate%20the%20ruleset%20for%20every%20present%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20device%0A%23%0APresentControllerPolicy%3Dkeep%0A%0A%23%0A%23%20Inserted%20device%20policy.%0A%23%0A%23%20How%20to%20treat%20USB%20devices%20that%20are%20already%20connected%0A%23%20%2Aafter%2A%20the%20daemon%20starts.%20One%20of%3A%0A%23%0A%23%20%2A%20block%20%20%20%20%20%20%20%20-%20deauthorize%20every%20present%20device%0A%23%20%2A%20reject%20%20%20%20%20%20%20-%20remove%20every%20present%20device%0A%23%20%2A%20apply-policy%20-%20evaluate%20the%20ruleset%20for%20every%20present%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20device%0A%23%0AInsertedDevicePolicy%3Dapply-policy%0A%0A%23%0A%23%20Control%20which%20devices%20are%20authorized%20by%20default.%0A%23%0A%23%20The%20USBGuard%20daemon%20modifies%20some%20the%20default%20authorization%20state%20attributes%0A%23%20of%20controller%20devices.%20This%20setting%2C%20enables%20you%20to%20define%20what%20value%20the%0A%23%20default%20authorization%20is%20set%20to.%0A%23%0A%23%20%2A%20keep%20%20%20%20%20%20%20%20%20-%20do%20not%20change%20the%20authorization%20state%0A%23%20%2A%20none%20%20%20%20%20%20%20%20%20-%20every%20new%20device%20starts%20out%20deauthorized%0A%23%20%2A%20all%20%20%20%20%20%20%20%20%20%20-%20every%20new%20device%20starts%20out%20authorized%0A%23%20%2A%20internal%20%20%20%20%20-%20internal%20devices%20start%20out%20authorized%2C%20external%20devices%20start%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20out%20deauthorized%20%28this%20requires%20the%20ACPI%20tables%20to%20properly%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%20internal%20devices%2C%20and%20kernel%20support%29%0A%23%0A%23AuthorizedDefault%3Dnone%0A%0A%23%0A%23%20Restore%20controller%20device%20state.%0A%23%0A%23%20The%20USBGuard%20daemon%20modifies%20some%20attributes%20of%20controller%0A%23%20devices%20like%20the%20default%20authorization%20state%20of%20new%20child%20device%0A%23%20instances.%20Using%20this%20setting%2C%20you%20can%20control%20whether%20the%0A%23%20daemon%20will%20try%20to%20restore%20the%20attribute%20values%20to%20the%20state%0A%23%20before%20modification%20on%20shutdown.%0A%23%0A%23%20SECURITY%20CONSIDERATIONS%3A%20If%20set%20to%20true%2C%20the%20USB%20authorization%0A%23%20policy%20could%20be%20bypassed%20by%20performing%20some%20sort%20of%20attack%20on%20the%0A%23%20daemon%20%28via%20a%20local%20exploit%20or%20via%20a%20USB%20device%29%20to%20make%20it%20shutdown%0A%23%20and%20restore%20to%20the%20operating-system%20default%20state%20%28known%20to%20be%20permissive%29.%0A%23%0ARestoreControllerDeviceState%3Dfalse%0A%0A%23%0A%23%20Device%20manager%20backend%0A%23%0A%23%20Which%20device%20manager%20backend%20implementation%20to%20use.%20One%20of%3A%0A%23%0A%23%20%2A%20uevent%20%20%20-%20Netlink%20based%20implementation%20which%20uses%20sysfs%20to%20scan%20for%20present%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20devices%20and%20an%20uevent%20netlink%20socket%20for%20receiving%20USB%20device%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20related%20events.%0A%23%20%2A%20umockdev%20-%20umockdev%20based%20device%20manager%20capable%20of%20simulating%20devices%20based%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20on%20umockdev-record%20files.%20Useful%20for%20testing.%0A%23%0ADeviceManagerBackend%3Duevent%0A%0A%23%21%21%21%20WARNING%3A%20It%27s%20good%20practice%20to%20set%20at%20least%20one%20of%20the%20%21%21%21%0A%23%21%21%21%20%20%20%20%20%20%20%20%20%20two%20options%20bellow.%20If%20none%20of%20them%20are%20set%2C%20%20%21%21%21%0A%23%21%21%21%20%20%20%20%20%20%20%20%20%20the%20daemon%20will%20accept%20IPC%20connections%20from%20%20%20%21%21%21%0A%23%21%21%21%20%20%20%20%20%20%20%20%20%20anyone%2C%20thus%20allowing%20anyone%20to%20modify%20the%20%20%20%20%21%21%21%0A%23%21%21%21%20%20%20%20%20%20%20%20%20%20rule%20set%20and%20%28de%29authorize%20USB%20devices.%20%20%20%20%20%20%20%21%21%21%0A%0A%23%0A%23%20Users%20allowed%20to%20use%20the%20IPC%20interface.%0A%23%0A%23%20A%20space%20delimited%20list%20of%20usernames%20that%20the%20daemon%20will%0A%23%20accept%20IPC%20connections%20from.%0A%23%0A%23%20IPCAllowedUsers%3Dusername1%20username2%20...%0A%23%0AIPCAllowedUsers%3Droot%0A%0A%23%0A%23%20Groups%20allowed%20to%20use%20the%20IPC%20interface.%0A%23%0A%23%20A%20space%20delimited%20list%20of%20groupnames%20that%20the%20daemon%20will%0A%23%20accept%20IPC%20connections%20from.%0A%23%0A%23%20IPCAllowedGroups%3Dgroupname1%20groupname2%20...%0A%23%0AIPCAllowedGroups%3Dwheel%0A%0A%23%0A%23%20IPC%20access%20control%20definition%20files%20path.%0A%23%0A%23%20The%20files%20at%20this%20location%20will%20be%20interpreted%20by%20the%20daemon%0A%23%20as%20access%20control%20definition%20files.%20The%20%28base%29name%20of%20a%20file%0A%23%20should%20be%20in%20the%20form%3A%0A%23%0A%23%20%20%20%5Buser%5D%5B%3A%3Cgroup%3E%5D%0A%23%0A%23%20and%20should%20contain%20lines%20in%20the%20form%3A%0A%23%0A%23%20%20%20%3Csection%3E%3D%5Bprivilege%5D%20...%0A%23%0A%23%20This%20way%20each%20file%20defines%20who%20is%20able%20to%20connect%20to%20the%20IPC%0A%23%20bus%20and%20what%20privileges%20he%20has.%0A%23%0AIPCAccessControlFiles%3D/etc/usbguard/IPCAccessControl.d/%0A%0A%23%0A%23%20Generate%20device%20specific%20rules%20including%20the%20%22via-port%22%0A%23%20attribute.%0A%23%0A%23%20This%20option%20modifies%20the%20behavior%20of%20the%20allowDevice%0A%23%20action.%20When%20instructed%20to%20generate%20a%20permanent%20rule%2C%0A%23%20the%20action%20can%20generate%20a%20port%20specific%20rule.%20Because%0A%23%20some%20systems%20have%20unstable%20port%20numbering%2C%20the%20generated%0A%23%20rule%20might%20not%20match%20the%20device%20after%20rebooting%20the%20system.%0A%23%0A%23%20If%20set%20to%20false%2C%20the%20generated%20rule%20will%20still%20contain%0A%23%20the%20%22parent-hash%22%20attribute%20which%20also%20defines%20an%20association%0A%23%20to%20the%20parent%20device.%20See%20usbguard-rules.conf%285%29%20for%20more%0A%23%20details.%0A%23%0ADeviceRulesWithPort%3Dfalse%0A%0A%23%0A%23%20USBGuard%20Audit%20events%20log%20backend%0A%23%0A%23%20One%20of%3A%0A%23%0A%23%20%2A%20FileAudit%20-%20Log%20audit%20events%20into%20a%20file%20specified%20by%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20AuditFilePath%20setting%20%28see%20below%29%0A%23%20%2A%20LinuxAudit%20-%20Log%20audit%20events%20using%20the%20Linux%20Audit%0A%23%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20subsystem%20%28using%20audit_log_user_message%29%0A%23%0AAuditBackend%3DLinuxAudit%0A%0A%23%0A%23%20USBGuard%20audit%20events%20log%20file%20path.%0A%23%0A%23AuditFilePath%3D/var/log/usbguard/usbguard-audit.log%0A%0A%23%0A%23%20Hides%20personally%20identifiable%20information%20such%20as%20device%20serial%20numbers%20and%0A%23%20hashes%20of%20descriptors%20%28which%20include%20the%20serial%20number%29%20from%20audit%20entries.%0A%23%0A%23HidePII%3Dfalse }}
        mode: 0600
        path: /etc/usbguard/usbguard-daemon.conf
        overwrite: true

Complexity:low
Disruption:low
Strategy:restrict
# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && ! grep -q s390x /proc/sys/kernel/osrelease ) && { rpm --quiet -q usbguard; }; then

if [ -e "/etc/usbguard/usbguard-daemon.conf" ] ; then
    
    LC_ALL=C sed -i "/^\s*AuditBackend=/d" "/etc/usbguard/usbguard-daemon.conf"
else
    touch "/etc/usbguard/usbguard-daemon.conf"
fi
# make sure file has newline at the end
sed -i -e '$a\' "/etc/usbguard/usbguard-daemon.conf"

cp "/etc/usbguard/usbguard-daemon.conf" "/etc/usbguard/usbguard-daemon.conf.bak"
# Insert at the end of the file
printf '%s\n' "AuditBackend=LinuxAudit" >> "/etc/usbguard/usbguard-daemon.conf"
# Clean up after ourselves.
rm "/etc/usbguard/usbguard-daemon.conf.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Rule   Generate USBGuard Policy   [ref]

By default USBGuard when enabled prevents access to all USB devices and this lead to inaccessible system if they use USB mouse/keyboard. To prevent this scenario, the initial policy configuration must be generated based on current connected USB devices.
Rationale:
The usbguard must be configured to allow connected USB devices to work properly, avoiding the system to become inaccessible.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_usbguard_generate_policy
Identifiers and References

References:  CCI-000416, CCI-001958, CM-8(3)(a), IA-3, FMT_SMF_EXT.1, SRG-OS-000378-GPOS-00163, OL08-00-040140, SV-248863r853872_rule


Complexity:low
Disruption:low
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - DISA-STIG-OL08-00-040140
  - NIST-800-53-CM-8(3)(a)
  - NIST-800-53-IA-3
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - usbguard_generate_policy

- name: Generate USBGuard Policy
  block:

  - name: Gather the package facts
    package_facts:
      manager: auto

  - name: Check that the /etc/usbguard/rules.conf exists
    stat:
      path: /etc/usbguard/rules.conf
    register: policy_file

  - name: Create USBGuard Policy configuration
    command: usbguard generate-policy
    register: policy
    when: not policy_file.stat.exists or policy_file.stat.size == 0

  - name: Copy the Generated Policy configuration to a persistent file
    copy:
      content: '{{ policy.stdout }}'
      dest: /etc/usbguard/rules.conf
      mode: 384
    when: not policy_file.stat.exists or policy_file.stat.size == 0

  - name: Add comment into /etc/usbguard/rules.conf when system has no USB devices
    lineinfile:
      path: /etc/usbguard/rules.conf
      line: '# No USB devices found'
      state: present
    when: not policy_file.stat.exists or policy_file.stat.size == 0

  - name: Enable service usbguard
    systemd:
      name: usbguard
      enabled: 'yes'
      state: started
      masked: 'no'
  when:
  - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    and ansible_architecture != "s390x" )
  - '"usbguard" in ansible_facts.packages'
  tags:
  - DISA-STIG-OL08-00-040140
  - NIST-800-53-CM-8(3)(a)
  - NIST-800-53-IA-3
  - configure_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - usbguard_generate_policy

Complexity:low
Disruption:low
Strategy:configure
# Remediation is applicable only in certain platforms
if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && ! grep -q s390x /proc/sys/kernel/osrelease ); then

if rpm --quiet -q usbguard
then
    USBGUARD_CONF=/etc/usbguard/rules.conf
    if [ ! -f "$USBGUARD_CONF" ] || [ ! -s "$USBGUARD_CONF" ]; then
        usbguard generate-policy > $USBGUARD_CONF
        if [ ! -s "$USBGUARD_CONF" ]; then
            # make sure OVAL check doesn't fail on systems where
            # generate-policy doesn't find any USB devices (for
            # example a system might not have a USB bus)
            echo "# No USB devices found" > $USBGUARD_CONF
        fi
        # make sure it has correct permissions
        chmod 600 $USBGUARD_CONF

        SYSTEMCTL_EXEC='/usr/bin/systemctl'
        "$SYSTEMCTL_EXEC" unmask 'usbguard.service'
        "$SYSTEMCTL_EXEC" restart 'usbguard.service'
        "$SYSTEMCTL_EXEC" enable 'usbguard.service'
    fi
else
    echo "USBGuard is not installed. No remediation was applied!"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   X Window System   Group contains 1 group and 2 rules
[ref]   The X Window System implementation included with the system is called X.org.
Group   Disable X Windows   Group contains 2 rules
[ref]   Unless there is a mission-critical reason for the system to run a graphical user interface, ensure X is not set to start automatically at boot and remove the X Windows software packages. There is usually no reason to run X Windows on a dedicated server system, as it increases the system's attack surface and consumes system resources. Administrators of server systems should instead login via SSH or on the text console.

Rule   Disable graphical user interface   [ref]

By removing the following packages, the system no longer has X Windows installed. xorg-x11-server-Xorg xorg-x11-server-common xorg-x11-server-utils xorg-x11-server-Xwayland If X Windows is not installed then the system cannot boot into graphical user mode. This prevents the system from being accidentally or maliciously booted into a graphical.target mode. To do so, run the following command:
sudo yum remove xorg-x11-server-Xorg xorg-x11-server-common xorg-x11-server-utils xorg-x11-server-Xwayland
Warning:  The installation and use of a Graphical User Interface (GUI) increases your attack vector and decreases your overall security posture. Removing the package xorg-x11-server-common package will remove the graphical target which might bring your system to an inconsistent state requiring additional configuration to access the system again. The rule xwindows_runlevel_target can be used to configure the system to boot into the multi-user.target. If a GUI is an operational requirement, a tailored profile that removes this rule should be used before continuing installation.
Rationale:
Unnecessary service packages must not be installed to decrease the attack surface of the system. X windows has a long history of security vulnerabilities and should not be installed unless approved and documented.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_xwindows_remove_packages
Identifiers and References

References:  CCI-000366, CM-6(b), SRG-OS-000480-GPOS-00227, OL08-00-040320, SV-248898r780260_rule



package --remove=xorg-x11-server-Xorg --remove=xorg-x11-server-common --remove=xorg-x11-server-utils --remove=xorg-x11-server-Xwayland

Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Ensure xorg packages are removed
  package:
    name:
    - xorg-x11-server-Xorg
    - xorg-x11-server-common
    - xorg-x11-server-utils
    - xorg-x11-server-Xwayland
    state: absent
  tags:
  - DISA-STIG-OL08-00-040320
  - NIST-800-53-CM-6(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy
  - xwindows_remove_packages

Complexity:low
Disruption:low
Reboot:true
Strategy:restrict


# remove packages
if rpm -q --quiet "xorg-x11-server-Xorg" ; then

    yum remove -y "xorg-x11-server-Xorg"

fi
if rpm -q --quiet "xorg-x11-server-utils" ; then

    yum remove -y "xorg-x11-server-utils"

fi
if rpm -q --quiet "xorg-x11-server-common" ; then

    yum remove -y "xorg-x11-server-common"

fi

if rpm -q --quiet "xorg-x11-server-Xwayland" ; then

    yum remove -y "xorg-x11-server-Xwayland"

fi

Rule   Disable X Windows Startup By Setting Default Target   [ref]

Systems that do not require a graphical user interface should only boot by default into multi-user.target mode. This prevents accidental booting of the system into a graphical.target mode. Setting the system's default target to multi-user.target will prevent automatic startup of the X server. To do so, run:
$ systemctl set-default multi-user.target
You should see the following output:
Removed symlink /etc/systemd/system/default.target.
Created symlink from /etc/systemd/system/default.target to /usr/lib/systemd/system/multi-user.target.
Rationale:
Services that are not required for system and application processes must not be active to decrease the attack surface of the system. X windows has a long history of security vulnerabilities and should not be used unless approved and documented.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_xwindows_runlevel_target
Identifiers and References

References:  12, 15, 8, APO13.01, DSS01.04, DSS05.02, DSS05.03, CCI-000366, 4.3.3.6.6, SR 1.13, SR 2.6, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.13.1.1, A.13.2.1, A.14.1.3, A.6.2.1, A.6.2.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.PT-4, SRG-OS-000480-GPOS-00227, OL08-00-040321, SV-252663r818785_rule


Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: Switch to multi-user runlevel
  file:
    src: /usr/lib/systemd/system/multi-user.target
    dest: /etc/systemd/system/default.target
    state: link
    force: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - DISA-STIG-OL08-00-040321
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-7(a)
  - NIST-800-53-CM-7(b)
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy
  - xwindows_runlevel_target

Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

systemctl set-default multi-user.target

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Red Hat and Red Hat Enterprise Linux are either registered trademarks or trademarks of Red Hat, Inc. in the United States and other countries. All other names are registered trademarks or trademarks of their respective companies.