Guide to the Secure Configuration of Red Hat Enterprise Linux 10

with profile ANSSI-BP-028 (high)
This profile contains configurations that align to ANSSI-BP-028 v2.0 at the high hardening level. ANSSI is the French National Information Security Agency, and stands for Agence nationale de la sécurité des systèmes d'information. ANSSI-BP-028 is a configuration recommendation for GNU/Linux systems. A copy of the ANSSI-BP-028 can be found at the ANSSI website: https://www.ssi.gouv.fr/administration/guide/recommandations-de-securite-relatives-a-un-systeme-gnulinux/ An English version of the ANSSI-BP-028 can also be found at the ANSSI website: https://cyber.gouv.fr/publications/configuration-recommendations-gnulinux-system
This guide presents a catalog of security-relevant configuration settings for Red Hat Enterprise Linux 10. 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 TitleANSSI-BP-028 (high)
Profile IDxccdf_org.ssgproject.content_profile_anssi_bp28_high

CPE Platforms

  • cpe:/o:redhat:enterprise_linux:10

Revision History

Current version: 0.1.74

  • draft (as of 2024-05-28)

Table of Contents

  1. System Settings
    1. Installing and Maintaining Software
    2. Account and Access Control
    3. GRUB2 bootloader configuration
    4. Kernel Configuration
    5. Configure Syslog
    6. Network Configuration and Firewalls
    7. File Permissions and Masks
    8. SELinux
  2. Services
    1. DHCP
    2. Mail Server Software
    3. Network Time Protocol
    4. Obsolete Services
    5. SSH Server
    6. System Security Services Daemon
  3. System Accounting with auditd
    1. Configure auditd Rules for Comprehensive Auditing

Checklist

Group   Guide to the Secure Configuration of Red Hat Enterprise Linux 10   Group contains 76 groups and 399 rules
Group   System Settings   Group contains 49 groups and 314 rules
[ref]   Contains rules that check correct system settings.
Group   Installing and Maintaining Software   Group contains 6 groups and 38 rules
[ref]   The following sections contain information on security-relevant choices during the initial operating system installation process and the setup of software updates.
Group   System and Software Integrity   Group contains 2 groups and 6 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 6 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 6 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 dnf 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
References:
cis-csc1, 11, 12, 13, 14, 15, 16, 2, 3, 5, 7, 8, 9
cjis5.10.1.3
cobit5APO01.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
disaCCI-002696, CCI-002699, CCI-001744
isa-62443-20094.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4
isa-62443-2013SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 4.1, SR 6.2, SR 7.6
ism1034, 1288, 1341, 1417
iso27001-2013A.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
nistCM-6(a)
nist-csfDE.CM-1, DE.CM-7, PR.DS-1, PR.DS-6, PR.DS-8, PR.IP-1, PR.IP-3
pcidssReq-11.5
os-srgSRG-OS-000445-GPOS-00199
anssiR76, R79
pcidss411.5.2

Complexity:low
Disruption:low
Reboot:false
Strategy:enable
- name: Ensure aide is installed
  package:
    name: aide
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.3
  - 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
Reboot:false
Strategy:enable
include install_aide

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

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

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

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

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

package --add=aide


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

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
References:
cis-csc1, 11, 12, 13, 14, 15, 16, 2, 3, 5, 7, 8, 9
cjis5.10.1.3
cobit5APO01.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
isa-62443-20094.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4
isa-62443-2013SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 4.1, SR 6.2, SR 7.6
iso27001-2013A.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
nistCM-6(a)
nist-csfDE.CM-1, DE.CM-7, PR.DS-1, PR.DS-6, PR.DS-8, PR.IP-1, PR.IP-3
pcidssReq-11.5
os-srgSRG-OS-000445-GPOS-00199
anssiR76, R79
pcidss411.5.2

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Build and Test AIDE Database - Ensure AIDE Is Installed
  ansible.builtin.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
  - 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 - Build and Test AIDE Database
  ansible.builtin.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
  - 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 - Check Whether the Stock AIDE Database Exists
  ansible.builtin.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
  - 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 - Stage AIDE Database
  ansible.builtin.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
  - 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
    dnf 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 Periodic Execution of AIDE   [ref]

At a minimum, AIDE should be configured to run a weekly scan. To implement a daily execution of AIDE at 4:05am using cron, add the following line to /etc/crontab:
05 4 * * * root /usr/sbin/aide --check
To implement a weekly execution of AIDE at 4:05am using cron, add the following line to /etc/crontab:
05 4 * * 0 root /usr/sbin/aide --check
AIDE can be executed periodically through other means; this is merely one example. The usage of cron's special time codes, such as @daily and @weekly is acceptable.
Rationale:
By default, AIDE does not install itself for periodic execution. Periodically running AIDE is necessary to reveal unexpected changes in installed files.

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_periodic_cron_checking
References:
cis-csc1, 11, 12, 13, 14, 15, 16, 2, 3, 5, 7, 8, 9
cjis5.10.1.3
cobit5APO01.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
disaCCI-001744, CCI-002699, CCI-002702
isa-62443-20094.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4
isa-62443-2013SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 4.1, SR 6.2, SR 7.6
iso27001-2013A.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
nistSI-7, SI-7(1), CM-6(a)
nist-csfDE.CM-1, DE.CM-7, PR.DS-1, PR.DS-6, PR.DS-8, PR.IP-1, PR.IP-3
pcidssReq-11.5
os-srgSRG-OS-000363-GPOS-00150, SRG-OS-000446-GPOS-00200, SRG-OS-000447-GPOS-00201
anssiR76
pcidss411.5.2

Complexity:low
Disruption:low
Reboot:false
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
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_periodic_cron_checking
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set cron package name - RedHat
  set_fact:
    cron_pkg_name: cronie
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_os_family == "RedHat" or ansible_os_family == "Suse"
  tags:
  - CJIS-5.10.1.3
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_periodic_cron_checking
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set cron package name - Debian
  set_fact:
    cron_pkg_name: cron
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ansible_os_family == "Debian"
  tags:
  - CJIS-5.10.1.3
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_periodic_cron_checking
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Install cron
  package:
    name: '{{ cron_pkg_name }}'
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.3
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_periodic_cron_checking
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Configure Periodic Execution of AIDE
  cron:
    name: run AIDE check
    minute: 5
    hour: 4
    weekday: 0
    user: root
    job: /usr/sbin/aide --check
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.10.1.3
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - PCI-DSS-Req-11.5
  - PCI-DSSv4-11.5.2
  - aide_periodic_cron_checking
  - 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
    dnf install -y "aide"
fi

if ! grep -q "/usr/sbin/aide --check" /etc/crontab ; then
    echo "05 4 * * * root /usr/sbin/aide --check" >> /etc/crontab
else
    sed -i '\!^.* --check.*$!d' /etc/crontab
    echo "05 4 * * * root /usr/sbin/aide --check" >> /etc/crontab
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
References:
cis-csc1, 11, 12, 13, 15, 16, 2, 3, 5, 7, 8, 9
cobit5BAI01.06, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS05.02, DSS05.05, DSS05.07
disaCCI-001744, CCI-002699, CCI-002702
isa-62443-20094.3.4.3.2, 4.3.4.3.3
isa-62443-2013SR 6.2, SR 7.6
iso27001-2013A.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
nistCM-6(a), CM-3(5)
nist-csfDE.CM-1, DE.CM-7, PR.IP-1, PR.IP-3
os-srgSRG-OS-000363-GPOS-00150, SRG-OS-000446-GPOS-00200, SRG-OS-000447-GPOS-00201
anssiR76

Complexity:low
Disruption:low
Reboot:false
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: '{{ item }}'
    state: present
  with_items:
  - aide
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - 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:
  - 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
    dnf install -y "aide"
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
References:
cis-csc2, 3
cobit5APO01.06, BAI03.05, BAI06.01, DSS06.02
disaCCI-000366
isa-62443-20094.3.4.4.4
isa-62443-2013SR 3.1, SR 3.3, SR 3.4, SR 3.8
iso27001-2013A.11.2.4, A.12.2.1, A.12.5.1, A.14.1.2, A.14.1.3, A.14.2.4
nistSI-7, SI-7(1), CM-6(a)
nist-csfPR.DS-6, PR.DS-8
os-srgSRG-OS-000480-GPOS-00227
anssiR76

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather list of packages
  package_facts:
    manager: auto
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ''
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - aide_verify_acls
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

- name: Get rules groups
  shell: |
    set -o pipefail
    LC_ALL=C grep "^[A-Z][A-Za-z_]*" /etc/aide.conf | grep -v "^ALLXTRAHASHES" | cut -f1 -d '=' | tr -d ' ' | sort -u || true
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '''aide'' in ansible_facts.packages'
  register: find_rules_groups_results
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - aide_verify_acls
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure the acl rule is present when aide is installed.
  replace:
    path: /etc/aide.conf
    regexp: (^\s*{{ item }}\s*=\s*)(?!.*acl)([^\s]*)
    replace: \g<1>\g<2>+acl
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_rules_groups_results is not skipped and "'aide' in ansible_facts.packages"
  with_items: '{{ find_rules_groups_results.stdout_lines | map(''trim'') | list }}'
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - aide_verify_acls
  - 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

if ! rpm -q --quiet "aide" ; then
    dnf 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
References:
cis-csc2, 3
cobit5APO01.06, BAI03.05, BAI06.01, DSS06.02
disaCCI-000366
isa-62443-20094.3.4.4.4
isa-62443-2013SR 3.1, SR 3.3, SR 3.4, SR 3.8
iso27001-2013A.11.2.4, A.12.2.1, A.12.5.1, A.14.1.2, A.14.1.3, A.14.2.4
nistSI-7, SI-7(1), CM-6(a)
nist-csfPR.DS-6, PR.DS-8
os-srgSRG-OS-000480-GPOS-00227
anssiR76

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather list of packages
  package_facts:
    manager: auto
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ''
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - aide_verify_ext_attributes
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

- name: Get rules groups
  shell: |
    set -o pipefail
    LC_ALL=C grep "^[A-Z][A-Za-z_]*" /etc/aide.conf | grep -v "^ALLXTRAHASHES" | cut -f1 -d '=' | tr -d ' ' | sort -u || true
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - '''aide'' in ansible_facts.packages'
  register: find_rules_groups_results
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - aide_verify_ext_attributes
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure the xattrs rule is present when aide is installed.
  replace:
    path: /etc/aide.conf
    regexp: (^\s*{{ item }}\s*=\s*)(?!.*xattrs)([^\s]*)
    replace: \g<1>\g<2>+xattrs
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - find_rules_groups_results is not skipped and "'aide' in ansible_facts.packages"
  with_items: '{{ find_rules_groups_results.stdout_lines | map(''trim'') | list }}'
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-7
  - NIST-800-53-SI-7(1)
  - aide_verify_ext_attributes
  - 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

if ! rpm -q --quiet "aide" ; then
    dnf 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
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   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: 
low
Rule ID:xccdf_org.ssgproject.content_rule_partition_for_home
References:
cis-csc12, 15, 8
cobit5APO13.01, DSS05.02
disaCCI-000366, CCI-001208
isa-62443-2013SR 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
iso27001-2013A.13.1.1, A.13.2.1, A.14.1.3
nistCM-6(a), SC-5(2)
nist-csfPR.PT-4
os-srgSRG-OS-000480-GPOS-00227
anssiR28

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

part /home


[[customizations.filesystem]]
mountpoint = "/home"
size = 1073741824

Rule   Ensure /srv Located On Separate Partition   [ref]

If a file server (FTP, TFTP...) is hosted locally, create a separate partition for /srv at installation time (or migrate it later using LVM). If /srv 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:
Srv deserves files for local network file server such as FTP. Ensuring that /srv 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: 
unknown
Rule ID:xccdf_org.ssgproject.content_rule_partition_for_srv
References:
anssiR28

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

part /srv


[[customizations.filesystem]]
mountpoint = "/srv"
size = 1073741824

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
References:
cis-csc12, 15, 8
cobit5APO13.01, DSS05.02
disaCCI-000366
isa-62443-2013SR 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
iso27001-2013A.13.1.1, A.13.2.1, A.14.1.3
nistCM-6(a), SC-5(2)
nist-csfPR.PT-4
os-srgSRG-OS-000480-GPOS-00227
anssiR28

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

part /var


[[customizations.filesystem]]
mountpoint = "/var"
size = 3221225472

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
References:
cis-csc1, 12, 14, 15, 16, 3, 5, 6, 8
cobit5APO11.04, APO13.01, BAI03.05, DSS05.02, DSS05.04, DSS05.07, MEA02.01
disaCCI-000366
isa-62443-20094.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
isa-62443-2013SR 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
iso27001-2013A.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
nerc-cipCIP-007-3 R6.5
nistCM-6(a), AU-4, SC-5(2)
nist-csfPR.PT-1, PR.PT-4
os-srgSRG-OS-000480-GPOS-00227
anssiR28

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

part /var/log


[[customizations.filesystem]]
mountpoint = "/var/log"
size = 5368709120

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
References:
cis-csc1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 8
cobit5APO11.04, APO13.01, BAI03.05, BAI04.04, DSS05.02, DSS05.04, DSS05.07, MEA02.01
disaCCI-000366, CCI-001849
hipaa164.312(a)(2)(ii)
isa-62443-20094.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
isa-62443-2013SR 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
iso27001-2013A.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
nerc-cipCIP-007-3 R6.5
nistCM-6(a), AU-4, SC-5(2)
nist-csfPR.DS-4, PR.PT-1, PR.PT-4
osppFMT_SMF_EXT.1
os-srgSRG-OS-000341-GPOS-00132, SRG-OS-000480-GPOS-00227
app-srg-ctrSRG-APP-000357-CTR-000800
anssiR71

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

part /var/log/audit


[[customizations.filesystem]]
mountpoint = "/var/log/audit"
size = 10737418240

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
References:
os-srgSRG-OS-000480-GPOS-00227
anssiR28

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

part /var/tmp


[[customizations.filesystem]]
mountpoint = "/var/tmp"
size = 1073741824

Rule   Ensure tmp.mount Unit Us Enabled   [ref]

The /tmp directory is a world-writable directory used for temporary file storage. This directory is managed by systemd-tmpfiles. Ensure that the tmp.mount systemd unit is enabled.
Rationale:
The /tmp directory is used as temporary storage by many programs. Placing /tmp in a tmpfs filesystem enables the setting of more restrictive mount options, which can help protect programs which use it. The tmp.mount unit configures the tmpfs filesystem and ensures the /tmp directory is wiped during reboot.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_systemd_tmp_mount_enabled
References:
anssiR28

Complexity:low
Disruption:low
Reboot:false
Strategy:enable
- name: Enable mount tmp
  ansible.builtin.systemd:
    name: tmp.mount
    enabled: 'yes'
    state: started
    masked: 'false'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - enable_strategy
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - systemd_tmp_mount_enabled

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

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'tmp.mount'
"$SYSTEMCTL_EXEC" start 'tmp.mount'
"$SYSTEMCTL_EXEC" enable 'tmp.mount'

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

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

