diff --git a/linux_os/guide/auditing/auditd_configure_rules/audit_rules_sudoers_d/rule.yml b/linux_os/guide/auditing/auditd_configure_rules/audit_rules_sudoers_d/rule.yml
index 5a91e876a445..94c775e562e0 100644
--- a/linux_os/guide/auditing/auditd_configure_rules/audit_rules_sudoers_d/rule.yml
+++ b/linux_os/guide/auditing/auditd_configure_rules/audit_rules_sudoers_d/rule.yml
@@ -6,7 +6,7 @@ title: 'Ensure auditd Collects System Administrator Actions - /etc/sudoers.d/'
description: |-
At a minimum, the audit system should collect administrator actions
for all users and root.
- {{{ describe_audit_rules_watch("/etc/sudoers.d/", "actions") }}}
+ {{{ describe_audit_rules_watch("/etc/sudoers.d", "actions") }}}
rationale: |-
The actions taken by system administrators should be audited to keep a record
@@ -30,14 +30,14 @@ references:
ocil_clause: 'the command does not return a line, or the line is commented out'
ocil: |-
- {{{ ocil_audit_rules_watch("/etc/sudoers.d/", "actions") }}}
+ {{{ ocil_audit_rules_watch("/etc/sudoers.d", "actions") }}}
-fixtext: '{{{ fixtext_audit_file_watch_rule("/etc/sudoers.d/", "identity", "/etc/audit/rules.d/audit.rules") }}}'
+fixtext: '{{{ fixtext_audit_file_watch_rule("/etc/sudoers.d", "identity", "/etc/audit/rules.d/audit.rules") }}}'
-srg_requirement: '{{{ srg_requirement_audit_file_watch_rule("/etc/sudoers.d/") }}}'
+srg_requirement: '{{{ srg_requirement_audit_file_watch_rule("/etc/sudoers.d") }}}'
template:
name: audit_rules_watch
vars:
- path: /etc/sudoers.d/
+ path: /etc/sudoers.d
key: actions
diff --git a/shared/templates/audit_rules_watch/oval.template b/shared/templates/audit_rules_watch/oval.template
index f610dc6db7e4..ee4fbda2de1c 100644
--- a/shared/templates/audit_rules_watch/oval.template
+++ b/shared/templates/audit_rules_watch/oval.template
@@ -1,13 +1,13 @@
{{% macro local_variable_modern_style(lvarid, arch) %}}
- ^\-a\s+always,exit\s+\-F\s+arch={{{ arch }}}\s+\-F\s+{{{ FILTER_TYPE }}}=
+ ^\-a\s+always,exit\s+\-F\s+arch={{{ arch }}}\s+\-F\s+(dir|path)=
{{% if PATH_IS_VARIABLE %}}
{{% else %}}
{{{ PATH_ESCAPED }}}
{{% endif %}}
- \s+\-F\s+perm=\b([rx]*w[rx]*a[rx]*|[rx]*a[rx]*w[rx]*)\b.*$
+ /?\s+\-F\s+perm=\b([rx]*w[rx]*a[rx]*|[rx]*a[rx]*w[rx]*)\b.*$
{{% endmacro %}}
@@ -109,7 +109,7 @@
{{% else %}}
{{{ PATH_ESCAPED }}}
{{% endif %}}
- [\s]+\-p[\s]+\b([rx]*w[rx]*a[rx]*|[rx]*a[rx]*w[rx]*)\b.*$
+ /?[\s]+\-p[\s]+\b([rx]*w[rx]*a[rx]*|[rx]*a[rx]*w[rx]*)\b.*$
{{% endif %}}
diff --git a/shared/templates/audit_rules_watch/template.py b/shared/templates/audit_rules_watch/template.py
index 1ed61e14622e..732506b957bb 100644
--- a/shared/templates/audit_rules_watch/template.py
+++ b/shared/templates/audit_rules_watch/template.py
@@ -7,7 +7,13 @@ def preprocess(data, lang):
name = ssg.utils.escape_id(os.path.basename(os.path.normpath(path)))
data["name"] = name
if lang == "oval":
- data["path_escaped"] = path.replace("/", "\\/")
+ # Normalize away a trailing slash so it can be made optional in the
+ # OVAL pattern (via '/?'). This lets the check accept the watched path
+ # with or without a trailing slash regardless of how the rule is
+ # written on disk (e.g. both '-F dir=/etc/sudoers.d' and
+ # '-F path=/etc/sudoers.d/').
+ normalized_path = path.rstrip("/") or "/"
+ data["path_escaped"] = normalized_path.replace("/", "\\/")
if "key" not in data:
data["key"] = data["_rule_id"]
if data["path"].endswith("/"):
diff --git a/shared/templates/audit_rules_watch/tests/auditctl_alternate_filter_type.pass.sh b/shared/templates/audit_rules_watch/tests/auditctl_alternate_filter_type.pass.sh
new file mode 100644
index 000000000000..0908cdbb9209
--- /dev/null
+++ b/shared/templates/audit_rules_watch/tests/auditctl_alternate_filter_type.pass.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+# packages = audit
+
+{{{ setup_auditctl_environment() }}}
+path={{{ PATH }}}
+style={{{ audit_watches_style }}}
+filter_type={{{ FILTER_TYPE }}}
+. $SHARED/audit_rules_watch/auditctl_alternate_filter_type.pass.sh
diff --git a/shared/templates/audit_rules_watch/tests/auditctl_trailing_slash_toggle.pass.sh b/shared/templates/audit_rules_watch/tests/auditctl_trailing_slash_toggle.pass.sh
new file mode 100644
index 000000000000..5a30f28c1088
--- /dev/null
+++ b/shared/templates/audit_rules_watch/tests/auditctl_trailing_slash_toggle.pass.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+# packages = audit
+
+{{{ setup_auditctl_environment() }}}
+path={{{ PATH }}}
+style={{{ audit_watches_style }}}
+filter_type={{{ FILTER_TYPE }}}
+. $SHARED/audit_rules_watch/auditctl_trailing_slash_toggle.pass.sh
diff --git a/shared/templates/audit_rules_watch/tests/augenrules_alternate_filter_type.pass.sh b/shared/templates/audit_rules_watch/tests/augenrules_alternate_filter_type.pass.sh
new file mode 100644
index 000000000000..fa90c39cbe6c
--- /dev/null
+++ b/shared/templates/audit_rules_watch/tests/augenrules_alternate_filter_type.pass.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# packages = audit
+
+{{{ setup_augenrules_environment() }}}
+
+path={{{ PATH }}}
+style={{{ audit_watches_style }}}
+filter_type={{{ FILTER_TYPE }}}
+. $SHARED/audit_rules_watch/augenrules_alternate_filter_type.pass.sh
diff --git a/shared/templates/audit_rules_watch/tests/augenrules_trailing_slash_toggle.pass.sh b/shared/templates/audit_rules_watch/tests/augenrules_trailing_slash_toggle.pass.sh
new file mode 100644
index 000000000000..f3358e011245
--- /dev/null
+++ b/shared/templates/audit_rules_watch/tests/augenrules_trailing_slash_toggle.pass.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# packages = audit
+
+{{{ setup_augenrules_environment() }}}
+
+path={{{ PATH }}}
+style={{{ audit_watches_style }}}
+filter_type={{{ FILTER_TYPE }}}
+. $SHARED/audit_rules_watch/augenrules_trailing_slash_toggle.pass.sh
diff --git a/tests/shared/audit_rules_watch/auditctl_alternate_filter_type.pass.sh b/tests/shared/audit_rules_watch/auditctl_alternate_filter_type.pass.sh
new file mode 100644
index 000000000000..4daf4e3c7665
--- /dev/null
+++ b/tests/shared/audit_rules_watch/auditctl_alternate_filter_type.pass.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+# packages = audit
+
+# The OVAL check accepts either the 'dir=' (subtree) or 'path=' (single object)
+# filter type. Configure the rule using the opposite filter type from the one
+# the remediation would emit and confirm the check still passes.
+if [[ "$style" == "modern" ]] ; then
+ if [[ "$filter_type" == "dir" ]]; then alt_filter="path"; else alt_filter="dir"; fi
+ echo "-a always,exit -F arch=b32 -F $alt_filter=$path -F perm=wa -F key=logins" >> /etc/audit/audit.rules
+ echo "-a always,exit -F arch=b64 -F $alt_filter=$path -F perm=wa -F key=logins" >> /etc/audit/audit.rules
+else
+ # Legacy '-w' watches have no dir/path distinction; use the standard form.
+ echo "-w $path -p wa -k login" >> /etc/audit/audit.rules
+fi
diff --git a/tests/shared/audit_rules_watch/auditctl_trailing_slash_toggle.pass.sh b/tests/shared/audit_rules_watch/auditctl_trailing_slash_toggle.pass.sh
new file mode 100644
index 000000000000..a8ed2ef80000
--- /dev/null
+++ b/tests/shared/audit_rules_watch/auditctl_trailing_slash_toggle.pass.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+# packages = audit
+
+# The OVAL check treats a trailing slash on the watched path as optional.
+# Toggle the trailing slash relative to the rule's configured path and confirm
+# the check still passes.
+if [[ "$path" == */ ]]; then toggled_path="${path%/}"; else toggled_path="${path}/"; fi
+
+if [[ "$style" == "modern" ]] ; then
+ echo "-a always,exit -F arch=b32 -F $filter_type=$toggled_path -F perm=wa -F key=logins" >> /etc/audit/audit.rules
+ echo "-a always,exit -F arch=b64 -F $filter_type=$toggled_path -F perm=wa -F key=logins" >> /etc/audit/audit.rules
+else
+ echo "-w $toggled_path -p wa -k login" >> /etc/audit/audit.rules
+fi
diff --git a/tests/shared/audit_rules_watch/augenrules_alternate_filter_type.pass.sh b/tests/shared/audit_rules_watch/augenrules_alternate_filter_type.pass.sh
new file mode 100644
index 000000000000..2c8717c55070
--- /dev/null
+++ b/tests/shared/audit_rules_watch/augenrules_alternate_filter_type.pass.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+# packages = audit
+
+# The OVAL check accepts either the 'dir=' (subtree) or 'path=' (single object)
+# filter type. Configure the rule using the opposite filter type from the one
+# the remediation would emit and confirm the check still passes.
+if [[ "$style" == "modern" ]] ; then
+ if [[ "$filter_type" == "dir" ]]; then alt_filter="path"; else alt_filter="dir"; fi
+ echo "-a always,exit -F arch=b32 -F $alt_filter=$path -F perm=wa -F key=logins" >> /etc/audit/rules.d/login.rules
+ echo "-a always,exit -F arch=b64 -F $alt_filter=$path -F perm=wa -F key=logins" >> /etc/audit/rules.d/login.rules
+else
+ # Legacy '-w' watches have no dir/path distinction; use the standard form.
+ echo "-w $path -p wa -k login" >> /etc/audit/rules.d/login.rules
+fi
diff --git a/tests/shared/audit_rules_watch/augenrules_trailing_slash_toggle.pass.sh b/tests/shared/audit_rules_watch/augenrules_trailing_slash_toggle.pass.sh
new file mode 100644
index 000000000000..3259be7c3566
--- /dev/null
+++ b/tests/shared/audit_rules_watch/augenrules_trailing_slash_toggle.pass.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+# packages = audit
+
+# The OVAL check treats a trailing slash on the watched path as optional.
+# Toggle the trailing slash relative to the rule's configured path and confirm
+# the check still passes.
+if [[ "$path" == */ ]]; then toggled_path="${path%/}"; else toggled_path="${path}/"; fi
+
+if [[ "$style" == "modern" ]] ; then
+ echo "-a always,exit -F arch=b32 -F $filter_type=$toggled_path -F perm=wa -F key=logins" >> /etc/audit/rules.d/login.rules
+ echo "-a always,exit -F arch=b64 -F $filter_type=$toggled_path -F perm=wa -F key=logins" >> /etc/audit/rules.d/login.rules
+else
+ echo "-w $toggled_path -p wa -k login" >> /etc/audit/rules.d/login.rules
+fi