diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/ansible/rhel9.yml b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/ansible/rhel9.yml
new file mode 100644
index 000000000000..cb4e2c447f6f
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/ansible/rhel9.yml
@@ -0,0 +1,55 @@
+# platform = Red Hat Enterprise Linux 9
+# reboot = false
+# strategy = restrict
+# complexity = low
+# disruption = low
+
+{{% set rx_beg = "^[\s]*-a[\s]+always,exit[\s]+" %}}
+{{% set rx_b32 = "-F[\s]+arch=b32[\s]+" %}}
+{{% set rx_b64 = "-F[\s]+arch=b64[\s]+" %}}
+{{% set rx_path = "-F[\s]+path=" ~ xccdf_value("var_accounts_passwords_pam_faillock_dir") ~ "[\s]+" %}}
+{{% set rx_perm = "-F[\s]+perm=wa[\s]+" %}}
+{{% set rx_auid = "-F[\s]+auid>=" ~ uid_min ~ "[\s]+-F[\s]+auid!=unset[\s]+" %}}
+{{% set rx_end = "(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$" %}}
+
+{{{ ansible_instantiate_variables("var_accounts_passwords_pam_faillock_dir") }}}
+
+- name: "{{{ rule_title }}} - Service facts"
+ ansible.builtin.service_facts:
+
+- name: "{{{ rule_title }}} - Set faillock_audit_rules fact"
+ ansible.builtin.set_fact:
+ faillock_audit_rules:
+ - rule: '-a always,exit -F arch=b32 -F path={{ var_accounts_passwords_pam_faillock_dir }} -F perm=wa -F auid>={{{ uid_min }}} -F auid!=unset -k logins'
+ regex: {{{ rx_beg + rx_b32 + rx_path + rx_perm + rx_auid + rx_end }}}
+ - rule: '-a always,exit -F arch=b64 -F path={{ var_accounts_passwords_pam_faillock_dir }} -F perm=wa -F auid>={{{ uid_min }}} -F auid!=unset -k logins'
+ regex: {{{ rx_beg + rx_b64 + rx_path + rx_perm + rx_auid + rx_end }}}
+
+- name: "{{{ rule_title }}} - Update /etc/audit/rules.d/logins.rules to audit faillock"
+ ansible.builtin.lineinfile:
+ path: /etc/audit/rules.d/logins.rules
+ line: "{{ item.rule }}"
+ regexp: "{{ item.regex }}"
+ create: yes
+ mode: 0600
+ when:
+ - '"auditd.service" in ansible_facts.services'
+ register: augenrules_faillock_update_result
+ with_items: "{{ faillock_audit_rules }}"
+
+- name: "{{{ rule_title }}} - Update /etc/audit/audit.rules to audit faillock"
+ ansible.builtin.lineinfile:
+ path: /etc/audit/audit.rules
+ line: "{{ item.rule }}"
+ regexp: "{{ item.regex }}"
+ create: yes
+ mode: 0600
+ when:
+ - '"auditd.service" in ansible_facts.services'
+ register: auditctl_faillock_update_result
+ with_items: "{{ faillock_audit_rules }}"
+
+- name: "{{{ rule_title }}} - Restart auditd"
+ ansible.builtin.command: /usr/sbin/service auditd restart
+ when:
+ - augenrules_faillock_update_result is changed or auditctl_faillock_update_result is changed
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/bash/rhel9.sh b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/bash/rhel9.sh
new file mode 100644
index 000000000000..4c5a10628337
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/bash/rhel9.sh
@@ -0,0 +1,18 @@
+# platform = Red Hat Enterprise Linux 9
+
+{{{ bash_instantiate_variables("var_accounts_passwords_pam_faillock_dir") }}}
+
+[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")
+
+for ARCH in "${RULE_ARCHS[@]}"
+do
+ ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
+ OTHER_FILTERS="-F path=${var_accounts_passwords_pam_faillock_dir} -F perm=wa"
+ AUID_FILTERS="-F auid>={{{ uid_min }}} -F auid!=unset"
+ SYSCALL=""
+ SYSCALL_GROUPING=""
+ KEY="logins"
+
+ {{{ bash_fix_audit_syscall_rule("augenrules", "$ACTION_ARCH_FILTERS", "$OTHER_FILTERS", "$AUID_FILTERS", "$SYSCALL", "$SYSCALL_GROUPING", "$KEY") }}}
+ {{{ bash_fix_audit_syscall_rule("auditctl", "$ACTION_ARCH_FILTERS", "$OTHER_FILTERS", "$AUID_FILTERS", "$SYSCALL", "$SYSCALL_GROUPING", "$KEY") }}}
+done
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/oval/rhel9.xml b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/oval/rhel9.xml
new file mode 100644
index 000000000000..194b281a15e6
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/oval/rhel9.xml
@@ -0,0 +1,88 @@
+
+
+ {{{ oval_metadata("Record Attempts to Alter Logon and Logout Events - faillock", rule_title=rule_title) }}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ^/etc/audit/rules\.d/.*\.rules$
+
+ 1
+
+
+
+
+
+ ^/etc/audit/rules\.d/.*\.rules$
+
+ 1
+
+
+
+
+
+ /etc/audit/audit.rules
+
+ 1
+
+
+
+
+
+ /etc/audit/audit.rules
+
+ 1
+
+
+
+
+ ^\-a\s+always,exit\s+\-F\s+arch=b32\s+\-F\s+path=
+
+
+
+ \s+\-F\s+perm=\b([rx]*w[rx]*a[rx]*|[rx]*a[rx]*w[rx]*)\b.*$
+
+
+
+
+
+
+ ^\-a\s+always,exit\s+\-F\s+arch=b64\s+\-F\s+path=
+
+
+
+ \s+\-F\s+perm=\b([rx]*w[rx]*a[rx]*|[rx]*a[rx]*w[rx]*)\b.*$
+
+
+
+
+
+
+
+
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/rule.yml b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/rule.yml
index 7681a2a4b9d1..1e58e0672bf3 100644
--- a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/rule.yml
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/rule.yml
@@ -45,6 +45,16 @@ ocil_clause: 'the command does not return a line, or the line is commented out'
ocil: |-
{{{ ocil_audit_rules_watch(xccdf_value("var_accounts_passwords_pam_faillock_dir"), "logins") }}}
+{{% if product in ["rhel9"] %}}
+warnings:
+ - general: |-
+ Note that the OVAL check for this rule accepts audit rules with or without
+ the auid filters (-F auid>=1000 -F auid!=unset).
+ However, the Bash and Ansible remediations will add these filters to ensure
+ full compliance. This approach ensures compatibility with existing systems
+ while bringing them into compliance when remediation is applied.
+{{% endif %}}
+
template:
name: audit_rules_watch
vars:
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_correct_auid.pass.sh b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_correct_auid.pass.sh
new file mode 100644
index 000000000000..7e2f8ee1f8e8
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_correct_auid.pass.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+# platform = Red Hat Enterprise Linux 9
+# packages = audit
+# variables = var_accounts_passwords_pam_faillock_dir=/var/log/faillock
+
+echo "-a always,exit -F arch=b32 -F path=/var/log/faillock -F perm=wa -F auid>=1000 -F auid!=unset -F key=logins" >> /etc/audit/rules.d/logins.rules
+echo "-a always,exit -F arch=b64 -F path=/var/log/faillock -F perm=wa -F auid>=1000 -F auid!=unset -F key=logins" >> /etc/audit/rules.d/logins.rules
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_missing_auid.fail.sh b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_missing_auid.fail.sh
new file mode 100644
index 000000000000..8d9f0558b0ea
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_missing_auid.fail.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+# platform = Red Hat Enterprise Linux 9
+# packages = audit
+# variables = var_accounts_passwords_pam_faillock_dir=/var/log/faillock
+
+# This test should fail because it's missing the auid filters
+echo "-a always,exit -F arch=b32 -F path=/var/log/faillock -F perm=wa -F key=logins" >> /etc/audit/rules.d/logins.rules
+echo "-a always,exit -F arch=b64 -F path=/var/log/faillock -F perm=wa -F key=logins" >> /etc/audit/rules.d/logins.rules
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_missing_b32.fail.sh b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_missing_b32.fail.sh
new file mode 100644
index 000000000000..3ae7b00ea4da
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_faillock/tests/rhel9_missing_b32.fail.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+# platform = Red Hat Enterprise Linux 9
+# packages = audit
+# variables = var_accounts_passwords_pam_faillock_dir=/var/log/faillock
+
+# This test should fail because it's missing the b32 arch rule
+echo "-a always,exit -F arch=b64 -F path=/var/log/faillock -F perm=wa -F auid>=1000 -F auid!=unset -F key=logins" >> /etc/audit/rules.d/logins.rules
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/ansible/rhel9.yml b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/ansible/rhel9.yml
new file mode 100644
index 000000000000..03d5d328706e
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/ansible/rhel9.yml
@@ -0,0 +1,53 @@
+# platform = Red Hat Enterprise Linux 9
+# reboot = false
+# strategy = restrict
+# complexity = low
+# disruption = low
+
+{{% set rx_beg = "^[\s]*-a[\s]+always,exit[\s]+" %}}
+{{% set rx_b32 = "-F[\s]+arch=b32[\s]+" %}}
+{{% set rx_b64 = "-F[\s]+arch=b64[\s]+" %}}
+{{% set rx_path = "-F[\s]+path=/var/log/lastlog[\s]+" %}}
+{{% set rx_perm = "-F[\s]+perm=wa[\s]+" %}}
+{{% set rx_auid = "-F[\s]+auid>=" ~ uid_min ~ "[\s]+-F[\s]+auid!=unset[\s]+" %}}
+{{% set rx_end = "(?:-k[\s]+|-F[\s]+key=)[\S]+[\s]*$" %}}
+
+- name: "{{{ rule_title }}} - Service facts"
+ ansible.builtin.service_facts:
+
+- name: "{{{ rule_title }}} - Set lastlog_audit_rules fact"
+ ansible.builtin.set_fact:
+ lastlog_audit_rules:
+ - rule: '-a always,exit -F arch=b32 -F path=/var/log/lastlog -F perm=wa -F auid>={{{ uid_min }}} -F auid!=unset -k logins'
+ regex: {{{ rx_beg + rx_b32 + rx_path + rx_perm + rx_auid + rx_end }}}
+ - rule: '-a always,exit -F arch=b64 -F path=/var/log/lastlog -F perm=wa -F auid>={{{ uid_min }}} -F auid!=unset -k logins'
+ regex: {{{ rx_beg + rx_b64 + rx_path + rx_perm + rx_auid + rx_end }}}
+
+- name: "{{{ rule_title }}} - Update /etc/audit/rules.d/logins.rules to audit lastlog"
+ ansible.builtin.lineinfile:
+ path: /etc/audit/rules.d/logins.rules
+ line: "{{ item.rule }}"
+ regexp: "{{ item.regex }}"
+ create: yes
+ mode: 0600
+ when:
+ - '"auditd.service" in ansible_facts.services'
+ register: augenrules_lastlog_update_result
+ with_items: "{{ lastlog_audit_rules }}"
+
+- name: "{{{ rule_title }}} - Update /etc/audit/audit.rules to audit lastlog"
+ ansible.builtin.lineinfile:
+ path: /etc/audit/audit.rules
+ line: "{{ item.rule }}"
+ regexp: "{{ item.regex }}"
+ create: yes
+ mode: 0600
+ when:
+ - '"auditd.service" in ansible_facts.services'
+ register: auditctl_lastlog_update_result
+ with_items: "{{ lastlog_audit_rules }}"
+
+- name: "{{{ rule_title }}} - Restart auditd"
+ ansible.builtin.command: /usr/sbin/service auditd restart
+ when:
+ - augenrules_lastlog_update_result is changed or auditctl_lastlog_update_result is changed
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/bash/rhel9.sh b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/bash/rhel9.sh
new file mode 100644
index 000000000000..6f21664f964a
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/bash/rhel9.sh
@@ -0,0 +1,16 @@
+# platform = Red Hat Enterprise Linux 9
+
+[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")
+
+for ARCH in "${RULE_ARCHS[@]}"
+do
+ ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
+ OTHER_FILTERS="-F path=/var/log/lastlog -F perm=wa"
+ AUID_FILTERS="-F auid>={{{ uid_min }}} -F auid!=unset"
+ SYSCALL=""
+ SYSCALL_GROUPING=""
+ KEY="logins"
+
+ {{{ bash_fix_audit_syscall_rule("augenrules", "$ACTION_ARCH_FILTERS", "$OTHER_FILTERS", "$AUID_FILTERS", "$SYSCALL", "$SYSCALL_GROUPING", "$KEY") }}}
+ {{{ bash_fix_audit_syscall_rule("auditctl", "$ACTION_ARCH_FILTERS", "$OTHER_FILTERS", "$AUID_FILTERS", "$SYSCALL", "$SYSCALL_GROUPING", "$KEY") }}}
+done
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/oval/rhel9.xml b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/oval/rhel9.xml
new file mode 100644
index 000000000000..dbd90ef35635
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/oval/rhel9.xml
@@ -0,0 +1,86 @@
+
+
+ {{{ oval_metadata("Record Attempts to Alter Logon and Logout Events - lastlog", rule_title=rule_title) }}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ^/etc/audit/rules\.d/.*\.rules$
+
+ 1
+
+
+
+
+
+ ^/etc/audit/rules\.d/.*\.rules$
+
+ 1
+
+
+
+
+
+ /etc/audit/audit.rules
+
+ 1
+
+
+
+
+
+ /etc/audit/audit.rules
+
+ 1
+
+
+
+
+ ^\-a\s+always,exit\s+\-F\s+arch=b32\s+\-F\s+path=
+
+ \/var\/log\/lastlog
+
+ \s+\-F\s+perm=\b([rx]*w[rx]*a[rx]*|[rx]*a[rx]*w[rx]*)\b.*$
+
+
+
+
+
+
+ ^\-a\s+always,exit\s+\-F\s+arch=b64\s+\-F\s+path=
+
+ \/var\/log\/lastlog
+
+ \s+\-F\s+perm=\b([rx]*w[rx]*a[rx]*|[rx]*a[rx]*w[rx]*)\b.*$
+
+
+
+
+
+
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/rule.yml b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/rule.yml
index 2e1a28d286a8..0953c2db595a 100644
--- a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/rule.yml
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/rule.yml
@@ -48,6 +48,16 @@ ocil_clause: 'the command does not return a line, or the line is commented out'
ocil: |-
{{{ ocil_audit_rules_watch("/var/log/lastlog", "logins") }}}
+{{% if product in ["rhel9"] %}}
+warnings:
+ - general: |-
+ Note that the OVAL check for this rule accepts audit rules with or without
+ the auid filters (-F auid>=1000 -F auid!=unset).
+ However, the Bash and Ansible remediations will add these filters to ensure
+ full compliance. This approach ensures compatibility with existing systems
+ while bringing them into compliance when remediation is applied.
+{{% endif %}}
+
template:
name: audit_rules_watch
vars:
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_correct_auid.pass.sh b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_correct_auid.pass.sh
new file mode 100644
index 000000000000..b07974f6db03
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_correct_auid.pass.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+# platform = Red Hat Enterprise Linux 9
+# packages = audit
+
+echo "-a always,exit -F arch=b32 -F path=/var/log/lastlog -F perm=wa -F auid>=1000 -F auid!=unset -k logins" >> /etc/audit/rules.d/logins.rules
+echo "-a always,exit -F arch=b64 -F path=/var/log/lastlog -F perm=wa -F auid>=1000 -F auid!=unset -k logins" >> /etc/audit/rules.d/logins.rules
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_missing_auid.fail.sh b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_missing_auid.fail.sh
new file mode 100644
index 000000000000..109c0a1b21e6
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_missing_auid.fail.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+# platform = Red Hat Enterprise Linux 9
+# packages = audit
+
+# This test should fail because it's missing the auid filters
+echo "-a always,exit -F arch=b32 -F path=/var/log/lastlog -F perm=wa -k logins" >> /etc/audit/rules.d/logins.rules
+echo "-a always,exit -F arch=b64 -F path=/var/log/lastlog -F perm=wa -k logins" >> /etc/audit/rules.d/logins.rules
diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_missing_b32.fail.sh b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_missing_b32.fail.sh
new file mode 100644
index 000000000000..53eb866bacd3
--- /dev/null
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_login_events/audit_rules_login_events_lastlog/tests/rhel9_missing_b32.fail.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+# platform = Red Hat Enterprise Linux 9
+# packages = audit
+
+# This test should fail because it's missing the b32 arch rule
+echo "-a always,exit -F arch=b64 -F path=/var/log/lastlog -F perm=wa -F auid>=1000 -F auid!=unset -k logins" >> /etc/audit/rules.d/logins.rules