services --enabled=tmp.mount
Group   Sudo   Group contains 15 rules
[ref]   Sudo, which stands for "su 'do'", provides the ability to delegate authority to certain users, groups of users, or system administrators. When configured for system users and/or groups, Sudo can allow a user or group to execute privileged commands that normally only root is allowed to execute.

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

Rule   Install sudo Package   [ref]

The sudo package can be installed with the following command:
$ sudo dnf install sudo
Rationale:
sudo is a program designed to allow a system administrator to give limited root privileges to users and log root activity. The basic philosophy is to give as few privileges as possible but still allow system users to get their work done.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_sudo_installed
Identifiers:

CCE-87100-4

References:
ism1382, 1384, 1386
nistCM-6(a)
osppFMT_MOF_EXT.1
os-srgSRG-OS-000324-GPOS-00125
anssiR33
pcidss42.2.6

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

Complexity:low
Disruption:low
Reboot:false
Strategy:enable
include install_sudo

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

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

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

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

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

package --add=sudo


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

Rule   Verify Group Who Owns /etc/sudoers.d Directory   [ref]

To properly set the group owner of /etc/sudoers.d, run the command:
$ sudo chgrp root /etc/sudoers.d
Rationale:
The ownership of the /etc/sudoers.d directory by the root group is important because this directory hosts sudo configuration. Protection of this directory is critical for system security. Assigning the ownership to root ensures exclusive control of the sudo configuration.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_directory_groupowner_etc_sudoersd
References:
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Ensure group owner on /etc/sudoers.d/
  file:
    path: /etc/sudoers.d/
    state: directory
    group: root
  tags:
  - configure_strategy
  - directory_groupowner_etc_sudoersd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
find -H /etc/sudoers.d/ -maxdepth 1 -type d -exec chgrp root {} \;

Rule   Verify User Who Owns /etc/sudoers.d Directory   [ref]

To properly set the owner of /etc/sudoers.d, run the command:
$ sudo chown root /etc/sudoers.d 
Rationale:
The ownership of the /etc/sudoers.d directory by the root user is important because this directory hosts sudo configuration. Protection of this directory is critical for system security. Assigning the ownership to root ensures exclusive control of the sudo configuration.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_directory_owner_etc_sudoersd
References:
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Ensure owner on directory /etc/sudoers.d/
  file:
    path: /etc/sudoers.d/
    state: directory
    owner: '0'
  tags:
  - configure_strategy
  - directory_owner_etc_sudoersd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
find -H /etc/sudoers.d/ -maxdepth 1 -type d -exec chown 0 {} \;

Rule   Verify Permissions On /etc/sudoers.d Directory   [ref]

To properly set the permissions of /etc/sudoers.d, run the command:
$ sudo chmod 0750 /etc/sudoers.d
Rationale:
Setting correct permissions on the /etc/sudoers.d directory is important because this directory hosts sudo configuration. Protection of this directory is critical for system security. Restricting the permissions ensures exclusive control of the sudo configuration.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_directory_permissions_etc_sudoersd
References:
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Find /etc/sudoers.d/ file(s)
  command: 'find -H /etc/sudoers.d/ -maxdepth 1 -perm /u+s,g+ws,o+xwrt  -type d '
  register: files_found
  changed_when: false
  failed_when: false
  check_mode: false
  tags:
  - configure_strategy
  - directory_permissions_etc_sudoersd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Set permissions for /etc/sudoers.d/ file(s)
  file:
    path: '{{ item }}'
    mode: u-s,g-ws,o-xwrt
    state: directory
  with_items:
  - '{{ files_found.stdout_lines }}'
  tags:
  - configure_strategy
  - directory_permissions_etc_sudoersd
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

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





find -H /etc/sudoers.d/ -maxdepth 1 -perm /u+s,g+ws,o+xwrt -type d -exec chmod u-s,g-ws,o-xwrt {} \;

Rule   Verify Group Who Owns /etc/sudoers File   [ref]

To properly set the group owner of /etc/sudoers, run the command:
$ sudo chgrp root /etc/sudoers
Rationale:
The ownership of the /etc/sudoers file by the root group is important because this file hosts sudo configuration. Protection of this file is critical for system security. Assigning the ownership to root ensures exclusive control of the sudo configuration.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupowner_etc_sudoers
References:
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Test for existence /etc/sudoers
  stat:
    path: /etc/sudoers
  register: file_exists
  tags:
  - configure_strategy
  - file_groupowner_etc_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner root on /etc/sudoers
  file:
    path: /etc/sudoers
    group: root
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - configure_strategy
  - file_groupowner_etc_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
chgrp root /etc/sudoers

Rule   Verify User Who Owns /etc/sudoers File   [ref]

To properly set the owner of /etc/sudoers, run the command:
$ sudo chown root /etc/sudoers 
Rationale:
The ownership of the /etc/sudoers file by the root user is important because this file hosts sudo configuration. Protection of this file is critical for system security. Assigning the ownership to root ensures exclusive control of the sudo configuration.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_owner_etc_sudoers
References:
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Test for existence /etc/sudoers
  stat:
    path: /etc/sudoers
  register: file_exists
  tags:
  - configure_strategy
  - file_owner_etc_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /etc/sudoers
  file:
    path: /etc/sudoers
    owner: '0'
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - configure_strategy
  - file_owner_etc_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
chown 0 /etc/sudoers

Rule   Verify Permissions On /etc/sudoers File   [ref]

To properly set the permissions of /etc/sudoers, run the command:
$ sudo chmod 0440 /etc/sudoers
Rationale:
Setting correct permissions on the /etc/sudoers file is important because this file hosts sudo configuration. Protection of this file is critical for system security. Restricting the permissions ensures exclusive control of the sudo configuration.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_etc_sudoers
References:
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Test for existence /etc/sudoers
  stat:
    path: /etc/sudoers
  register: file_exists
  tags:
  - configure_strategy
  - file_permissions_etc_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-xws,g-xws,o-xwrt on /etc/sudoers
  file:
    path: /etc/sudoers
    mode: u-xws,g-xws,o-xwrt
  when: file_exists.stat is defined and file_exists.stat.exists
  tags:
  - configure_strategy
  - file_permissions_etc_sudoers
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

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





chmod u-xws,g-xws,o-xwrt /etc/sudoers

Rule   Ensure That the sudo Binary Has the Correct Permissions   [ref]

To properly set the permissions of /usr/bin/sudo, run the command:
$ sudo chmod 4111 /usr/bin/sudo
Rationale:
The sudoers program should only be usable by people who have the correct permissions.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_sudo
References:
anssiR38

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - configure_strategy
  - file_permissions_sudo
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /usr/bin/sudo
  stat:
    path: /usr/bin/sudo
  register: file_exists
  when: '"sudo" in ansible_facts.packages'
  tags:
  - configure_strategy
  - file_permissions_sudo
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission 4110 on /usr/bin/sudo
  file:
    path: /usr/bin/sudo
    mode: '4110'
  when:
  - '"sudo" in ansible_facts.packages'
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - configure_strategy
  - file_permissions_sudo
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

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

chmod 4110 /usr/bin/sudo

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

Rule   Ensure Privileged Escalated Commands Cannot Execute Other Commands - sudo NOEXEC   [ref]

The sudo NOEXEC tag, when specified, prevents user executed commands from executing other commands, like a shell for example. This should be enabled by making sure that the NOEXEC tag exists in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/.
Rationale:
Restricting the capability of sudo allowed commands to execute sub-commands prevents users from running programs with privileges they wouldn't have otherwise.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_sudo_add_noexec
References:
anssiR39

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Ensure noexec is enabled in /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    regexp: ^[\s]*Defaults.*\bnoexec\b.*$
    line: Defaults noexec
    validate: /usr/sbin/visudo -cf %s
  tags:
  - high_severity
  - low_complexity
  - low_disruption
  - no_reboot_needed
  - restrict_strategy
  - sudo_add_noexec

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

if /usr/sbin/visudo -qcf /etc/sudoers; then
    cp /etc/sudoers /etc/sudoers.bak
    if ! grep -P '^[\s]*Defaults[\s]*\bnoexec\b.*$' /etc/sudoers; then
        # sudoers file doesn't define Option noexec
        echo "Defaults noexec" >> /etc/sudoers
    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

Rule   Ensure Only Users Logged In To Real tty Can Execute Sudo - sudo requiretty   [ref]

The sudo requiretty tag, when specified, will only execute sudo commands from users logged in to a real tty. This should be enabled by making sure that the requiretty tag exists in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/.
Rationale:
Restricting the use cases in which a user is allowed to execute sudo commands reduces the attack surface.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudo_add_requiretty
References:
anssiR39

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Ensure requiretty is enabled in /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    regexp: ^[\s]*Defaults.*\brequiretty\b.*$
    line: Defaults requiretty
    validate: /usr/sbin/visudo -cf %s
  tags:
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_add_requiretty

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

if /usr/sbin/visudo -qcf /etc/sudoers; then
    cp /etc/sudoers /etc/sudoers.bak
    if ! grep -P '^[\s]*Defaults[\s]*\brequiretty\b.*$' /etc/sudoers; then
        # sudoers file doesn't define Option requiretty
        echo "Defaults requiretty" >> /etc/sudoers
    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

Rule   Ensure Only Users Logged In To Real tty Can Execute Sudo - sudo use_pty   [ref]

The sudo use_pty tag, when specified, will only execute sudo commands from users logged in to a real tty. This should be enabled by making sure that the use_pty tag exists in /etc/sudoers configuration file or any sudo configuration snippets in /etc/sudoers.d/.
Rationale:
Requiring that sudo commands be run in a pseudo-terminal can prevent an attacker from retaining access to the user's terminal after the main program has finished executing.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudo_add_use_pty
References:
pcidssReq-10.2.5
anssiR39
pcidss42.2.6

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-2.2.6
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_add_use_pty

- name: Ensure use_pty is enabled in /etc/sudoers
  lineinfile:
    path: /etc/sudoers
    regexp: ^[\s]*Defaults.*\buse_pty\b.*$
    line: Defaults use_pty
    validate: /usr/sbin/visudo -cf %s
  when: '"sudo" in ansible_facts.packages'
  tags:
  - PCI-DSS-Req-10.2.5
  - PCI-DSSv4-2.2.6
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
  - sudo_add_use_pty

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

if /usr/sbin/visudo -qcf /etc/sudoers; then
    cp /etc/sudoers /etc/sudoers.bak
    if ! grep -P '^[\s]*Defaults[\s]*\buse_pty\b.*$' /etc/sudoers; then
        # sudoers file doesn't define Option use_pty
        echo "Defaults use_pty" >> /etc/sudoers
    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   Ensure a dedicated group owns sudo   [ref]

Restrict the execution of privilege escalated commands to a dedicated group of users. Ensure the group owner of /usr/bin/sudo is sudogrp.
Warning:  Changing group owner of /usr/bin/sudo to a group with no member users will prevent any and all escalatation of privileges. Additionally, the system may become unmanageable if root logins are not allowed.
Warning:  This rule doesn't come with a remediation, before remediating the sysadmin needs to add users to the dedicated sudo group.
Rationale:
Restricting the set of users able to execute commands as privileged user reduces the attack surface.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudo_dedicated_group
References:
anssiR38

Rule   Explicit arguments in sudo specifications   [ref]

All commands in the sudoers file must strictly specify the arguments allowed to be used for a given user. If the command is supposed to be executed only without arguments, pass "" as an argument in the corresponding user specification.
Warning:  This rule doesn't come with a remediation, as absence of arguments in the user spec doesn't mean that the command is intended to be executed with no arguments.
Warning:  The rule can produce false findings when an argument contains a comma - sudoers syntax allows comma escaping using backslash, but the check doesn't support that. For example, root ALL=(ALL) echo 1\,2 allows root to execute echo 1,2, but the check would interpret it as two commands echo 1\ and 2.
Rationale:
Any argument can modify quite significantly the behavior of a program, whether regarding the realized operation (read, write, delete, etc.) or accessed resources (path in a file system tree). To avoid any possibility of misuse of a command by a user, the ambiguities must be removed at the level of its specification. For example, on some systems, the kernel messages are only accessible by root. If a user nevertheless must have the privileges to read them, the argument of the dmesg command has to be restricted in order to prevent the user from flushing the buffer through the -c option:
user ALL = dmesg ""
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudoers_explicit_command_args
References:
anssiR43

Rule   Don't define allowed commands in sudoers by means of exclusion   [ref]

Policies applied by sudo through the sudoers file should not involve negation. Each user specification in the sudoers file contains a comma-delimited list of command specifications. The definition can make use glob patterns, as well as of negations. Indirect definition of those commands by means of exclusion of a set of commands is trivial to bypass, so it is not allowed to use such constructs.
Warning:  This rule doesn't come with a remediation, as negations indicate design issues with the sudoers user specifications design. Just removing negations doesn't increase the security - you typically have to rethink the definition of allowed commands to fix the issue.
Rationale:
Specifying access right using negation is inefficient and can be easily circumvented. For example, it is expected that a specification like
# To avoid absolutely , this rule can be easily circumvented!
user ALL = ALL ,!/ bin/sh
prevents the execution of the shell but that’s not the case: just copy the binary /bin/sh to a different name to make it executable again through the rule keyword ALL.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudoers_no_command_negation
References:
anssiR42

Rule   Don't target root user in the sudoers file   [ref]

The targeted users of a user specification should be, as much as possible, non privileged users (i.e.: non-root). User specifications have to explicitly list the runas spec (i.e. the list of target users that can be impersonated), and ALL or root should not be used.
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:
It is common that the command to be executed does not require superuser rights (editing a file whose the owner is not root, sending a signal to an unprivileged process,etc.). In order to limit any attempt of privilege escalation through a command, it is better to apply normal user rights.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_sudoers_no_root_target
References:
anssiR40
Group   Updating Software   Group contains 9 rules
[ref]   The dnf command line tool is used to install and update software packages. The system also provides a graphical software update tool in the System menu, in the Administration submenu, called Software Update.

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

Rule   Install dnf-automatic Package   [ref]

The dnf-automatic package can be installed with the following command:
$ sudo dnf install dnf-automatic
Rationale:
dnf-automatic is an alternative command line interface (CLI) to dnf upgrade suitable for automatic, regular execution.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_package_dnf-automatic_installed
References:
os-srgSRG-OS-000191-GPOS-00080
anssiR61

Complexity:low
Disruption:low
Reboot:false
Strategy:enable
- name: Ensure dnf-automatic is installed
  package:
    name: dnf-automatic
    state: present
  tags:
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - package_dnf-automatic_installed

Complexity:low
Disruption:low
Reboot:false
Strategy:enable
include install_dnf-automatic

class install_dnf-automatic {
  package { 'dnf-automatic':
    ensure => 'installed',
  }
}

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

if ! rpm -q --quiet "dnf-automatic" ; then
    dnf install -y "dnf-automatic"
fi

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

package --add=dnf-automatic


[[packages]]
name = "dnf-automatic"
version = "*"

Rule   Configure dnf-automatic to Install Available Updates Automatically   [ref]

To ensure that the packages comprising the available updates will be automatically installed by dnf-automatic, set apply_updates to yes under [commands] section in /etc/dnf/automatic.conf.
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. The automated installation of updates ensures that recent security patches are applied in a timely manner.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_dnf-automatic_apply_updates
References:
ism0940, 1144, 1467, 1472, 1483, 1493, 1494, 1495
nistSI-2(5), CM-6(a), SI-2(c)
osppFMT_SMF_EXT.1
os-srgSRG-OS-000191-GPOS-00080
anssiR61

Complexity:low
Disruption:medium
Reboot:false
Strategy:unknown
- name: Configure dnf-automatic to Install Available Updates Automatically
  ini_file:
    dest: /etc/dnf/automatic.conf
    section: commands
    option: apply_updates
    value: 'yes'
    create: true
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-2(5)
  - NIST-800-53-SI-2(c)
  - dnf-automatic_apply_updates
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
  - unknown_strategy


found=false

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

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

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

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

Rule   Configure dnf-automatic to Install Only Security Updates   [ref]

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

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


found=false

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

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

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

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

Rule   Ensure gpgcheck Enabled In Main dnf Configuration   [ref]

The gpgcheck option controls whether RPM packages' signatures are always checked prior to installation. To configure dnf to check package signatures before installing them, ensure the following line appears in /etc/dnf/dnf.conf in the [main] section:
gpgcheck=1
Rationale:
Changes to any software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor.
Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization.
Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA).
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_ensure_gpgcheck_globally_activated
References:
cis-csc11, 2, 3, 9
cjis5.10.4.1
cobit5APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02
cui3.4.8
disaCCI-001749
hipaa164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i)
isa-62443-20094.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4
isa-62443-2013SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6
iso27001-2013A.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
nistCM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), SA-12, SA-12(10), CM-11(a), CM-11(b)
nist-csfPR.DS-6, PR.DS-8, PR.IP-1
osppFPT_TUD_EXT.1, FPT_TUD_EXT.2
pcidssReq-6.2
os-srgSRG-OS-000366-GPOS-00153
anssiR59
pcidss46.3.3

Complexity:low
Disruption:medium
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.10.4.1
  - NIST-800-171-3.4.8
  - NIST-800-53-CM-11(a)
  - NIST-800-53-CM-11(b)
  - NIST-800-53-CM-5(3)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SA-12
  - NIST-800-53-SA-12(10)
  - NIST-800-53-SC-12
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SI-7
  - PCI-DSS-Req-6.2
  - 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/dnf/dnf.conf
    section: main
    option: gpgcheck
    value: 1
    no_extra_spaces: true
    create: false
  when: '"dnf" in ansible_facts.packages'
  tags:
  - CJIS-5.10.4.1
  - NIST-800-171-3.4.8
  - NIST-800-53-CM-11(a)
  - NIST-800-53-CM-11(b)
  - NIST-800-53-CM-5(3)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SA-12
  - NIST-800-53-SA-12(10)
  - NIST-800-53-SC-12
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SI-7
  - PCI-DSS-Req-6.2
  - 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 dnf; 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/dnf/dnf.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/dnf/dnf.conf"
else
    if [[ -s "/etc/dnf/dnf.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/dnf/dnf.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/dnf/dnf.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/dnf/dnf.conf"
fi

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

Rule   Ensure gpgcheck Enabled for Local Packages   [ref]

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

Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages
References:
cis-csc11, 3, 9
cobit5BAI10.01, BAI10.02, BAI10.03, BAI10.05
cui3.4.8
disaCCI-001749
hipaa164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i)
isa-62443-20094.3.4.3.2, 4.3.4.3.3
isa-62443-2013SR 7.6
iso27001-2013A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4
nistCM-11(a), CM-11(b), CM-6(a), CM-5(3), SA-12, SA-12(10)
nist-csfPR.IP-1
osppFPT_TUD_EXT.1, FPT_TUD_EXT.2
os-srgSRG-OS-000366-GPOS-00153
anssiR59

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

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

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

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

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

# Remediation is applicable only in certain platforms
if rpm --quiet -q dnf; 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/dnf/dnf.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/dnf/dnf.conf"
else
    if [[ -s "/etc/dnf/dnf.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/dnf/dnf.conf" || true)" ]]; then
        LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/dnf/dnf.conf"
    fi
    printf '%s\n' "$formatted_output" >> "/etc/dnf/dnf.conf"
fi

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

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

To ensure signature checking is not disabled for any repos, remove any lines from files in /etc/yum.repos.d of the form:
gpgcheck=0
Rationale:
Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA)."
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_ensure_gpgcheck_never_disabled
References:
cis-csc11, 2, 3, 9
cjis5.10.4.1
cobit5APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02
cui3.4.8
disaCCI-001749
hipaa164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i)
isa-62443-20094.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4
isa-62443-2013SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6
iso27001-2013A.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
nistCM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), SA-12, SA-12(10), CM-11(a), CM-11(b)
nist-csfPR.DS-6, PR.DS-8, PR.IP-1
osppFPT_TUD_EXT.1, FPT_TUD_EXT.2
pcidssReq-6.2
os-srgSRG-OS-000366-GPOS-00153
anssiR59
pcidss46.3.3

Complexity:low
Disruption:medium
Reboot:false
Strategy:enable
- name: Grep for dnf 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
  - 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 dnf repo
  ini_file:
    path: '{{ item[0] }}'
    section: '{{ item[1] }}'
    option: gpgcheck
    value: '1'
    no_extra_spaces: true
  loop: '{{ repo_grep_results.stdout |regex_findall( ''(.+\.repo):\[(.+)\]\n?'' )
    if repo_grep_results is not skipped else [] }}'
  when: repo_grep_results is not skipped
  tags:
  - CJIS-5.10.4.1
  - NIST-800-171-3.4.8
  - NIST-800-53-CM-11(a)
  - NIST-800-53-CM-11(b)
  - NIST-800-53-CM-5(3)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SA-12
  - NIST-800-53-SA-12(10)
  - NIST-800-53-SC-12
  - NIST-800-53-SC-12(3)
  - NIST-800-53-SI-7
  - PCI-DSS-Req-6.2
  - 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 Red Hat GPG Key Installed   [ref]

To ensure the system can cryptographically verify base software packages come from Red Hat (and to connect to the Red Hat Network to receive them), the Red Hat GPG key must properly be installed. To install the Red Hat GPG key, run:
$ sudo subscription-manager register
If the system is not connected to the Internet or an RHN Satellite, then install the Red Hat GPG key from trusted media such as the Red Hat installation CD-ROM or DVD. Assuming the disc is mounted in /media/cdrom, use the following command as the root user to import it into the keyring:
$ sudo rpm --import /media/cdrom/RPM-GPG-KEY
Alternatively, the key may be pre-loaded during the RHEL installation. In such cases, the key can be installed by running the following command:
sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
Rationale:
Changes to software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor. The Red Hat GPG key is necessary to cryptographically verify packages are from Red Hat.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed
References:
cis-csc11, 2, 3, 9
cjis5.10.4.1
cobit5APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02
cui3.4.8
disaCCI-001749
hipaa164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i)
isa-62443-20094.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4
isa-62443-2013SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6
iso27001-2013A.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
nerc-cipCIP-003-8 R4.2, CIP-003-8 R6, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nistCM-5(3), SI-7, SC-12, SC-12(3), CM-6(a)
nist-csfPR.DS-6, PR.DS-8, PR.IP-1
osppFPT_TUD_EXT.1, FPT_TUD_EXT.2
pcidssReq-6.2
os-srgSRG-OS-000366-GPOS-00153
anssiR59
pcidss46.3.3

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

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

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

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

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

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

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

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

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

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

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

Rule   Ensure Software Patches Installed   [ref]



NOTE: U.S. Defense systems are required to be patched within 30 days or sooner as local policy dictates.
Warning:  Red Hat Enterprise Linux 10 does not have a corresponding OVAL CVE Feed. Therefore, this will result in a "not checked" result during a scan.
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
References:
cis-csc18, 20, 4
cjis5.10.4.1
cobit5APO12.01, APO12.02, APO12.03, APO12.04, BAI03.10, DSS05.01, DSS05.02
disaCCI-000366, CCI-001227
isa-62443-20094.2.3, 4.2.3.12, 4.2.3.7, 4.2.3.9
iso27001-2013A.12.6.1, A.14.2.3, A.16.1.3, A.18.2.2, A.18.2.3
nistSI-2(5), SI-2(c), CM-6(a)
nist-csfID.RA-1, PR.IP-12
osppFMT_MOF_EXT.1
pcidssReq-6.2
os-srgSRG-OS-000480-GPOS-00227
anssiR61
pcidss46.3.3

Rule   Enable dnf-automatic Timer   [ref]

The dnf-automatic timer can be enabled with the following command:
$ sudo systemctl enable dnf-automatic.timer
Rationale:
The dnf-automatic is an alternative command line interface (CLI) to dnf upgrade with specific facilities to make it suitable to be executed automatically and regularly from systemd timers, cron jobs and similar. The tool is controlled by dnf-automatic.timer SystemD timer.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_timer_dnf-automatic_enabled
References:
nistSI-2(5), CM-6(a), SI-2(c)
osppFMT_SMF_EXT.1
os-srgSRG-OS-000191-GPOS-00080
anssiR61

Complexity:low
Disruption:low
Reboot:false
Strategy:enable
- name: Enable timer dnf-automatic
  block:

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

  - name: Enable timer dnf-automatic
    systemd:
      name: dnf-automatic.timer
      enabled: 'yes'
      state: started
    when:
    - '"dnf-automatic" in ansible_facts.packages'
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SI-2(5)
  - NIST-800-53-SI-2(c)
  - enable_strategy
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - timer_dnf-automatic_enabled

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

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" start 'dnf-automatic.timer'
"$SYSTEMCTL_EXEC" enable 'dnf-automatic.timer'

Rule   Prefer to use a 64-bit Operating System when supported   [ref]

Prefer installation of 64-bit operating systems when the CPU supports it.
Warning:  There is no remediation besides installing a 64-bit operating system.
Rationale:
Use of a 64-bit operating system offers a few advantages, like a larger address space range for Address Space Layout Randomization (ASLR) and systematic presence of No eXecute and Execute Disable (NX/XD) protection bits.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_prefer_64bit_os
References:
anssiR1
Group   Account and Access Control   Group contains 12 groups and 32 rules
[ref]   In traditional Unix security, if an attacker gains shell access to a certain login account, they can perform any action or access any file to which that account has access. Therefore, making it more difficult for unauthorized people to gain shell access to accounts, particularly to privileged accounts, is a necessary part of securing a system. This section introduces mechanisms for restricting access to accounts under Red Hat Enterprise Linux 10.
Group   Protect Accounts by Configuring PAM   Group contains 4 groups and 13 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 5 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   [ref]

Do not allow users to reuse recent passwords. This can be accomplished by using the remember option for the pam_unix or pam_pwhistory PAM modules.
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_unix_remember
References:
cis-csc1, 12, 15, 16, 5
cjis5.6.2.1.1
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui3.5.8
disaCCI-000200
isa-62443-20094.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
isa-62443-2013SR 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
iso27001-2013A.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
nistIA-5(f), IA-5(1)(e)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
pcidssReq-8.2.5
os-srgSRG-OS-000077-GPOS-00045
anssiR31
pcidss48.3.7

Complexity:low
Disruption:medium
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.1.1
  - NIST-800-171-3.5.8
  - NIST-800-53-IA-5(1)(e)
  - NIST-800-53-IA-5(f)
  - PCI-DSS-Req-8.2.5
  - PCI-DSSv4-8.3.7
  - accounts_password_pam_unix_remember
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
- name: XCCDF Value var_password_pam_unix_remember # promote to variable
  set_fact:
    var_password_pam_unix_remember: !!str 2
  tags:
    - always

- name: Limit Password Reuse - 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
  - 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
  - PCI-DSSv4-8.3.7
  - accounts_password_pam_unix_remember
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: Limit Password Reuse - 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
  - 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
  - PCI-DSSv4-8.3.7
  - accounts_password_pam_unix_remember
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

  - name: Limit Password Reuse - 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 - 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 - 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 - 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 - 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
  - 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
  - PCI-DSSv4-8.3.7
  - accounts_password_pam_unix_remember
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

  - name: Limit Password Reuse - 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 - Check if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

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

    - name: Limit Password Reuse - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - Check if expected PAM module line is present in {{
      pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      regexp: ^\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwhistory.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

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

    - name: Limit Password Reuse - 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 - 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 - 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    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 - 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
  - 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
  - PCI-DSSv4-8.3.7
  - accounts_password_pam_unix_remember
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: Limit Password Reuse - 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
  - 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
  - PCI-DSSv4-8.3.7
  - accounts_password_pam_unix_remember
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

  - name: Limit Password Reuse - 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_unix_remember }}
      state: present

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

    - name: Limit Password Reuse - 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 - Check the proper remediation for the system
      block:

      - name: Limit Password Reuse - 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 - Check if system relies on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

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

        - name: Limit Password Reuse - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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
  - 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
  - PCI-DSSv4-8.3.7
  - accounts_password_pam_unix_remember
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

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

  - name: Limit Password Reuse - 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 - Check if system relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

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

    - name: Limit Password Reuse - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - Check if expected PAM module line is present in {{
      pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      regexp: ^\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwhistory.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

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

    - name: Limit Password Reuse - 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 - 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 - 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 - 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 - 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' | regex_escape() }}\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 - 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' | regex_escape() }}\s+pam_pwhistory.so.*)
      line: \1 remember={{ var_password_pam_unix_remember }}
      state: present
    register: result_pam_remember_add
    when:
    - result_pam_module_remember_option_present.found == 0

  - name: Limit Password Reuse - 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' | regex_escape() }}\s+pam_pwhistory.so\s+.*)(remember)=[0-9a-zA-Z]+\s*(.*)
      line: \1\2={{ var_password_pam_unix_remember }} \3
    register: result_pam_remember_edit
    when:
    - result_pam_module_remember_option_present.found > 0

  - name: Limit Password Reuse - 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
  - 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
  - PCI-DSSv4-8.3.7
  - accounts_password_pam_unix_remember
  - 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_unix_remember='2'






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+'"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
                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     '"requisite"'    pam_pwhistory.so' "$PAM_FILE_PATH"
                else
                    echo 'password    '"requisite"'    pam_pwhistory.so' >> "$PAM_FILE_PATH"
                fi
            fi
        fi
    fi
else
    if ! grep -qP '^\s*password\s+'"requisite"'\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'"requisite"' \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     '"requisite"'    pam_pwhistory.so' "/etc/pam.d/system-auth"
            else
                echo 'password    '"requisite"'    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_unix_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_unix_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_unix_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_unix_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   Lock Accounts After Failed Password Attempts   [ref]

This rule configures the system to lock out accounts after a number of incorrect login attempts using pam_faillock.so. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. Ensure that the file /etc/security/faillock.conf contains the following entry: deny = <count> Where count should be less than or equal to 3 and greater than 0. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
By limiting the number of failed logon attempts, the risk of unauthorized system access via user password guessing, also known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny
References:
cis-csc1, 12, 15, 16
cjis5.5.3
cobit5DSS05.04, DSS05.10, DSS06.10
cui3.1.8
disaCCI-000044, CCI-002236, CCI-002237, CCI-002238
isa-62443-20094.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
isa-62443-2013SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nistCM-6(a), AC-7(a)
nist-csfPR.AC-7
osppFIA_AFL.1
pcidssReq-8.1.6
os-srgSRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005
anssiR31
pcidss48.3.4

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.3
  - NIST-800-171-3.1.8
  - NIST-800-53-AC-7(a)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.6
  - 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
  - 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
  - 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
  - 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
  - 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
  - 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
  - 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
  - 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
References:
cis-csc1, 12, 15, 16
cobit5DSS05.04, DSS05.10, DSS06.10
disaCCI-002238, CCI-000044
isa-62443-20094.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
isa-62443-2013SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nistCM-6(a), AC-7(b), IA-5(c)
nist-csfPR.AC-7
osppFMT_MOF_EXT.1
os-srgSRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005
anssiR31

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-53-AC-7(b)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(c)
  - accounts_passwords_pam_faillock_deny_root
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: 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:
  - 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:
  - 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:
  - 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:
  - 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:
  - 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:
  - 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:
  - 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   Set Interval For Counting Failed Password Attempts   [ref]

Utilizing pam_faillock.so, the fail_interval directive configures the system to lock out an account after a number of incorrect login attempts within a specified time period. Ensure that the file /etc/security/faillock.conf contains the following entry: fail_interval = <interval-in-seconds> where interval-in-seconds is 900 or greater. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval
References:
cis-csc1, 12, 15, 16
cobit5DSS05.04, DSS05.10, DSS06.10
disaCCI-000044, CCI-002236, CCI-002237, CCI-002238
isa-62443-20094.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
isa-62443-2013SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nistCM-6(a), AC-7(a)
nist-csfPR.AC-7
osppFIA_AFL.1
os-srgSRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005
anssiR31

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-53-AC-7(a)
  - NIST-800-53-CM-6(a)
  - accounts_passwords_pam_faillock_interval
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: 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:
  - 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:
  - 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:
  - 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:
  - 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:
  - 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:
  - 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:
  - 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   Set Lockout Time for Failed Password Attempts   [ref]

This rule configures the system to lock out accounts during a specified time period after a number of incorrect login attempts using pam_faillock.so. Ensure that the file /etc/security/faillock.conf contains the following entry: unlock_time=<interval-in-seconds> where interval-in-seconds is 900 or greater. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid any errors when manually editing these files, it is recommended to use the appropriate tools, such as authselect or authconfig, depending on the OS version. If unlock_time is set to 0, manual intervention by an administrator is required to unlock a user. This should be done using the faillock tool.
Warning:  If the system supports the new /etc/security/faillock.conf file but the pam_faillock.so parameters are defined directly in /etc/pam.d/system-auth and /etc/pam.d/password-auth, the remediation will migrate the unlock_time parameter to /etc/security/faillock.conf to ensure compatibility with authselect tool. The parameters deny and fail_interval, if used, also have to be migrated by their respective remediation.
Warning:  If the system relies on authselect tool to manage PAM settings, the remediation will also use authselect tool. However, if any manual modification was made in PAM files, the authselect integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the /etc/security/faillock.conf file, the pam_faillock parameters should be defined in faillock.conf file.
Rationale:
By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time
References:
cis-csc1, 12, 15, 16
cjis5.5.3
cobit5DSS05.04, DSS05.10, DSS06.10
cui3.1.8
disaCCI-000044, CCI-002236, CCI-002237, CCI-002238
isa-62443-20094.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
isa-62443-2013SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nistCM-6(a), AC-7(b)
nist-csfPR.AC-7
osppFIA_AFL.1
pcidssReq-8.1.7
os-srgSRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005
anssiR31
pcidss48.3.4

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.3
  - NIST-800-171-3.1.8
  - NIST-800-53-AC-7(b)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-8.1.7
  - 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
  - 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
  - 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
  - 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 900
  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
  - 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
  - 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
  - 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
  - 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='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*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 7 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 7 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: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit
References:
cis-csc1, 12, 15, 16, 5
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
disaCCI-000194
isa-62443-20094.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
isa-62443-2013SR 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
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.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
nistIA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
osppFMT_SMF_EXT.1
pcidssReq-8.2.3
os-srgSRG-OS-000071-GPOS-00039
anssiR31
pcidss48.3.6

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - 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 - 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:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - accounts_password_pam_dcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

var_password_pam_dcredit='-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' <<< "^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 - 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: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit
References:
cis-csc1, 12, 15, 16, 5
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
disaCCI-000193
isa-62443-20094.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
isa-62443-2013SR 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
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.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
nistIA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
osppFMT_SMF_EXT.1
pcidssReq-8.2.3
os-srgSRG-OS-000070-GPOS-00038
anssiR31
pcidss48.3.6

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - 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 -
    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:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - accounts_password_pam_lcredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

var_password_pam_lcredit='-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' <<< "^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 - 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
References:
cis-csc1, 12, 15, 16, 5
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
disaCCI-000195
isa-62443-20094.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
isa-62443-2013SR 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
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.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
nistIA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
os-srgSRG-OS-000072-GPOS-00040
anssiR68

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - accounts_password_pam_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 -
    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:
  - 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
Reboot:false
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_minclass='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' <<< "^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
References:
cis-csc1, 12, 15, 16, 5
cjis5.6.2.1.1
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
disaCCI-000205
isa-62443-20094.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
isa-62443-2013SR 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
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.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
nistIA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
osppFMT_SMF_EXT.1
pcidssReq-8.2.3
os-srgSRG-OS-000078-GPOS-00046
anssiR31, R68
pcidss48.3.6

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.1.1
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - PCI-DSSv4-8.3.6
  - accounts_password_pam_minlen
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_minlen # promote to variable
  set_fact:
    var_password_pam_minlen: !!str 15
  tags:
    - always

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

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

var_password_pam_minlen='15'






# 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: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit
References:
cis-csc1, 12, 15, 16, 5
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
disaCCI-001619
isa-62443-20094.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
isa-62443-2013SR 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
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.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
nistIA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
osppFMT_SMF_EXT.1
os-srgSRG-OS-000266-GPOS-00101
anssiR31

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - accounts_password_pam_ocredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_ocredit # promote to variable
  set_fact:
    var_password_pam_ocredit: !!str -1
  tags:
    - always

- name: Ensure PAM 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:
  - 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
Reboot:false
Strategy:restrict
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then

var_password_pam_ocredit='-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' <<< "^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 Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session   [ref]

To configure the number of retry prompts that are permitted per-session: Edit the pam_pwquality.so statement in /etc/pam.d/system-auth to show 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
References:
cis-csc1, 11, 12, 15, 16, 3, 5, 9
cjis5.5.3
cobit5BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
disaCCI-000192, CCI-000366
isa-62443-20094.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
isa-62443-2013SR 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
iso27001-2013A.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
nistCM-6(a), AC-7(a), IA-5(4)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7, PR.IP-1
osppFMT_MOF_EXT.1
os-srgSRG-OS-000069-GPOS-00037, SRG-OS-000480-GPOS-00227
anssiR68

Complexity:low
Disruption:medium
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.3
  - 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 Enforces Password Requirements - Authentication Retry Prompts Permitted
    Per-Session - 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*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwquality.so\s*.*
    state: absent
  check_mode: true
  changed_when: false
  register: result_pam_line_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.5.3
  - 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 - Include or update the PAM module line in /etc/pam.d/system-auth
  block:

  - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
      Permitted Per-Session - 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*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 Enforces Password Requirements - Authentication Retry Prompts
      Permitted Per-Session - 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*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 Enforces Password Requirements - Authentication Retry Prompts
      Permitted Per-Session - Ensure the required PAM module line is included in /etc/pam.d/system-auth
    ansible.builtin.lineinfile:
      dest: /etc/pam.d/system-auth
      insertafter: ^\s*account
      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 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 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:
  - '"pam" in ansible_facts.packages'
  - result_pam_line_present.found is defined
  - result_pam_line_present.found == 0
  tags:
  - CJIS-5.5.3
  - 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 the required PAM module option is present in /etc/pam.d/system-auth
  ansible.builtin.lineinfile:
    path: /etc/pam.d/system-auth
    regexp: ^\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwquality.so\s*.*\sretry\b
    state: absent
  check_mode: true
  changed_when: false
  register: result_pam_module_retry_option_present
  when: '"pam" in ansible_facts.packages'
  tags:
  - CJIS-5.5.3
  - 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 - Ensure the "retry" PAM option for "pam_pwquality.so" is included
    in /etc/pam.d/system-auth
  ansible.builtin.lineinfile:
    path: /etc/pam.d/system-auth
    backrefs: true
    regexp: ^(\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwquality.so.*)
    line: \1 retry={{ var_password_pam_retry }}
    state: present
  register: result_pam_retry_add
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_module_retry_option_present.found == 0
  tags:
  - CJIS-5.5.3
  - 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 - Ensure the required value for "retry" PAM option from "pam_pwquality.so"
    in /etc/pam.d/system-auth
  ansible.builtin.lineinfile:
    path: /etc/pam.d/system-auth
    backrefs: true
    regexp: ^(\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwquality.so\s+.*)(retry)=[0-9a-zA-Z]+\s*(.*)
    line: \1\2={{ var_password_pam_retry }} \3
  register: result_pam_retry_edit
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_module_retry_option_present.found > 0
  tags:
  - CJIS-5.5.3
  - 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'



	
		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 "^\s*account" "$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
        # Check the option
        if ! grep -qP '^\s*password\s+'"requisite"'\s+pam_pwquality.so\s*.*\sretry\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks '/\s*password\s+'"requisite"'\s+pam_pwquality.so.*/ s/$/ retry='"$var_password_pam_retry"'/' "$PAM_FILE_PATH"
        else
            sed -i -E --follow-symlinks 's/(\s*password\s+'"requisite"'\s+pam_pwquality.so\s+.*)('"retry"'=)[[:alnum:]]+\s*(.*)/\1\2'"$var_password_pam_retry"' \3/' "$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: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit
References:
cis-csc1, 12, 15, 16, 5
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
disaCCI-000192, CCI-000193
isa-62443-20094.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
isa-62443-2013SR 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
ism0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
iso27001-2013A.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
nistIA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
osppFMT_SMF_EXT.1
pcidssReq-8.2.3
os-srgSRG-OS-000069-GPOS-00037, SRG-OS-000070-GPOS-00038
anssiR31

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(a)
  - NIST-800-53-IA-5(4)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.3
  - accounts_password_pam_ucredit
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- name: XCCDF Value var_password_pam_ucredit # promote to variable
  set_fact:
    var_password_pam_ucredit: !!str -1
  tags:
    - always

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

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

var_password_pam_ucredit='-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' <<< "^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 1 rule
[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 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
References:
cis-csc1, 12, 15, 16, 5
cjis5.6.2.2
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui3.13.11
disaCCI-000196, CCI-000803
isa-62443-20094.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
isa-62443-2013SR 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
ism0418, 1055, 1402
iso27001-2013A.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
nistIA-5(c), IA-5(1)(c), CM-6(a)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
pcidssReq-8.2.1
os-srgSRG-OS-000073-GPOS-00041, SRG-OS-000120-GPOS-00061
anssiR68
pcidss48.3.2

Complexity:low
Disruption:medium
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.6.2.2
  - NIST-800-171-3.13.11
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-5(1)(c)
  - NIST-800-53-IA-5(c)
  - PCI-DSS-Req-8.2.1
  - 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
  - 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' | regex_escape() }}\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' | regex_escape() }}\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' | regex_escape() }}\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
  - 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
Group   Protect Physical Console Access   Group contains 1 rule
[ref]   It is impossible to fully protect a system from an attacker with physical access, so securing the space in which the system is located should be considered a necessary step. However, there are some steps which, if taken, make it more difficult for an attacker to quickly or undetectably modify a system from its console.

Rule   Configure Logind to terminate idle sessions after certain time of inactivity   [ref]

To configure logind service to terminate inactive user sessions after 600 seconds, edit the file /etc/systemd/logind.conf. Ensure that there is a section
[Login]
which contains the configuration
StopIdleSessionSec=600
        
.
Rationale:
Terminating an idle 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_logind_session_timeout
References:
cis-csc1, 12, 13, 14, 15, 16, 18, 3, 5, 7, 8
cjis5.5.6
cobit5APO13.01, BAI03.01, BAI03.02, BAI03.03, DSS01.03, DSS03.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui3.1.11
isa-62443-20094.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
isa-62443-2013SR 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
iso27001-2013A.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
nerc-cipCIP-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
nistCM-6(a), AC-17(a), AC-2(5), AC-12, AC-17(a), SC-10, CM-6(a)
nist-csfDE.CM-1, DE.CM-3, PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.IP-2
osppFMT_SMF_EXT.1.1
pcidssReq-8.1.8
os-srgSRG-OS-000163-GPOS-00072
anssiR32

Complexity:low
Disruption:low
Reboot:true
Strategy:restrict
- name: XCCDF Value var_logind_session_timeout # promote to variable
  set_fact:
    var_logind_session_timeout: !!str 600
  tags:
    - always

- name: Set 'StopIdleSessionSec' to '{{ var_logind_session_timeout }}' in the [Login]
    section of '/etc/systemd/logind.conf'
  ini_file:
    path: /etc/systemd/logind.conf
    section: Login
    option: StopIdleSessionSec
    value: '{{ var_logind_session_timeout }}'
    create: true
    mode: 420
  when:
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - ( ansible_distribution == 'RedHat' and ansible_distribution_version is version('8.7',
    '>=') and ansible_distribution == 'RedHat' and ansible_distribution_version is
    version('9.0', '!=') ) or ansible_distribution == 'OracleLinux' and ansible_distribution_version
    is version('8.7', '>=')
  tags:
  - CJIS-5.5.6
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-17(a)
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSS-Req-8.1.8
  - logind_session_timeout
  - low_complexity
  - low_disruption
  - medium_severity
  - reboot_required
  - restrict_strategy

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

var_logind_session_timeout='600'



# Try find '[Login]' and 'StopIdleSessionSec' in '/etc/systemd/logind.conf', if it exists, set
# to '$var_logind_session_timeout', if it isn't here, add it, if '[Login]' doesn't exist, add it there
if grep -qzosP '[[:space:]]*\[Login]([^\n\[]*\n+)+?[[:space:]]*StopIdleSessionSec' '/etc/systemd/logind.conf'; then
    
    sed -i "s/StopIdleSessionSec[^(\n)]*/StopIdleSessionSec=$var_logind_session_timeout/" '/etc/systemd/logind.conf'
elif grep -qs '[[:space:]]*\[Login]' '/etc/systemd/logind.conf'; then
    sed -i "/[[:space:]]*\[Login]/a StopIdleSessionSec=$var_logind_session_timeout" '/etc/systemd/logind.conf'
else
    if test -d "/etc/systemd"; then
        printf '%s\n' '[Login]' "StopIdleSessionSec=$var_logind_session_timeout" >> '/etc/systemd/logind.conf'
    else
        echo "Config file directory '/etc/systemd' doesnt exist, not remediating, assuming non-applicability." >&2
    fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Group   Protect Accounts by Restricting Password-Based Login   Group contains 3 groups and 5 rules
[ref]   Conventionally, Unix shell accounts are accessed by providing a username and password to a login program, which tests these values for correctness using the /etc/passwd and /etc/shadow files. Password-based login is vulnerable to guessing of weak passwords, and to sniffing and man-in-the-middle attacks against passwords entered over a network or at an insecure console. Therefore, mechanisms for accessing accounts by entering usernames and passwords should be restricted to those which are operationally necessary.
Group   Set Password Expiration Parameters   Group contains 2 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 Root Account Password Maximum Age   [ref]

Configure the root account to enforce a 365-day maximum password lifetime restriction by running the following command:
$ sudo chage -M 365 root
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_root
References:
anssiR31

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: XCCDF Value var_accounts_maximum_age_root # promote to variable
  set_fact:
    var_accounts_maximum_age_root: !!str 365
  tags:
    - always

- name: Change the maximum time period between password changes
  ansible.builtin.user:
    user: root
    password_expire_max: '{{ var_accounts_maximum_age_root }}'
  tags:
  - accounts_password_set_max_life_root
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

var_accounts_maximum_age_root='365'

chage -M $var_accounts_maximum_age_root root
Group   Verify Proper Storage and Existence of Password Hashes   Group contains 2 rules
[ref]   By default, password hashes for local accounts are stored in the second field (colon-separated) in /etc/shadow. This file should be readable only by processes running with root credentials, preventing users from casually accessing others' password hashes and attempting to crack them. However, it remains possible to misconfigure the system and store password hashes in world-readable files such as /etc/passwd, or to even store passwords themselves in plaintext on the system. Using system-provided tools for password change/creation should allow administrators to avoid such misconfiguration.

Rule   Set number of Password Hashing Rounds - password-auth   [ref]

Configure the number or rounds for the password hashing algorithm. This can be accomplished by using the rounds option for the pam_unix PAM module.

In file /etc/pam.d/password-auth append rounds=11 to the pam_unix.so entry, as shown below:
password sufficient pam_unix.so ...existing_options... rounds=11
         
The system's default number of rounds is 5000.
Warning:  Setting a high number of hashing rounds makes it more difficult to brute force the password, but requires more CPU resources to authenticate users.
Rationale:
Using a higher number of rounds makes password cracking attacks more difficult.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_rounds_password_auth
References:
disaCCI-000196
os-srgSRG-OS-000073-GPOS-00041
anssiR68

Complexity:low
Disruption:medium
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - accounts_password_pam_unix_rounds_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
- name: XCCDF Value var_password_pam_unix_rounds # promote to variable
  set_fact:
    var_password_pam_unix_rounds: !!str 11
  tags:
    - always

- name: Set number of Password Hashing Rounds - 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:
  - accounts_password_pam_unix_rounds_password_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: Set number of Password Hashing Rounds - password-auth - Check the proper remediation
    for the system
  block:

  - name: Set number of Password Hashing Rounds - 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 number of Password Hashing Rounds - password-auth - Check if system
      relies on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect
      custom profile is used if authselect is present
    block:

    - name: Set number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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' | regex_escape() }}\s+pam_unix.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

  - name: Set number of Password Hashing Rounds - password-auth - Include or update
      the PAM module line in {{ pam_file_path }}
    block:

    - name: Set number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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 number of Password Hashing Rounds - 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' | regex_escape() }}\s+pam_unix.so\s*.*\srounds\b
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_module_rounds_option_present

  - name: Set number of Password Hashing Rounds - password-auth - Ensure the "rounds"
      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' | regex_escape() }}\s+pam_unix.so.*)
      line: \1 rounds={{ var_password_pam_unix_rounds }}
      state: present
    register: result_pam_rounds_add
    when:
    - result_pam_module_rounds_option_present.found == 0

  - name: Set number of Password Hashing Rounds - password-auth - Ensure the required
      value for "rounds" PAM option from "pam_unix.so" in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      backrefs: true
      regexp: ^(\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s+.*)(rounds)=[0-9a-zA-Z]+\s*(.*)
      line: \1\2={{ var_password_pam_unix_rounds }} \3
    register: result_pam_rounds_edit
    when:
    - result_pam_module_rounds_option_present.found > 0

  - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect
      changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - |-
      (result_pam_rounds_add is defined and result_pam_rounds_add.changed)
       or (result_pam_rounds_edit is defined and result_pam_rounds_edit.changed)
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - accounts_password_pam_unix_rounds_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_unix_rounds='11'



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*.*\srounds\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks '/\s*password\s+'"sufficient"'\s+pam_unix.so.*/ s/$/ rounds='"$var_password_pam_unix_rounds"'/' "$PAM_FILE_PATH"
        else
            sed -i -E --follow-symlinks 's/(\s*password\s+'"sufficient"'\s+pam_unix.so\s+.*)('"rounds"'=)[[:alnum:]]+\s*(.*)/\1\2'"$var_password_pam_unix_rounds"' \3/' "$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 number of Password Hashing Rounds - system-auth   [ref]

Configure the number or rounds for the password hashing algorithm. This can be accomplished by using the rounds option for the pam_unix PAM module.

In file /etc/pam.d/system-auth append rounds=11 to the pam_unix.so entry, as shown below:
password sufficient pam_unix.so ...existing_options... rounds=11
         
The system's default number of rounds is 5000.
Warning:  Setting a high number of hashing rounds makes it more difficult to brute force the password, but requires more CPU resources to authenticate users.
Rationale:
Using a higher number of rounds makes password cracking attacks more difficult.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_rounds_system_auth
References:
disaCCI-000196
os-srgSRG-OS-000073-GPOS-00041
anssiR68

Complexity:low
Disruption:medium
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - accounts_password_pam_unix_rounds_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed
- name: XCCDF Value var_password_pam_unix_rounds # promote to variable
  set_fact:
    var_password_pam_unix_rounds: !!str 11
  tags:
    - always

- name: Set number of Password Hashing Rounds - 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:
  - accounts_password_pam_unix_rounds_system_auth
  - configure_strategy
  - low_complexity
  - medium_disruption
  - medium_severity
  - no_reboot_needed

- name: Set number of Password Hashing Rounds - system-auth - Check the proper remediation
    for the system
  block:

  - name: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - system-auth - Check if system relies
      on authselect tool
    ansible.builtin.stat:
      path: /usr/bin/authselect
    register: result_authselect_present

  - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect
      custom profile is used if authselect is present
    block:

    - name: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s*.*
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_line_present

  - name: Set number of Password Hashing Rounds - system-auth - Include or update
      the PAM module line in {{ pam_file_path }}
    block:

    - name: Set number of Password Hashing Rounds - 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_unix.so\s*
        state: absent
      check_mode: true
      changed_when: false
      register: result_pam_line_other_control_present

    - name: Set number of Password Hashing Rounds - 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_unix.so.*)
        replace: \1sufficient \2
      register: result_pam_module_edit
      when:
      - result_pam_line_other_control_present.found == 1

    - name: Set number of Password Hashing Rounds - system-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 number of Password Hashing Rounds - 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: Set number of Password Hashing Rounds - 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+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s*.*\srounds\b
      state: absent
    check_mode: true
    changed_when: false
    register: result_pam_module_rounds_option_present

  - name: Set number of Password Hashing Rounds - system-auth - Ensure the "rounds"
      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' | regex_escape() }}\s+pam_unix.so.*)
      line: \1 rounds={{ var_password_pam_unix_rounds }}
      state: present
    register: result_pam_rounds_add
    when:
    - result_pam_module_rounds_option_present.found == 0

  - name: Set number of Password Hashing Rounds - system-auth - Ensure the required
      value for "rounds" PAM option from "pam_unix.so" in {{ pam_file_path }}
    ansible.builtin.lineinfile:
      path: '{{ pam_file_path }}'
      backrefs: true
      regexp: ^(\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s+.*)(rounds)=[0-9a-zA-Z]+\s*(.*)
      line: \1\2={{ var_password_pam_unix_rounds }} \3
    register: result_pam_rounds_edit
    when:
    - result_pam_module_rounds_option_present.found > 0

  - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect
      changes are applied
    ansible.builtin.command:
      cmd: authselect apply-changes -b
    when:
    - result_authselect_present.stat.exists
    - |-
      (result_pam_rounds_add is defined and result_pam_rounds_add.changed)
       or (result_pam_rounds_edit is defined and result_pam_rounds_edit.changed)
  when:
  - '"pam" in ansible_facts.packages'
  - result_pam_file_present.stat.exists
  tags:
  - accounts_password_pam_unix_rounds_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_unix_rounds='11'


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*.*\srounds\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks '/\s*password\s+'"sufficient"'\s+pam_unix.so.*/ s/$/ rounds='"$var_password_pam_unix_rounds"'/' "$PAM_FILE_PATH"
        else
            sed -i -E --follow-symlinks 's/(\s*password\s+'"sufficient"'\s+pam_unix.so\s+.*)('"rounds"'=)[[:alnum:]]+\s*(.*)/\1\2'"$var_password_pam_unix_rounds"' \3/' "$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
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   Direct root Logins Not Allowed   [ref]

To further limit access to the root account, administrators can disable root logins at the console by editing the /etc/securetty file. This file lists all devices the root user is allowed to login to. If the file does not exist at all, the root user can login through any communication device on the system, whether via the console or via a raw network interface. This is dangerous as user can login to the system as root via Telnet, which sends the password in plain text over the network. By default, Red Hat Enterprise Linux 10's /etc/securetty file only allows the root user to login at the console physically attached to the system. To prevent root from logging in, remove the contents of this file. To prevent direct root logins, remove the contents of this file by typing the following command:
$ sudo echo > /etc/securetty
Warning:  This rule only checks the /etc/securetty file existence and its content. If you need to restrict user access using the /etc/securetty file, make sure the pam_securetty.so PAM module is properly enabled in relevant PAM files.
Rationale:
Disabling direct root logins ensures proper accountability and multifactor authentication to privileged accounts. Users will first login, then escalate to privileged (root) access via su / sudo. This is required for FISMA Low and FISMA Moderate systems.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_no_direct_root_logins
References:
cis-csc1, 12, 15, 16, 5
cobit5DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui3.1.1, 3.1.6
hipaa164.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)
isa-62443-20094.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
isa-62443-2013SR 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
iso27001-2013A.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
nerc-cipCIP-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
nistIA-2, CM-6(a)
nist-csfPR.AC-1, PR.AC-6, PR.AC-7
anssiR33
pcidss48.6.1

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Direct root Logins Not Allowed
  copy:
    dest: /etc/securetty
    content: ''
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - NIST-800-171-3.1.1
  - NIST-800-171-3.1.6
  - NIST-800-53-CM-6(a)
  - NIST-800-53-IA-2
  - PCI-DSSv4-8.6.1
  - low_complexity
  - low_disruption
  - medium_severity
  - no_direct_root_logins
  - no_reboot_needed
  - restrict_strategy

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

echo > /etc/securetty

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

---
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
spec:
  config:
    ignition:
      version: 3.1.0
    storage:
      files:
      - contents:
          source: data:,
        mode: 0600
        path: /etc/securetty
        overwrite: true
Group   Secure Session Configuration Files for Login Accounts   Group contains 1 group and 12 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 3 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
References:
cis-csc18
cobit5APO13.01, BAI03.01, BAI03.02, BAI03.03
disaCCI-000366
isa-62443-20094.3.4.3.3
iso27001-2013A.14.1.1, A.14.2.1, A.14.2.5, A.6.1.5
nerc-cipCIP-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
nistAC-6(1), CM-6(a)
nist-csfPR.IP-2
os-srgSRG-OS-000480-GPOS-00228, SRG-OS-000480-GPOS-00227
anssiR36

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - accounts_umask_etc_bashrc
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy
- 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: ^[^#]*\bumask\s+\d+$
    state: absent
  check_mode: true
  changed_when: false
  register: umask_replace
  when: '"bash" in ansible_facts.packages'
  tags:
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - 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: ^([^#]*\b)umask\s+\d+$
    replace: \g<1>umask {{ var_accounts_user_umask }}
  when:
  - '"bash" in ansible_facts.packages'
  - umask_replace.found > 0
  tags:
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - 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:
  - '"bash" in ansible_facts.packages'
  - umask_replace.found == 0
  tags:
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - accounts_umask_etc_bashrc
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

var_accounts_user_umask='077'






grep -q "^[^#]*\bumask" /etc/bashrc && \
  sed -i -E -e "s/^([^#]*\bumask).*/\1 $var_accounts_user_umask/g" /etc/bashrc
if ! [ $? -eq 0 ]; then
    echo "umask $var_accounts_user_umask" >> /etc/bashrc
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
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
References:
cis-csc18
cobit5APO13.01, BAI03.01, BAI03.02, BAI03.03
disaCCI-000366
isa-62443-20094.3.4.3.3
iso27001-2013A.14.1.1, A.14.2.1, A.14.2.5, A.6.1.5
nerc-cipCIP-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
nistAC-6(1), CM-6(a)
nist-csfPR.IP-2
os-srgSRG-OS-000480-GPOS-00228, SRG-OS-000480-GPOS-00227
anssiR36

Complexity:low
Disruption:low
Reboot:false
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:
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - 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:
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - 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:
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - 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:
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - accounts_umask_etc_profile
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Reboot:false
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 -E "s/^(\s*umask\s*)[0-7]+/\1$var_accounts_user_umask/g" "$file"
done

if ! grep -qrE '^[^#]*umask' /etc/profile*; then
  echo "umask $var_accounts_user_umask" >> /etc/profile
fi

Rule   Configure Polyinstantiation of /tmp Directories   [ref]

To configure polyinstantiated /tmp directories, first create the parent directories which will hold the polyinstantiation child directories. Use the following command:
$ sudo mkdir --mode 000 /tmp/tmp-inst
Then, add the following entry to /etc/security/namespace.conf:
/tmp     /tmp/tmp-inst/            level      root,adm
Rationale:
Polyinstantiation of temporary directories is a proactive security measure which reduces chances of attacks that are made possible by /tmp directories being world-writable.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_accounts_polyinstantiated_tmp
References:
anssiR55

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Create /tmp/tmp-inst directory
  file:
    path: /tmp/tmp-inst
    state: directory
    mode: '000'
    seuser: system_u
    serole: object_r
    setype: tmp_t
  tags:
  - accounts_polyinstantiated_tmp
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

- name: Make changes to /etc/security/namespace.conf
  lineinfile:
    path: /etc/security/namespace.conf
    create: false
    regexp: ^\s*/tmp\s+/tmp/tmp-inst/\s+level\s+root,adm$
    line: /tmp     /tmp/tmp-inst/        level      root,adm
    state: present
  tags:
  - accounts_polyinstantiated_tmp
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy


# shellcheck disable=SC2174
mkdir -p --mode 000 /tmp/tmp-inst
chmod 000 /tmp/tmp-inst
chcon --reference=/tmp /tmp/tmp-inst

if ! grep -Eq '^\s*/tmp\s+/tmp/tmp-inst/\s+level\s+root,adm$' /etc/security/namespace.conf ; then
    if grep -Eq '^\s*/tmp\s+' /etc/security/namespace.conf ; then
        sed -i '/^\s*\/tmp/d' /etc/security/namespace.conf
    fi
    echo "/tmp     /tmp/tmp-inst/        level      root,adm" >> /etc/security/namespace.conf
fi

Rule   Configure Polyinstantiation of /var/tmp Directories   [ref]

To configure polyinstantiated /tmp directories, first create the parent directories which will hold the polyinstantiation child directories. Use the following command:
$ sudo mkdir --mode 000 /var/tmp/tmp-inst
Then, add the following entry to /etc/security/namespace.conf:
/var/tmp /var/tmp/tmp-inst/    level      root,adm
Rationale:
Polyinstantiation of temporary directories is a proactive security measure which reduces chances of attacks that are made possible by /var/tmp directories being world-writable.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_accounts_polyinstantiated_var_tmp
References:
anssiR55

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Create /var/tmp/tmp-inst directory
  file:
    path: /var/tmp/tmp-inst
    state: directory
    mode: '000'
    seuser: system_u
    serole: object_r
    setype: tmp_t
  tags:
  - accounts_polyinstantiated_var_tmp
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy

- name: Make changes to /etc/security/namespace.conf
  lineinfile:
    path: /etc/security/namespace.conf
    create: false
    regexp: ^\s*/var/tmp\s+/var/tmp/tmp-inst/\s+level\s+root,adm$
    line: /var/tmp /var/tmp/tmp-inst/    level      root,adm
    state: present
  tags:
  - accounts_polyinstantiated_var_tmp
  - low_complexity
  - low_disruption
  - low_severity
  - no_reboot_needed
  - restrict_strategy


# shellcheck disable=SC2174
mkdir -p --mode 000 /var/tmp/tmp-inst
chmod 000 /var/tmp/tmp-inst
chcon --reference=/var/tmp /var/tmp/tmp-inst

if ! grep -Eq '^\s*/var/tmp\s+/var/tmp/tmp-inst/\s+level\s+root,adm$' /etc/security/namespace.conf ; then
    if grep -Eq '^\s*/var/tmp\s+' /etc/security/namespace.conf ; then
        sed -i '/^\s*\/var\/tmp/d' /etc/security/namespace.conf
    fi
    echo "/var/tmp /var/tmp/tmp-inst/    level      root,adm" >> /etc/security/namespace.conf
fi

Rule   Set Interactive Session Timeout   [ref]

Setting the TMOUT option in /etc/profile ensures that all user sessions will terminate based on inactivity. The value of TMOUT should be exported and read only. The TMOUT setting in a file loaded by /etc/profile, e.g. /etc/profile.d/tmout.sh should read as follows:
typeset -xr TMOUT=600
        
or
declare -xr TMOUT=600
        
Using the typeset keyword is preferred for wider compatibility with ksh and other shells.
Rationale:
Terminating an idle 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 left unattended.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_tmout
References:
cis-csc1, 12, 15, 16
cobit5DSS05.04, DSS05.10, DSS06.10
cui3.1.11
disaCCI-000057, CCI-001133, CCI-002361
isa-62443-20094.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
isa-62443-2013SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
iso27001-2013A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nerc-cipCIP-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
nistAC-12, SC-10, AC-2(5), CM-6(a)
nist-csfPR.AC-7
osppFMT_MOF_EXT.1
os-srgSRG-OS-000163-GPOS-00072, SRG-OS-000029-GPOS-00010
anssiR32
pcidss48.6.1

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: XCCDF Value var_accounts_tmout # promote to variable
  set_fact:
    var_accounts_tmout: !!str 600
  tags:
    - always

- name: Correct any occurrence of TMOUT in /etc/profile
  replace:
    path: /etc/profile
    regexp: ^[^#].*TMOUT=.*
    replace: typeset -xr TMOUT={{ var_accounts_tmout }}
  register: profile_replaced
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSSv4-8.6.1
  - accounts_tmout
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Set Interactive Session Timeout
  lineinfile:
    path: /etc/profile.d/tmout.sh
    create: true
    regexp: TMOUT=
    line: typeset -xr TMOUT={{ var_accounts_tmout }}
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - NIST-800-171-3.1.11
  - NIST-800-53-AC-12
  - NIST-800-53-AC-2(5)
  - NIST-800-53-CM-6(a)
  - NIST-800-53-SC-10
  - PCI-DSSv4-8.6.1
  - accounts_tmout
  - 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

var_accounts_tmout='600'


# if 0, no occurence of tmout found, if 1, occurence found
tmout_found=0


for f in /etc/profile /etc/profile.d/*.sh; do

    if grep --silent '^[^#].*TMOUT' $f; then
        sed -i -E "s/^(.*)TMOUT\s*=\s*(\w|\$)*(.*)$/typeset -xr TMOUT=$var_accounts_tmout\3/g" $f
        tmout_found=1
    fi
done

if [ $tmout_found -eq 0 ]; then
        echo -e "\n# Set TMOUT to $var_accounts_tmout per security requirements" >> /etc/profile.d/tmout.sh
        echo "typeset -xr TMOUT=$var_accounts_tmout" >> /etc/profile.d/tmout.sh
fi

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

Rule   User Initialization Files Must Be Group-Owned By The Primary Group   [ref]

Change the group owner of interactive users files to the group found in
/etc/passwd
for the user. To change the group owner of a local interactive user home directory, use the following command:
$ sudo chgrp USER_GROUP /home/USER/.INIT_FILE
        
This rule ensures every initialization file 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 their respective initialization files.
Rationale:
Local initialization files for interactive users 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_accounts_user_dot_group_ownership
References:
disaCCI-000366
os-srgSRG-OS-000480-GPOS-00227
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Ensure interactive local users are the group-owners of their respective initialization
    files
  ansible.builtin.command:
    cmd: awk -F':' '{ if ($3 >= 1000 && $3 != 65534) system("chgrp -f " $4" "$6"/.[^\.]?*")
      }' /etc/passwd
  tags:
  - accounts_user_dot_group_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

awk -F':' '{ if ($3 >= 1000 && $3 != 65534) system("chgrp -f " $4" "$6"/.[^\.]?*") }' /etc/passwd

Rule   User Initialization Files Must Be Owned By the Primary User   [ref]

Set the owner of the user initialization files for interactive users to the primary owner with the following command:
$ sudo chown USER /home/USER/.*
This rule ensures every initialization file related to an interactive user is 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 ownership of their respective initialization files.
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_accounts_user_dot_user_ownership
References:
disaCCI-000366
os-srgSRG-OS-000480-GPOS-00227
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Ensure interactive local users are the owners of their respective initialization
    files
  ansible.builtin.command:
    cmd: awk -F':' '{ if ($3 >= 1000 && $3 != 65534) system("chown -f " $3" "$6"/.[^\.]?*")
      }' /etc/passwd
  tags:
  - accounts_user_dot_user_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

awk -F':' '{ if ($3 >= 1000 && $3 != 65534) system("chown -f " $3" "$6"/.[^\.]?*") }' /etc/passwd

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
References:
disaCCI-000366
os-srgSRG-OS-000480-GPOS-00227
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - 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:
  - 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:
  - 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:
  - accounts_users_home_files_groupownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Reboot:false
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 a Valid Owner   [ref]

Either remove all files and directories from the system that do not have a valid user, or assign a valid user to all unowned files and directories. To assign a valid owner to a local interactive user's files and directories, use the following command:
$ sudo chown -R USER /home/USER
        
This rule ensures every file or directory under the home directory related to an interactive user is 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 ownership of folders or files in their respective home directories.
Rationale:
If local interactive users do not own the files in their directories, unauthorized users may be able to access them. Additionally, if files are not owned by the user, this could be an indication of system compromise.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_accounts_users_home_files_ownership
References:
disaCCI-000366
os-srgSRG-OS-000480-GPOS-00227
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - accounts_users_home_files_ownership
  - 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:
  - accounts_users_home_files_ownership
  - 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:
  - accounts_users_home_files_ownership
  - 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] }}'
    owner: '{{ item.0.value[1] }}'
    recurse: true
  loop: '{{ local_users|zip(path_exists.results)|list }}'
  when: item.1.stat is defined and item.1.stat.exists
  tags:
  - accounts_users_home_files_ownership
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Reboot:false
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)
    # Only update the ownership when necessary. This will avoid changing the inode timestamp
    # when the owner is already defined as expected, therefore not impacting in possible integrity
    # check systems that also check inodes timestamps.
    find $home_dir -not -user $user -exec chown -f $user {} \;
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
References:
disaCCI-000366
os-srgSRG-OS-000480-GPOS-00227
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: Get all local users from /etc/passwd
  ansible.builtin.getent:
    database: passwd
    split: ':'
  tags:
  - 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:
  - 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:
  - 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:
  - accounts_users_home_files_permissions
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

Complexity:low
Disruption:low
Reboot:false
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   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
References:
disaCCI-000366
os-srgSRG-OS-000480-GPOS-00227
anssiR50

Complexity:low
Disruption:low
Reboot:false
Strategy:restrict
- name: XCCDF Value var_user_initialization_files_regex # promote to variable
  set_fact:
    var_user_initialization_files_regex: !!str ^(\.bashrc|\.zshrc|\.cshrc|\.profile|\.bash_login|\.bash_profile)$
  tags:
    - always

- name: Ensure All User Initialization Files Have Mode 0740 Or Less Permissive - Gather
    User Info
  ansible.builtin.getent:
    database: passwd
  tags:
  - file_permission_user_init_files
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure All User Initialization Files Have Mode 0740 Or Less Permissive - Find
    Init Files
  ansible.builtin.find:
    paths: '{{ item.value[4] }}'
    pattern: '{{ var_user_initialization_files_regex }}'
    hidden: true
    use_regex: true
  with_dict: '{{ ansible_facts.getent_passwd }}'
  when:
  - item.value[4] != "/sbin/nologin"
  - item.key not in ["nobody", "nfsnobody"]
  - item.value[1] | int >= 1000
  register: found_init_files
  tags:
  - file_permission_user_init_files
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

- name: Ensure All User Initialization Files Have Mode 0740 Or Less Permissive - Fix
    Init Files Permissions
  ansible.builtin.file:
    path: '{{ item.1.path }}'
    mode: u-s,g-wxs,o=
  loop: '{{ q(''ansible.builtin.subelements'', found_init_files.results, ''files'',
    {''skip_missing'': True}) }}'
  tags:
  - file_permission_user_init_files
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed
  - restrict_strategy

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

var_user_initialization_files_regex='^(\.bashrc|\.zshrc|\.cshrc|\.profile|\.bash_login|\.bash_profile)$'


readarray -t interactive_users < <(awk -F: '$3>=1000   {print $1}' /etc/passwd)
readarray -t interactive_users_home < <(awk -F: '$3>=1000   {print $6}' /etc/passwd)
readarray -t interactive_users_shell < <(awk -F: '$3>=1000   {print $7}' /etc/passwd)

USERS_IGNORED_REGEX='nobody|nfsnobody'

for (( i=0; i<"${#interactive_users[@]}"; i++ )); do
    if ! grep -qP "$USERS_IGNORED_REGEX" <<< "${interactive_users[$i]}" && \
        [ "${interactive_users_shell[$i]}" != "/sbin/nologin" ]; then
        
        readarray -t init_files < <(find "${interactive_users_home[$i]}" -maxdepth 1 \
            -exec basename {} \; | grep -P "$var_user_initialization_files_regex")
        for file in "${init_files[@]}"; do
            chmod u-s,g-wxs,o= "${interactive_users_home[$i]}/$file"
        done
    fi
done

Rule   Enable authselect   [ref]

Configure user authentication setup to use the authselect tool. If authselect profile is selected, the rule will enable the minimal profile.
Warning:  If the sudo authselect select command returns an error informing that the chosen profile cannot be selected, it is probably because PAM files have already been modified by the administrator. If this is the case, in order to not overwrite the desired changes made by the administrator, the current PAM settings should be investigated before forcing the selection of the chosen authselect profile.
Rationale:
Authselect is a successor to authconfig. It is a tool to select system authentication and identity sources from a list of supported profiles instead of letting the administrator manually build the PAM stack. That way, it avoids potential breakage of configuration, as it ships several tested profiles that are well tested and supported to solve different use-cases.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_enable_authselect
References:
disaCCI-000213
hipaa164.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)
nistAC-3
osppFIA_UAU.1, FIA_AFL.1
os-srgSRG-OS-000480-GPOS-00227
anssiR31
pcidss48.3.4


var_authselect_profile='minimal'


authselect select "$var_authselect_profile"

if test "$?" -ne 0; then
    if rpm --quiet --verify pam; then
        authselect select --force "$var_authselect_profile"
    else
	echo "Files in the 'pam' package have been altered, so the authselect configuration won't be forced" >&2
    fi
fi
Group   GRUB2 bootloader configuration   Group contains 2 groups and 26 rules
[ref]   During the boot process, the boot loader is responsible for starting the execution of the kernel and passing options to it. The boot loader allows for the selection of different kernels - possibly on different partitions or media. The default Red Hat Enterprise Linux 10 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 7 rules
[ref]   Non-UEFI GRUB2 bootloader configuration

Rule   Verify /boot/grub2/grub.cfg Group Ownership   [ref]

The file /boot/grub2/grub.cfg should be group-owned by the root group to prevent destruction or modification of the file. To properly set the group owner of /boot/grub2/grub.cfg, run the command:
$ sudo chgrp root /boot/grub2/grub.cfg
Rationale:
The root group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupowner_grub2_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cjis5.5.2.2
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
hipaa164.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)
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
pcidssReq-7.1
os-srgSRG-OS-000480-GPOS-00227
anssiR29
pcidss42.2.6

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_groupowner_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/grub.cfg
  stat:
    path: /boot/grub2/grub.cfg
  register: file_exists
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_groupowner_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /boot/grub2/grub.cfg
  file:
    path: /boot/grub2/grub.cfg
    group: '0'
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_groupowner_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chgrp 0 /boot/grub2/grub.cfg

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

Rule   Verify /boot/grub2/user.cfg Group Ownership   [ref]

The file /boot/grub2/user.cfg should be group-owned by the root group to prevent reading or modification of the file. To properly set the group owner of /boot/grub2/user.cfg, run the command:
$ sudo chgrp root /boot/grub2/user.cfg
Rationale:
The root group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway. Non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupowner_user_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cjis5.5.2.2
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
hipaa164.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)
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
pcidssReq-7.1
os-srgSRG-OS-000480-GPOS-00227
anssiR29
pcidss42.2.6

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_groupowner_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/user.cfg
  stat:
    path: /boot/grub2/user.cfg
  register: file_exists
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_groupowner_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /boot/grub2/user.cfg
  file:
    path: /boot/grub2/user.cfg
    group: '0'
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_groupowner_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chgrp 0 /boot/grub2/user.cfg

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

Rule   Verify /boot/grub2/grub.cfg User Ownership   [ref]

The file /boot/grub2/grub.cfg should be owned by the root user to prevent destruction or modification of the file. To properly set the owner of /boot/grub2/grub.cfg, run the command:
$ sudo chown root /boot/grub2/grub.cfg 
Rationale:
Only root should be able to modify important boot parameters.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_owner_grub2_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cjis5.5.2.2
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
hipaa164.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)
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
pcidssReq-7.1
os-srgSRG-OS-000480-GPOS-00227
anssiR29
pcidss42.2.6

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_owner_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/grub.cfg
  stat:
    path: /boot/grub2/grub.cfg
  register: file_exists
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_owner_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /boot/grub2/grub.cfg
  file:
    path: /boot/grub2/grub.cfg
    owner: '0'
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_owner_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chown 0 /boot/grub2/grub.cfg

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

Rule   Verify /boot/grub2/user.cfg User Ownership   [ref]

The file /boot/grub2/user.cfg should be owned by the root user to prevent reading or modification of the file. To properly set the owner of /boot/grub2/user.cfg, run the command:
$ sudo chown root /boot/grub2/user.cfg 
Rationale:
Only root should be able to modify important boot parameters. Also, non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_owner_user_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cjis5.5.2.2
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
hipaa164.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)
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
pcidssReq-7.1
anssiR29
pcidss42.2.6

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_owner_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/user.cfg
  stat:
    path: /boot/grub2/user.cfg
  register: file_exists
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_owner_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /boot/grub2/user.cfg
  file:
    path: /boot/grub2/user.cfg
    owner: '0'
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_owner_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chown 0 /boot/grub2/user.cfg

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

Rule   Verify /boot/grub2/grub.cfg Permissions   [ref]

File permissions for /boot/grub2/grub.cfg should be set to 600. To properly set the permissions of /boot/grub2/grub.cfg, run the command:
$ sudo chmod 600 /boot/grub2/grub.cfg
Rationale:
Proper permissions ensure that only the root user can modify important boot parameters.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_grub2_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
hipaa164.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)
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
anssiR29
pcidss42.2.6

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/grub.cfg
  stat:
    path: /boot/grub2/grub.cfg
  register: file_exists
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-xs,g-xwrs,o-xwrt on /boot/grub2/grub.cfg
  file:
    path: /boot/grub2/grub.cfg
    mode: u-xs,g-xwrs,o-xwrt
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chmod u-xs,g-xwrs,o-xwrt /boot/grub2/grub.cfg

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

Rule   Verify /boot/grub2/user.cfg Permissions   [ref]

File permissions for /boot/grub2/user.cfg should be set to 600. To properly set the permissions of /boot/grub2/user.cfg, run the command:
$ sudo chmod 600 /boot/grub2/user.cfg
Rationale:
Proper permissions ensure that only the root user can read or modify important boot parameters.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_user_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
hipaa164.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)
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
anssiR29
pcidss42.2.6

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/user.cfg
  stat:
    path: /boot/grub2/user.cfg
  register: file_exists
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-xs,g-xwrs,o-xwrt on /boot/grub2/user.cfg
  file:
    path: /boot/grub2/user.cfg
    mode: u-xs,g-xwrs,o-xwrt
  when:
  - '"/boot/efi" not in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSSv4-2.2.6
  - configure_strategy
  - file_permissions_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ ! -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chmod u-xs,g-xwrs,o-xwrt /boot/grub2/user.cfg

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

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
References:
cis-csc1, 11, 12, 14, 15, 16, 18, 3, 5
cobit5DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10
cui3.4.5
disaCCI-000213
hipaa164.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)
isa-62443-20094.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
isa-62443-2013SR 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
iso27001-2013A.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
nistCM-6(a)
nist-csfPR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3
osppFIA_UAU.1
os-srgSRG-OS-000080-GPOS-00048
anssiR5
Group   UEFI GRUB2 bootloader configuration   Group contains 7 rules
[ref]   UEFI GRUB2 bootloader configuration
Warning:  UEFI generally uses vfat file systems, which does not support Unix-style permissions managed by chmod command. In this case, in order to change file permissions for files within /boot/efi it is necessary to update the mount options in /etc/fstab file and reboot the system.

Rule   Verify the UEFI Boot Loader grub.cfg Group Ownership   [ref]

The file /boot/grub2/grub.cfg should be group-owned by the root group to prevent destruction or modification of the file. To properly set the group owner of /boot/grub2/grub.cfg, run the command:
$ sudo chgrp root /boot/grub2/grub.cfg
Rationale:
The root group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupowner_efi_grub2_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cjis5.5.2.2
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
pcidssReq-7.1
anssiR29

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_groupowner_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/grub.cfg
  stat:
    path: /boot/grub2/grub.cfg
  register: file_exists
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_groupowner_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /boot/grub2/grub.cfg
  file:
    path: /boot/grub2/grub.cfg
    group: '0'
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_groupowner_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chgrp 0 /boot/grub2/grub.cfg

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

Rule   Verify /boot/grub2/user.cfg Group Ownership   [ref]

The file /boot/grub2/user.cfg should be group-owned by the root group to prevent reading or modification of the file. To properly set the group owner of /boot/grub2/user.cfg, run the command:
$ sudo chgrp root /boot/grub2/user.cfg
Rationale:
The root group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway. Non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_groupowner_efi_user_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cjis5.5.2.2
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
pcidssReq-7.1
anssiR29

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_groupowner_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/user.cfg
  stat:
    path: /boot/grub2/user.cfg
  register: file_exists
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_groupowner_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure group owner 0 on /boot/grub2/user.cfg
  file:
    path: /boot/grub2/user.cfg
    group: '0'
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_groupowner_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chgrp 0 /boot/grub2/user.cfg

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

Rule   Verify the UEFI Boot Loader grub.cfg User Ownership   [ref]

The file /boot/grub2/grub.cfg should be owned by the root user to prevent destruction or modification of the file. To properly set the owner of /boot/grub2/grub.cfg, run the command:
$ sudo chown root /boot/grub2/grub.cfg 
Rationale:
Only root should be able to modify important boot parameters.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_owner_efi_grub2_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cjis5.5.2.2
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
pcidssReq-7.1
anssiR29

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_owner_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/grub.cfg
  stat:
    path: /boot/grub2/grub.cfg
  register: file_exists
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_owner_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /boot/grub2/grub.cfg
  file:
    path: /boot/grub2/grub.cfg
    owner: '0'
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_owner_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chown 0 /boot/grub2/grub.cfg

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

Rule   Verify /boot/grub2/user.cfg User Ownership   [ref]

The file /boot/grub2/user.cfg should be owned by the root user to prevent reading or modification of the file. To properly set the owner of /boot/grub2/user.cfg, run the command:
$ sudo chown root /boot/grub2/user.cfg 
Rationale:
Only root should be able to modify important boot parameters. Also, non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_owner_efi_user_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cjis5.5.2.2
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
pcidssReq-7.1
anssiR29

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_owner_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/user.cfg
  stat:
    path: /boot/grub2/user.cfg
  register: file_exists
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_owner_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure owner 0 on /boot/grub2/user.cfg
  file:
    path: /boot/grub2/user.cfg
    owner: '0'
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - CJIS-5.5.2.2
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - PCI-DSS-Req-7.1
  - configure_strategy
  - file_owner_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chown 0 /boot/grub2/user.cfg

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

Rule   Verify the UEFI Boot Loader grub.cfg Permissions   [ref]

File permissions for /boot/grub2/grub.cfg should be set to 700. To properly set the permissions of /boot/grub2/grub.cfg, run the command:
$ sudo chmod 700 /boot/grub2/grub.cfg
Rationale:
Proper permissions ensure that only the root user can modify important boot parameters.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_efi_grub2_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
anssiR29

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/grub.cfg
  stat:
    path: /boot/grub2/grub.cfg
  register: file_exists
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-xwrs,o-xwrt on /boot/grub2/grub.cfg
  file:
    path: /boot/grub2/grub.cfg
    mode: u-s,g-xwrs,o-xwrt
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_efi_grub2_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chmod u-s,g-xwrs,o-xwrt /boot/grub2/grub.cfg

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

Rule   Verify /boot/grub2/user.cfg Permissions   [ref]

File permissions for /boot/grub2/user.cfg should be set to 600. To properly set the permissions of /boot/grub2/user.cfg, run the command:
$ sudo chmod 600 /boot/grub2/user.cfg
Rationale:
Proper permissions ensure that only the root user can read or modify important boot parameters.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_file_permissions_efi_user_cfg
References:
cis-csc12, 13, 14, 15, 16, 18, 3, 5
cobit5APO01.06, DSS05.04, DSS05.07, DSS06.02
cui3.4.5
disaCCI-000225
isa-62443-20094.3.3.7.3
isa-62443-2013SR 2.1, SR 5.2
iso27001-2013A.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
nistCM-6(a), AC-6(1)
nist-csfPR.AC-4, PR.DS-5
anssiR29

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Test for existence /boot/grub2/user.cfg
  stat:
    path: /boot/grub2/user.cfg
  register: file_exists
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

- name: Ensure permission u-s,g-xwrs,o-xwrt on /boot/grub2/user.cfg
  file:
    path: /boot/grub2/user.cfg
    mode: u-s,g-xwrs,o-xwrt
  when:
  - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  - file_exists.stat is defined and file_exists.stat.exists
  tags:
  - NIST-800-171-3.4.5
  - NIST-800-53-AC-6(1)
  - NIST-800-53-CM-6(a)
  - configure_strategy
  - file_permissions_efi_user_cfg
  - low_complexity
  - low_disruption
  - medium_severity
  - no_reboot_needed

Complexity:low
Disruption:low
Reboot:false
Strategy:configure
# Remediation is applicable only in certain platforms
if [ -d /sys/firmware/efi ] && rpm --quiet -q grub2-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

chmod u-s,g-xwrs,o-xwrt /boot/grub2/user.cfg

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

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
References:
cis-csc11, 12, 14, 15, 16, 18, 3, 5
cobit5DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06
cui3.4.5
disaCCI-000213
hipaa164.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)
isa-62443-20094.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
isa-62443-2013SR 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
iso27001-2013A.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
nistCM-6(a)
nist-csfPR.AC-4, PR.AC-6, PR.PT-3
osppFIA_UAU.1
os-srgSRG-OS-000080-GPOS-00048
anssiR5

Rule   IOMMU configuration directive   [ref]

On x86 architecture supporting VT-d, the IOMMU manages the access control policy between the hardware devices and some of the system critical units such as the memory. Configure the default Grub2 kernel command line to contain iommu=force as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) iommu=force"
Warning:  Depending on the hardware, devices and operating system used, enabling IOMMU can cause hardware instabilities. Proper function and stability should be assessed before applying remediation to production systems.
Rationale:
On x86 architectures, activating the I/OMMU prevents the system from arbitrary accesses potentially made by hardware devices.
Severity: 
unknown
Rule ID:xccdf_org.ssgproject.content_rule_grub2_enable_iommu_force
References:
anssiR7

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_enable_iommu_force
  - low_disruption
  - medium_complexity
  - reboot_required
  - restrict_strategy
  - unknown_severity

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="iommu=force"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_enable_iommu_force
  - low_disruption
  - medium_complexity
  - reboot_required
  - restrict_strategy
  - unknown_severity

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

grubby --update-kernel=ALL --args=iommu=force

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

[customizations.kernel]
append = "iommu=force"

Rule   Configure L1 Terminal Fault mitigations   [ref]

L1 Terminal Fault (L1TF) is a hardware vulnerability which allows unprivileged speculative access to data which is available in the Level 1 Data Cache when the page table entry isn't present. Select the appropriate mitigation by adding the argument l1tf=full,force to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain l1tf=full,force as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) l1tf=full,force"
Since Linux Kernel 4.19 you can check the L1TF vulnerability state with the following command: cat /sys/devices/system/cpu/vulnerabilities/l1tf
Warning:  Enabling L1TF mitigations may impact performance of the system.
Rationale:
The L1TF vulnerability allows an attacker to bypass memory access security controls imposed by the system or hypervisor. The L1TF vulnerability allows read access to any physical memory location that is cached in the L1 Data Cache.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_grub2_l1tf_argument
References:
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_l1tf_argument
  - high_severity
  - low_disruption
  - medium_complexity
  - reboot_required
  - restrict_strategy
- name: XCCDF Value var_l1tf_options # promote to variable
  set_fact:
    var_l1tf_options: !!str full,force
  tags:
    - always

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="l1tf={{ var_l1tf_options }}"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_l1tf_argument
  - high_severity
  - low_disruption
  - 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

var_l1tf_options='full,force'



grubby --update-kernel=ALL --args=l1tf=$var_l1tf_options

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

[customizations.kernel]
append = "l1tf=full,force"

Rule   Force kernel panic on uncorrected MCEs   [ref]

A Machine Check Exception is an error generated by the CPU itdetects an error in itself, memory or I/O devices. These errors may be corrected and generate a check log entry, if an error cannot be corrected the kernel may panic or SIGBUS. To force the kernel to panic on any uncorrected error reported by Machine Check set the MCE tolerance to zero by adding mce=0 to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain mce=0 as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) mce=0"
Rationale:
Allowing uncorrected errors to result on a SIGBUS may allow an attacker to continue trying to exploit a vulnerability such as Rowhammer.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_mce_argument
References:
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_mce_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="mce=0"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_mce_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=mce=0

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

[customizations.kernel]
append = "mce=0"

Rule   Configure Microarchitectural Data Sampling mitigation   [ref]

Microarchitectural Data Sampling (MDS) is a hardware vulnerability which allows unprivileged speculative access to data which is available in various CPU internal buffers. When performing store, load, L1 refill operations, processors write data into temporary microarchitectural structures (buffers), and the data in the buffer can be forwarded to load operations as an optimization. Under certain conditions, data unrelated to the load operations can be speculatively forwarded from the buffers to a disclosure gadget which allows in turn to infer the value via a cache side channel attack. Select the appropriate mitigation by adding the argument mds=full,nosmt to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain mds=full,nosmt as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) mds=full,nosmt"
Not all processors are affected by all variants of MDS, but the mitigation mechanism is identical for all of them. Since Linux Kernel 5.2 you can check whether the system is vulnerable or mitigated with the following command: cat /sys/devices/system/cpu/vulnerabilities/mds
Warning:  Enabling MDS mitigations will impact performance of the system, mainly by workloads with high rates of user-kernel-user space transitions. For example, system calls, NMIs and interrupts.
Rationale:
The MDS vulnerability allows an attacker to sample data from internal CPU buffers.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_mds_argument
References:
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_mds_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy
- name: XCCDF Value var_mds_options # promote to variable
  set_fact:
    var_mds_options: !!str full,nosmt
  tags:
    - always

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="mds={{ var_mds_options }}"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_mds_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

var_mds_options='full,nosmt'



grubby --update-kernel=ALL --args=mds=$var_mds_options

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

[customizations.kernel]
append = "mds=full,nosmt"

Rule   Ensure SMAP is not disabled during boot   [ref]

The SMAP is used to prevent the supervisor mode from unintentionally reading/writing into memory pages in the user space, it is enabled by default since Linux kernel 3.7. But it could be disabled through kernel boot parameters. Ensure that Supervisor Mode Access Prevention (SMAP) is not disabled by the nosmap boot paramenter option. Check that the line
GRUB_CMDLINE_LINUX="..."
within /etc/default/grub doesn't contain the argument nosmap. Run the following command to update command line for already installed kernels:
# grubby --update-kernel=ALL --remove-args="nosmap"
Rationale:
Disabling SMAP can facilitate exploitation of vulnerabilities caused by unintended access and manipulation of data in the user space.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_nosmap_argument_absent
References:
anssiR1

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_nosmap_argument_absent
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --remove-args="nosmap"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_nosmap_argument_absent
  - 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 --remove-args=nosmap

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

Rule   Ensure SMEP is not disabled during boot   [ref]

The SMEP is used to prevent the supervisor mode from executing user space code, it is enabled by default since Linux kernel 3.0. But it could be disabled through kernel boot parameters. Ensure that Supervisor Mode Execution Prevention (SMEP) is not disabled by the nosmep boot paramenter option. Check that the line
GRUB_CMDLINE_LINUX="..."
within /etc/default/grub doesn't contain the argument nosmep. Run the following command to update command line for already installed kernels:
# grubby --update-kernel=ALL --remove-args="nosmep"
Rationale:
Disabling SMEP can facilitate exploitation of certain vulnerabilities because it allows the kernel to unintentionally execute code in less privileged memory space.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_nosmep_argument_absent
References:
anssiR1

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_nosmep_argument_absent
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --remove-args="nosmep"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_nosmep_argument_absent
  - 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 --remove-args=nosmep

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

Rule   Enable randomization of the page allocator   [ref]

To enable randomization of the page allocator in the kernel, add the page_alloc.shuffle=1 argument to the default GRUB 2 command line. Configure the default Grub2 kernel command line to contain page_alloc.shuffle=1 as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) page_alloc.shuffle=1"
Rationale:
The CONFIG_SHUFFLE_PAGE_ALLOCATOR config option is primarily focused on improving the average utilization of a direct-mapped memory-side-cache. Aside of this performance effect, it also reduces predictability of page allocations in situations when the bad actor can crash the system and somehow leverage knowledge of (page) allocation order right after a fresh reboot, or can control the timing between a hot-pluggable memory node (as in NUMA node) and applications allocating memory ouf of that node. The page_alloc.shuffle=1 kernel command line parameter then forces this functionality irrespectively of memory cache architecture.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_page_alloc_shuffle_argument
References:
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_page_alloc_shuffle_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_alloc.shuffle=1"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_page_alloc_shuffle_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=page_alloc.shuffle=1

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

[customizations.kernel]
append = "page_alloc.shuffle=1"

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. Configure the default Grub2 kernel command line to contain pti=on as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) 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
References:
disaCCI-000381
nistSI-16
os-srgSRG-OS-000433-GPOS-00193, SRG-OS-000095-GPOS-00049
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - 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:
  - 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

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

[customizations.kernel]
append = "pti=on"

Rule   Configure the confidence in TPM for entropy   [ref]

The TPM security chip that is available in most modern systems has a hardware RNG. It is also used to feed the entropy pool, but generally not credited entropy. Use rng_core.default_quality in the kernel command line to set the trust level on the hardware generators. The trust level defines the amount of entropy to credit. A value of 0 tells the system not to trust the hardware random number generators available, and doesn't credit any entropy to the pool. A value of 1000 assigns full confidence in the generators, and credits all the entropy it provides to the pool. Note that the value of rng_core.default_quality is global, affecting the trust on all hardware random number generators. Select the appropriate confidence by adding the argument rng_core.default_quality=500 to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain rng_core.default_quality=500 as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) rng_core.default_quality=500"
Rationale:
A system may struggle to initialize its entropy pool and end up starving. Crediting entropy from the hardware number generators available in the system helps fill up the entropy pool.
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_grub2_rng_core_default_quality_argument
References:
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_rng_core_default_quality_argument
  - low_disruption
  - low_severity
  - medium_complexity
  - reboot_required
  - restrict_strategy
- name: XCCDF Value var_rng_core_default_quality # promote to variable
  set_fact:
    var_rng_core_default_quality: !!str 500
  tags:
    - always

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="rng_core.default_quality={{ var_rng_core_default_quality
    }}"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_rng_core_default_quality_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

var_rng_core_default_quality='500'



grubby --update-kernel=ALL --args=rng_core.default_quality=$var_rng_core_default_quality

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

[customizations.kernel]
append = "rng_core.default_quality=500"

Rule   Disable merging of slabs with similar size   [ref]

The kernel may merge similar slabs together to reduce overhead and increase cache hotness of objects. Disabling merging of slabs keeps the slabs separate and reduces the risk of kernel heap overflows overwriting objects in merged caches. To disable merging of slabs in the Kernel add the argument slab_nomerge=yes to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain slab_nomerge=yes as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) slab_nomerge=yes"
Warning:  Disabling merge of slabs will slightly increase kernel memory utilization.
Rationale:
Disabling the merge of slabs of similar sizes prevents the kernel from merging a seemingly useless but vulnerable slab with a useful and valuable slab. This increase the risk that a heap overflow could overwrite objects from merged caches, with unmerged caches the heap overflow would only affect the objects in the same cache. Overall, this reduces the kernel attack surface area by isolating slabs from each other.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_slab_nomerge_argument
References:
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_slab_nomerge_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="slab_nomerge=yes"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_slab_nomerge_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=slab_nomerge=yes

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

[customizations.kernel]
append = "slab_nomerge=yes"

Rule   Configure Speculative Store Bypass Mitigation   [ref]

Certain CPUs are vulnerable to an exploit against a common wide industry wide performance optimization known as Speculative Store Bypass (SSB). In such cases, recent stores to the same memory location cannot always be observed by later loads during speculative execution. However, such stores are unlikely and thus they can be detected prior to instruction retirement at the end of a particular speculation execution window. Since Linux Kernel 4.17 you can check the SSB mitigation state with the following command: cat /sys/devices/system/cpu/vulnerabilities/spec_store_bypass Select the appropriate SSB state by adding the argument spec_store_bypass_disable=seccomp to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain spec_store_bypass_disable=seccomp as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) spec_store_bypass_disable=seccomp"
Warning:  Disabling Speculative Store Bypass may impact performance of the system.
Rationale:
In vulnerable processsors, the speculatively forwarded store can be used in a cache side channel attack. An example of this is reading memory to which the attacker does not directly have access, for example inside the sandboxed code.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_grub2_spec_store_bypass_disable_argument
References:
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_spec_store_bypass_disable_argument
  - low_disruption
  - medium_complexity
  - medium_severity
  - reboot_required
  - restrict_strategy
- name: XCCDF Value var_spec_store_bypass_disable_options # promote to variable
  set_fact:
    var_spec_store_bypass_disable_options: !!str seccomp
  tags:
    - always

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="spec_store_bypass_disable={{ var_spec_store_bypass_disable_options
    }}"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_spec_store_bypass_disable_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

var_spec_store_bypass_disable_options='seccomp'



grubby --update-kernel=ALL --args=spec_store_bypass_disable=$var_spec_store_bypass_disable_options

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

[customizations.kernel]
append = "spec_store_bypass_disable=seccomp"

Rule   Enforce Spectre v2 mitigation   [ref]

Spectre V2 is an indirect branch poisoning attack that can lead to data leakage. An exploit for Spectre V2 tricks the indirect branch predictor into executing code from a future indirect branch chosen by the attacker, even if the privilege level is different. Since Linux Kernel 4.15 you can check the Spectre V2 mitigation state with the following command: cat /sys/devices/system/cpu/vulnerabilities/spectre_v2 Enforce the Spectre V2 mitigation by adding the argument spectre_v2=on to the default GRUB 2 command line for the Linux operating system. Configure the default Grub2 kernel command line to contain spectre_v2=on as follows:
# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) spectre_v2=on"
Rationale:
The Spectre V2 vulnerability allows an attacker to read memory that he should not have access to.
Severity: 
high
Rule ID:xccdf_org.ssgproject.content_rule_grub2_spectre_v2_argument
References:
anssiR8

Complexity:medium
Disruption:low
Reboot:true
Strategy:restrict
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
  - grub2_spectre_v2_argument
  - high_severity
  - low_disruption
  - medium_complexity
  - reboot_required
  - restrict_strategy

- name: Update grub defaults and the bootloader menu
  command: /sbin/grubby --update-kernel=ALL --args="spectre_v2=on"
  when:
  - '"grub2-common" in ansible_facts.packages'
  - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
  - grub2_spectre_v2_argument
  - high_severity
  - low_disruption
  - 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=spectre_v2=on

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

[customizations.kernel]
append = "spectre_v2=on"
Group   Kernel Configuration   Group contains 1 group and 67 rules
[ref]   Contains rules that check the kernel configuration that was used to build it.
Group   Kernel GCC plugin configuration   Group contains 5 rules
[ref]   Contains rules that check the configuration of GCC plugins used by the compiler

Rule   Generate some entropy during boot and runtime   [ref]

Instrument some kernel code to extract some entropy from both original and artificially created program state. This will help especially embedded systems where there is little 'natural' source of entropy normally. This configuration is available from kernel 4.9, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_LATENT_ENTROPY, run the following command: grep CONFIG_GCC_PLUGIN_LATENT_ENTROPY /boot/config-* For each kernel installed, a line with value "y" should be returned.
Warning:  There is no remediation for this besides re-compiling the kernel with the appropriate value for the config.
Warning:  Note that entropy extracted this way is not cryptographically secure!
Warning:  There is a performance cost during the boot process (about 0.5%) and fork and irq processing.
Rationale:
This helps generate entropy during startup and is particularly relevant for devices with inappropriate entropy sources.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_config_gcc_plugin_latent_entropy
References:
anssiR21

Rule   Randomize layout of sensitive kernel structures   [ref]

Randomize at compile-time the layouts of structures that are entirely function pointers (and have not been manually annotated with __no_randomize_layout), or structures that have been explicitly marked with __randomize_layout. This configuration is available from kernel 4.13, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_RANDSTRUCT, run the following command: grep CONFIG_GCC_PLUGIN_RANDSTRUCT /boot/config-* For each kernel installed, a line with value "y" should be returned.
Warning:  There is no remediation for this besides re-compiling the kernel with the appropriate value for the config.
Rationale:
Randomizing the layout of kernel data structures make it more difficult for an attacker to know the location of sensitive data.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_config_gcc_plugin_randstruct
References:
anssiR21

Rule   Poison kernel stack before returning from syscalls   [ref]

This option makes the kernel erase the kernel stack before returning from system calls. This has the effect of leaving the stack initialized to the poison value, which both reduces the lifetime of any sensitive stack contents and reduces potential for uninitialized stack variable exploits or information exposures (it does not cover functions reaching the same stack depth as prior functions during the same syscall). This configuration is available from kernel 4.20, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_STACKLEAK, run the following command: grep CONFIG_GCC_PLUGIN_STACKLEAK /boot/config-* For each kernel installed, a line with value "y" should be returned.
Warning:  There is no remediation for this besides re-compiling the kernel with the appropriate value for the config.
Warning:  The performance impact on a single CPU system kernel is of 1% slowdown.
Rationale:
This blocks most uninitialized stack variable attacks, with the performance impact being driven by the depth of the stack usage, rather than the function calling complexity.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_config_gcc_plugin_stackleak
References:
anssiR21

Rule   Force initialization of variables containing userspace addresses   [ref]

While the kernel is built with warnings enabled for any missed stack variable initializations, this warning is silenced for anything passed by reference to another function, under the occasionally misguided assumption that the function will do the initialization. As this regularly leads to exploitable flaws, this plugin is available to identify and zero-initialize such variables, depending on the chosen level of coverage. This configuration is available from kernel 4.11, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_STRUCTLEAK, run the following command: grep CONFIG_GCC_PLUGIN_STRUCTLEAK /boot/config-* For each kernel installed, a line with value "y" should be returned.
Warning:  There is no remediation for this besides re-compiling the kernel with the appropriate value for the config.
Rationale:
Initializing structures from userspace can prevent some classes of information exposure.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_config_gcc_plugin_structleak
References:
anssiR21

Rule   zero-init everything passed by reference   [ref]

Zero-initialize any stack variables that may be passed by reference and had not already been explicitly initialized. This configuration is available from kernel 4.14, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF_ALL, run the following command: grep CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF_ALL /boot/config-* For each kernel installed, a line with value "y" should be returned.
Warning:  There is no remediation for this besides re-compiling the kernel with the appropriate value for the config.
Rationale:
This eliminates all classes of uninitialized stack variable exploits and information exposures.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_config_gcc_plugin_structleak_byref_all
References:
anssiR21

Rule   Do not allow ACPI methods to be inserted/replaced at run time   [ref]

This debug facility allows ACPI AML methods to be inserted and/or replaced without rebooting the system. This configuration is available from kernel 3.0. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_ACPI_CUSTOM_METHOD, run the following command: grep CONFIG_ACPI_CUSTOM_METHOD /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned.
Warning:  There is no remediation for this besides re-compiling the kernel with the appropriate value for the config.
Rationale:
Enabling this feature allows arbitrary kernel memory to be written to by root (uid=0) users, allowing them to bypass certain security measures
Severity: 
low
Rule ID:xccdf_org.ssgproject.content_rule_kernel_config_acpi_custom_method
References:
anssiR15

Rule   Emulate Privileged Access Never (PAN)   [ref]

Enabling this option prevents the kernel from accessing user-space memory directly by pointing TTBR0_EL1 to a reserved zeroed area and reserved ASID. The user access routines restore the valid TTBR0_EL1 temporarily. This configuration is available from kernel 4.10, but may be available if backported by distros. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_ARM64_SW_TTBR0_PAN, run the following command: grep CONFIG_ARM64_SW_TTBR0_PAN /boot/config-* For each kernel installed, a line with value "y" should be returned.
Warning:  There is no remediation for this besides re-compiling the kernel with the appropriate value for the config.
Rationale:
The Privileged Access Never (PAN) is the ARM equivalent of the x86 Supervisor Mode Access Prevention (SMAP), and it prevents privileged acccess to user data unless explicitly enabled.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_config_arm64_sw_ttbr0_pan
References:
anssiR27

Rule   Disable kernel support for MISC binaries   [ref]

Enabling CONFIG_BINFMT_MISC makes it possible to plug wrapper-driven binary formats into the kernel. This is specially useful for programs that need an interpreter to run like Java, Python and DOS emulators. Once you have registered such a binary class with the kernel, you can start one of those programs simply by typing in its name at a shell prompt. The configuration that was used to build kernel is available at /boot/config-*. To check the configuration value for CONFIG_BINFMT_MISC, run the following command: grep CONFIG_BINFMT_MISC /boot/config-* Configs with value 'n' are not explicitly set in the file, so either commented lines or no lines should be returned.
Warning:  There is no remediation for this besides re-compiling the kernel with the appropriate value for the config.
Rationale:
This disables arbitrary binary format support and helps reduce attack surface.
Severity: 
medium
Rule ID:xccdf_org.ssgproject.content_rule_kernel_config_binfmt_misc
References:
anssiR23