Blob Blame History Raw
commit 26f72c842ec184ed517fbf0d3224c421ad7cc9c6
Author: Gabriel Becker <ggasparb@redhat.com>
Date:   Thu Feb 24 18:33:50 2022 +0100

    Manual edited patch scap-security-guide-0.1.59-multifile_templates-PR_7405.patch.

diff --git a/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/ansible/shared.yml b/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/ansible/shared.yml
deleted file mode 100644
index f6f2ab4..0000000
--- a/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/ansible/shared.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# platform = multi_platform_sle,Red Hat Enterprise Linux 8,multi_platform_fedora
-# reboot = false
-# strategy = restrict
-# complexity = medium
-# disruption = medium
-- name: "Read list libraries without root ownership"
-  find:
-    paths:
-      - "/usr/lib"
-      - "/usr/lib64"
-      - "/lib"
-      - "/lib64"
-    file_type: "directory"
-  register: library_dirs_not_group_owned_by_root
-
-- name: "Set group ownership of system library dirs to root"
-  file:
-    path: "{{ item.path }}"
-    group: "root"
-    state: "directory"
-    mode: "{{ item.mode }}"
-  with_items: "{{ library_dirs_not_group_owned_by_root.files }}"
-  when:
-    - library_dirs_not_group_owned_by_root.matched > 0
-    - item.gid != 0
diff --git a/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/bash/shared.sh b/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/bash/shared.sh
deleted file mode 100644
index 365b983..0000000
--- a/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/bash/shared.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-# platform = multi_platform_sle,Red Hat Enterprise Linux 8,multi_platform_fedora
-
-find /lib \
-/lib64 \
-/usr/lib \
-/usr/lib64 \
-\! -group root -type d -exec chgrp root '{}' \;
diff --git a/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/oval/shared.xml b/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/oval/shared.xml
deleted file mode 100644
index 3af60ff..0000000
--- a/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/oval/shared.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<def-group>
-  <definition class="compliance" id="dir_group_ownership_library_dirs" version="1">
-    {{{ oval_metadata("
-        Checks that /lib, /lib64, /usr/lib, /usr/lib64, /lib/modules, and
-        directories therein, are group-owned by root.
-      ") }}}
-    <criteria operator="AND">
-      <criterion test_ref="test_dir_group_ownership_lib_dir" />
-    </criteria>
-  </definition>
-
-  <unix:file_test  check="all" check_existence="none_exist" comment="library directories gid root" id="test_dir_group_ownership_lib_dir" version="1">
-    <unix:object object_ref="object_dir_group_ownership_lib_dir" />
-  </unix:file_test>
-
-  <unix:file_object comment="library directories" id="object_dir_group_ownership_lib_dir" version="1">
-    <!-- Check that /lib, /lib64, /usr/lib, and /usr/lib64 directories belong to group with gid 0 (root) -->
-    <unix:path operation="pattern match">(^\/lib(|64)\/|^\/usr\/lib(|64)\/)</unix:path>
-    <unix:filename xsi:nil="true" />
-    <filter action="include">state_group_owner_library_dirs_not_root</filter>
-  </unix:file_object>
-
-  <unix:file_state id="state_group_owner_library_dirs_not_root" version="1">
-    <unix:group_id datatype="int" operation="not equal">0</unix:group_id>
-  </unix:file_state>
-
-</def-group>
diff --git a/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/rule.yml b/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/rule.yml
index 8c0acc0..10203c9 100644
--- a/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/rule.yml
+++ b/linux_os/guide/system/permissions/files/permissions_within_important_dirs/dir_group_ownership_library_dirs/rule.yml
@@ -1,6 +1,6 @@
 documentation_complete: true
 
-prodtype: sle12,sle15,rhel8,fedora
+prodtype: fedora,rhel8,sle12,sle15,ubuntu2004
 
 title: 'Verify that Shared Library Directories Have Root Group Ownership'
 
@@ -40,6 +40,7 @@ references:
     stigid@rhel8: RHEL-08-010350
     stigid@sle12: SLES-12-010876
     stigid@sle15: SLES-15-010356
+    stigid@ubuntu2004: UBTU-20-010431
 
 ocil_clause: 'any of these directories are not group-owned by root'
 
@@ -52,3 +53,14 @@ ocil: |-
     For each of these directories, run the following command to find files not
     owned by root:
     <pre>$ sudo find -L <i>$DIR</i> ! -user root -type d -exec chgrp root {} \;</pre>
+
+template:
+    name: file_groupowner
+    vars:
+        filepath:
+            - /lib/
+            - /lib64/
+            - /usr/lib/
+            - /usr/lib64/
+        recursive: 'true'
+        filegid: '0'
diff --git a/products/ubuntu2004/profiles/stig.profile b/products/ubuntu2004/profiles/stig.profile
index ac96858..4c76824 100644
--- a/products/ubuntu2004/profiles/stig.profile
+++ b/products/ubuntu2004/profiles/stig.profile
@@ -470,6 +470,7 @@ selections:
     # UBTU-20-010430 The Ubuntu operating system library files must be group-owned by root.
 
     # UBTU-20-010431 The Ubuntu operating system library directories must be group-owned by root.
+    - dir_group_ownership_library_dirs
 
     # UBTU-20-010432 The Ubuntu operating system must be configured to preserve log records from failure events.
     - service_rsyslog_enabled
diff --git a/shared/templates/file_groupowner/ansible.template b/shared/templates/file_groupowner/ansible.template
index 073d356..68fc2e1 100644
--- a/shared/templates/file_groupowner/ansible.template
+++ b/shared/templates/file_groupowner/ansible.template
@@ -4,33 +4,44 @@
 # complexity = low
 # disruption = low
 
+{{% for path in FILEPATH %}}
 {{% if IS_DIRECTORY and FILE_REGEX %}}
 
-- name: Find {{{ FILEPATH }}} file(s) matching {{{ FILE_REGEX }}}
+- name: Find {{{ path }}} file(s) matching {{{ FILE_REGEX[loop.index0] }}}
   find:
-    paths: "{{{ FILEPATH }}}"
-    patterns: "{{{ FILE_REGEX }}}"
+    paths: "{{{ path }}}"
+    patterns: {{{ FILE_REGEX[loop.index0] }}}
     use_regex: yes
   register: files_found
 
-- name: Ensure group owner on {{{ FILEPATH }}} file(s) matching {{{ FILE_REGEX }}}
+- name: Ensure group owner on {{{ path }}} file(s) matching {{{ FILE_REGEX[loop.index0] }}}
   file:
     path: "{{ item.path }}"
     group: "{{{ FILEGID }}}"
   with_items:
     - "{{ files_found.files }}"
 
+{{% elif IS_DIRECTORY and RECURSIVE %}}
+
+- name: Ensure group owner on {{{ path }}} recursively
+  file:
+    path: "{{{ path }}}"
+    state: directory
+    recurse: yes
+    group: "{{{ FILEGID }}}"
+
 {{% else %}}
 
-- name: Test for existence {{{ FILEPATH }}}
+- name: Test for existence {{{ path }}}
   stat:
-    path: "{{{ FILEPATH }}}"
+    path: "{{{ path }}}"
   register: file_exists
 
-- name: Ensure group owner {{{ FILEGID }}} on {{{ FILEPATH }}}
+- name: Ensure group owner {{{ FILEGID }}} on {{{ path }}}
   file:
-    path: "{{{ FILEPATH }}}"
+    path: "{{{ path }}}"
     group: "{{{ FILEGID }}}"
   when: file_exists.stat is defined and file_exists.stat.exists
 
 {{% endif %}}
+{{% endfor %}}
diff --git a/shared/templates/file_groupowner/bash.template b/shared/templates/file_groupowner/bash.template
index 442e015..982d2f3 100644
--- a/shared/templates/file_groupowner/bash.template
+++ b/shared/templates/file_groupowner/bash.template
@@ -4,13 +4,17 @@
 # complexity = low
 # disruption = low
 
+{{% for path in FILEPATH %}}
 {{% if IS_DIRECTORY and FILE_REGEX %}}
-readarray -t files < <(find {{{ FILEPATH }}})
+readarray -t files < <(find {{{ path }}})
 for file in "${files[@]}"; do
-    if basename $file | grep -q '{{{ FILE_REGEX }}}'; then
+    if basename $file | grep -qE '{{{ FILE_REGEX[loop.index0] }}}'; then
         chgrp {{{ FILEGID }}} $file
     fi
 done
+{{% elif IS_DIRECTORY and RECURSIVE %}}
+find -L {{{ path }}} -type d -exec chgrp {{{ FILEGID }}} {} \;
 {{% else %}}
-chgrp {{{ FILEGID }}} {{{ FILEPATH }}}
+chgrp {{{ FILEGID }}} {{{ path }}}
 {{% endif %}}
+{{% endfor %}}
diff --git a/shared/templates/file_groupowner/oval.template b/shared/templates/file_groupowner/oval.template
index 1b637a6..fd2e5db 100644
--- a/shared/templates/file_groupowner/oval.template
+++ b/shared/templates/file_groupowner/oval.template
@@ -1,8 +1,16 @@
 <def-group>
   <definition class="compliance" id="{{{ _RULE_ID }}}" version="1">
+   {{% if FILEPATH is not string %}}
+      {{{ oval_metadata("This test makes sure that FILEPATH is group owned by " + FILEGID + ".") }}}
+      <criteria>
+    {{% for filepath in FILEPATH %}}
+      <criterion comment="Check file group ownership of {{{ filepath }}}" test_ref="test_file_groupowner{{{ FILEID }}}_{{{ loop.index0 }}}" />
+    {{% endfor %}}
+   {{% else %}}
     {{{ oval_metadata("This test makes sure that " + FILEPATH + " is group owned by " + FILEGID + ".") }}}
     <criteria>
       <criterion comment="Check file group ownership of {{{ FILEPATH }}}" test_ref="test_file_groupowner{{{ FILEID }}}" />
+   {{% endif %}}
     </criteria>
   </definition>
   {{%- if MISSING_FILE_PASS -%}}
@@ -12,23 +20,31 @@
     {{# All defined files must exist. When using regex, at least one file must match #}}
     {{% set FILE_EXISTENCE = "all_exist" %}}
   {{%- endif -%}}
-  <unix:file_test check="all" check_existence="{{{ FILE_EXISTENCE }}}" comment="Testing group ownership of {{{ FILEPATH }}}" id="test_file_groupowner{{{ FILEID }}}" version="1">
-    <unix:object object_ref="object_file_groupowner{{{ FILEID }}}" />
-    <unix:state state_ref="state_file_groupowner{{{ FILEID }}}_gid_{{{ FILEGID }}}" />
+
+
+  {{% for filepath in FILEPATH %}}
+  <unix:file_test check="all" check_existence="{{{ FILE_EXISTENCE }}}" comment="Testing group ownership of {{{ filepath }}}" id="test_file_groupowner{{{ FILEID }}}_{{{ loop.index0 }}}" version="1">
+    <unix:object object_ref="object_file_groupowner{{{ FILEID }}}_{{{ loop.index0 }}}" />
+    <unix:state state_ref="state_file_groupowner{{{ FILEID }}}_gid_{{{ FILEGID }}}_{{{ loop.index0 }}}" />
   </unix:file_test>
-  <unix:file_state id="state_file_groupowner{{{ FILEID }}}_gid_{{{ FILEGID }}}" version="1">
+  <unix:file_state id="state_file_groupowner{{{ FILEID }}}_gid_{{{ FILEGID }}}_{{{ loop.index0 }}}" version="1">
     <unix:group_id datatype="int">{{{ FILEGID }}}</unix:group_id>
   </unix:file_state>
-  <unix:file_object comment="{{{ FILEPATH }}}" id="object_file_groupowner{{{ FILEID }}}" version="1">
+  <unix:file_object comment="{{{ filepath }}}" id="object_file_groupowner{{{ FILEID }}}_{{{ loop.index0 }}}" version="1">
     {{%- if IS_DIRECTORY -%}}
-      <unix:path>{{{ FILEPATH }}}</unix:path>
-      {{%- if FILE_REGEX -%}}
-        <unix:filename operation="pattern match">{{{ FILE_REGEX }}}</unix:filename>
-      {{%- else -%}}
-        <unix:filename xsi:nil="true" />
-      {{%- endif -%}}
-    {{%- else -%}}
-      <unix:filepath{{% if FILEPATH_IS_REGEX %}} operation="pattern match"{{% endif %}}>{{{ FILEPATH }}}</unix:filepath>
-    {{%- endif -%}}
+      {{%- if FILE_REGEX %}}
+      <unix:path>{{{ filepath[:-1] }}}</unix:path>
+      <unix:filename operation="pattern match">{{{ FILE_REGEX[loop.index0] }}}</unix:filename>
+      {{%- elif RECURSIVE %}}
+      <unix:path operation="pattern match">{{{ filepath[:-1] }}}</unix:path>
+      <unix:filename xsi:nil="true" />
+      {{%- else %}}
+      <unix:path>{{{ filepath[:-1] }}}</unix:path>
+      <unix:filename xsi:nil="true" />
+      {{%- endif %}}
+    {{%- else %}}
+      <unix:filepath{{% if FILEPATH_IS_REGEX %}} operation="pattern match"{{% endif %}}>{{{ filepath }}}</unix:filepath>
+    {{%- endif %}}
   </unix:file_object>
+  {{% endfor %}}
 </def-group>
diff --git a/shared/templates/file_groupowner/template.py b/shared/templates/file_groupowner/template.py
index 2263ae8..10baed9 100644
--- a/shared/templates/file_groupowner/template.py
+++ b/shared/templates/file_groupowner/template.py
@@ -1,12 +1,25 @@
-from ssg.utils import parse_template_boolean_value
+from ssg.utils import parse_template_boolean_value, check_conflict_regex_directory
 
 def _file_owner_groupowner_permissions_regex(data):
-    data["is_directory"] = data["filepath"].endswith("/")
-    if "file_regex" in data and not data["is_directory"]:
-        raise ValueError(
-            "Used 'file_regex' key in rule '{0}' but filepath '{1}' does not "
-            "specify a directory. Append '/' to the filepath or remove the "
-            "'file_regex' key.".format(data["_rule_id"], data["filepath"]))
+    # this avoids code duplicates
+    if isinstance(data["filepath"], str):
+        data["filepath"] = [data["filepath"]]
+
+    if "file_regex" in data:
+        # we can have a list of filepaths, but only one regex
+        # instead of declaring the same regex multiple times
+        if isinstance(data["file_regex"], str):
+            data["file_regex"] = [data["file_regex"]] * len(data["filepath"])
+
+        # if the length of filepaths and file_regex are not the same, then error.
+        # in case we have multiple regexes for just one filepath, than we need
+        # to declare that filepath multiple times
+        if len(data["filepath"]) != len(data["file_regex"]):
+            raise ValueError(
+                "You should have one file_path per file_regex. Please check "
+                "rule '{0}'".format(data["_rule_id"]))
+
+    check_conflict_regex_directory(data)
 
 
 def preprocess(data, lang):
@@ -14,6 +27,10 @@ def preprocess(data, lang):
 
     data["missing_file_pass"] = parse_template_boolean_value(data, parameter="missing_file_pass", default_value=False)
 
+    data["recursive"] = parse_template_boolean_value(data,
+                                                     parameter="recursive",
+                                                     default_value=False)
+
     if lang == "oval":
         data["fileid"] = data["_rule_id"].replace("file_groupowner", "")
     return data
diff --git a/shared/templates/file_owner/ansible.template b/shared/templates/file_owner/ansible.template
index 6083fbe..80eaae8 100644
--- a/shared/templates/file_owner/ansible.template
+++ b/shared/templates/file_owner/ansible.template
@@ -4,33 +4,44 @@
 # complexity = low
 # disruption = low
 
+{{% for path in FILEPATH %}}
 {{% if IS_DIRECTORY and FILE_REGEX %}}
 
-- name: Find {{{ FILEPATH }}} file(s) matching {{{ FILE_REGEX }}}
+- name: Find {{{ path }}} file(s) matching {{{ FILE_REGEX[loop.index0] }}}
   find:
-    paths: "{{{ FILEPATH }}}"
-    patterns: "{{{ FILE_REGEX }}}"
+    paths: "{{{ path }}}"
+    patterns: {{{ FILE_REGEX[loop.index0] }}}
     use_regex: yes
   register: files_found
 
-- name: Ensure group owner on {{{ FILEPATH }}} file(s) matching {{{ FILE_REGEX }}}
+- name: Ensure group owner on {{{ path }}} file(s) matching {{{ FILE_REGEX[loop.index0] }}}
   file:
     path: "{{ item.path }}"
     owner: "{{{ FILEUID }}}"
   with_items:
     - "{{ files_found.files }}"
 
+{{% elif IS_DIRECTORY and RECURSIVE %}}
+
+- name: Ensure owner on {{{ path }}} recursively
+  file:
+    paths "{{{ path }}}"
+    state: directory
+    recurse: yes
+    owner: "{{{ FILEUID }}}"
+
 {{% else %}}
 
-- name: Test for existence {{{ FILEPATH }}}
+- name: Test for existence {{{ path }}}
   stat:
-    path: "{{{ FILEPATH }}}"
+    path: "{{{ path }}}"
   register: file_exists
 
-- name: Ensure owner {{{ FILEUID }}} on {{{ FILEPATH }}}
+- name: Ensure owner {{{ FILEUID }}} on {{{ path }}}
   file:
-    path: "{{{ FILEPATH }}}"
+    path: "{{{ path }}}"
     owner: "{{{ FILEUID }}}"
   when: file_exists.stat is defined and file_exists.stat.exists
 
 {{% endif %}}
+{{% endfor %}}
diff --git a/shared/templates/file_owner/bash.template b/shared/templates/file_owner/bash.template
index 16025b7..27b5a2a 100644
--- a/shared/templates/file_owner/bash.template
+++ b/shared/templates/file_owner/bash.template
@@ -4,13 +4,17 @@
 # complexity = low
 # disruption = low
 
+{{% for path in FILEPATH %}}
 {{% if IS_DIRECTORY and FILE_REGEX %}}
-readarray -t files < <(find {{{ FILEPATH }}})
+readarray -t files < <(find {{{ path }}})
 for file in "${files[@]}"; do
-    if basename $file | grep -q '{{{ FILE_REGEX }}}'; then
+    if basename $file | grep -qE '{{{ FILE_REGEX[loop.index0] }}}'; then
         chown {{{ FILEUID }}} $file
     fi
 done
+{{% elif IS_DIRECTORY and RECURSIVE %}}
+find -L {{{ path }}} -type d -exec chown {{{ FILEUID }}} {} \;
 {{% else %}}
-chown {{{ FILEUID }}} {{{ FILEPATH }}}
+chown {{{ FILEUID }}} {{{ path }}}
 {{% endif %}}
+{{% endfor %}}
diff --git a/shared/templates/file_owner/oval.template b/shared/templates/file_owner/oval.template
index 23ac161..105e29c 100644
--- a/shared/templates/file_owner/oval.template
+++ b/shared/templates/file_owner/oval.template
@@ -1,8 +1,16 @@
 <def-group>
   <definition class="compliance" id="{{{ _RULE_ID }}}" version="1">
+  {{% if FILEPATH is not string %}}
+    {{{ oval_metadata("This test makes sure that FILEPATH is owned by " + FILEUID + ".") }}}
+     <criteria>
+   {{% for filepath in FILEPATH %}}
+     <criterion comment="Check file ownership of {{{ filepath }}}" test_ref="test_file_owner{{{ FILEID }}}_{{{ loop.index0 }}}" />
+   {{% endfor %}}
+  {{% else %}}
     {{{ oval_metadata("This test makes sure that " + FILEPATH + " is owned by " + FILEUID + ".") }}}
     <criteria>
       <criterion comment="Check file ownership of {{{ FILEPATH }}}" test_ref="test_file_owner{{{ FILEID }}}" />
+  {{% endif %}}
     </criteria>
   </definition>
   {{%- if MISSING_FILE_PASS -%}}
@@ -12,23 +20,30 @@
     {{# All defined files must exist. When using regex, at least one file must match #}}
     {{% set FILE_EXISTENCE = "all_exist" %}}
   {{%- endif -%}}
-  <unix:file_test check="all" check_existence="{{{ FILE_EXISTENCE }}}" comment="Testing user ownership of {{{ FILEPATH }}}" id="test_file_owner{{{ FILEID }}}" version="1">
-    <unix:object object_ref="object_file_owner{{{ FILEID }}}" />
-    <unix:state state_ref="state_file_owner{{{ FILEID }}}_uid_{{{ FILEUID }}}" />
+
+  {{% for filepath in FILEPATH %}}
+  <unix:file_test check="all" check_existence="{{{ FILE_EXISTENCE }}}" comment="Testing user ownership of {{{ filepath }}}" id="test_file_owner{{{ FILEID }}}_{{{ loop.index0 }}}" version="1">
+    <unix:object object_ref="object_file_owner{{{ FILEID }}}_{{{ loop.index0 }}}" />
+    <unix:state state_ref="state_file_owner{{{ FILEID }}}_uid_{{{ FILEUID }}}_{{{ loop.index0 }}}" />
   </unix:file_test>
-  <unix:file_state id="state_file_owner{{{ FILEID }}}_uid_{{{ FILEUID }}}" version="1">
+  <unix:file_state id="state_file_owner{{{ FILEID }}}_uid_{{{ FILEUID }}}_{{{ loop.index0 }}}" version="1">
     <unix:user_id datatype="int">{{{ FILEUID }}}</unix:user_id>
   </unix:file_state>
-  <unix:file_object comment="{{{ FILEPATH }}}" id="object_file_owner{{{ FILEID }}}" version="1">
+  <unix:file_object comment="{{{ filepath }}}" id="object_file_owner{{{ FILEID }}}_{{{ loop.index0 }}}" version="1">
     {{%- if IS_DIRECTORY -%}}
-      <unix:path>{{{ FILEPATH }}}</unix:path>
-      {{%- if FILE_REGEX -%}}
-        <unix:filename operation="pattern match">{{{ FILE_REGEX }}}</unix:filename>
-      {{%- else -%}}
-        <unix:filename xsi:nil="true" />
-      {{%- endif -%}}
-    {{%- else -%}}
-      <unix:filepath{{% if FILEPATH_IS_REGEX %}} operation="pattern match"{{% endif %}}>{{{ FILEPATH }}}</unix:filepath>
-    {{%- endif -%}}
+      {{%- if FILE_REGEX %}}
+      <unix:path>{{{ filepath[:-1] }}}</unix:path>
+      <unix:filename operation="pattern match">{{{ FILE_REGEX[loop.index0] }}}</unix:filename>
+      {{%- elif RECURSIVE %}}
+      <unix:path operation="pattern match">{{{ filepath[:-1] }}}</unix:path>
+      <unix:filename xsi:nil="true" />
+      {{%- else %}}
+      <unix:path>{{{ filepath[:-1] }}}</unix:path>
+      <unix:filename xsi:nil="true" />
+      {{%- endif %}}
+    {{%- else %}}
+      <unix:filepath{{% if FILEPATH_IS_REGEX %}} operation="pattern match"{{% endif %}}>{{{ filepath }}}</unix:filepath>
+    {{%- endif %}}
   </unix:file_object>
+  {{% endfor %}}
 </def-group>
diff --git a/shared/templates/file_owner/template.py b/shared/templates/file_owner/template.py
index 0dd0008..1391dcf 100644
--- a/shared/templates/file_owner/template.py
+++ b/shared/templates/file_owner/template.py
@@ -1,12 +1,25 @@
-from ssg.utils import parse_template_boolean_value
+from ssg.utils import parse_template_boolean_value, check_conflict_regex_directory
 
 def _file_owner_groupowner_permissions_regex(data):
-    data["is_directory"] = data["filepath"].endswith("/")
-    if "file_regex" in data and not data["is_directory"]:
-        raise ValueError(
-            "Used 'file_regex' key in rule '{0}' but filepath '{1}' does not "
-            "specify a directory. Append '/' to the filepath or remove the "
-            "'file_regex' key.".format(data["_rule_id"], data["filepath"]))
+    # this avoids code duplicates
+    if isinstance(data["filepath"], str):
+        data["filepath"] = [data["filepath"]]
+
+    if "file_regex" in data:
+        # we can have a list of filepaths, but only one regex
+        # instead of declaring the same regex multiple times
+        if isinstance(data["file_regex"], str):
+            data["file_regex"] = [data["file_regex"]] * len(data["filepath"])
+
+        # if the length of filepaths and file_regex are not the same, then error.
+        # in case we have multiple regexes for just one filepath, than we need
+        # to declare that filepath multiple times
+        if len(data["filepath"]) != len(data["file_regex"]):
+            raise ValueError(
+                "You should have one file_path per file_regex. Please check "
+                "rule '{0}'".format(data["_rule_id"]))
+
+    check_conflict_regex_directory(data)
 
 
 def preprocess(data, lang):
@@ -14,6 +27,10 @@ def preprocess(data, lang):
 
     data["missing_file_pass"] = parse_template_boolean_value(data, parameter="missing_file_pass", default_value=False)
 
+    data["recursive"] = parse_template_boolean_value(data,
+                                                     parameter="recursive",
+                                                     default_value=False)
+
     if lang == "oval":
         data["fileid"] = data["_rule_id"].replace("file_owner", "")
     return data
diff --git a/shared/templates/file_permissions/ansible.template b/shared/templates/file_permissions/ansible.template
index 029d03f..fc211bd 100644
--- a/shared/templates/file_permissions/ansible.template
+++ b/shared/templates/file_permissions/ansible.template
@@ -3,33 +3,45 @@
 # strategy = configure
 # complexity = low
 # disruption = low
+
+{{% for path in FILEPATH %}}
 {{% if IS_DIRECTORY and FILE_REGEX %}}
 
-- name: Find {{{ FILEPATH }}} file(s)
+- name: Find {{{ path }}} file(s)
   find:
-    paths: "{{{ FILEPATH }}}"
-    patterns: "{{{ FILE_REGEX }}}"
+    paths: "{{{ path }}}"
+    patterns: {{{ FILE_REGEX[loop.index0] }}}
     use_regex: yes
   register: files_found
 
-- name: Set permissions for {{{ FILEPATH }}} file(s)
+- name: Set permissions for {{{ path }}} file(s)
   file:
     path: "{{ item.path }}"
     mode: "{{{ FILEMODE }}}"
   with_items:
     - "{{ files_found.files }}"
 
+{{% elif IS_DIRECTORY and RECURSIVE %}}
+
+- name: Set permissions for {{{ path }}} recursively
+  file:
+    path: "{{{ path }}}"
+    state: directory
+    recurse: yes
+    mode: "{{{ FILEMODE }}}"
+
 {{% else %}}
 
-- name: Test for existence {{{ FILEPATH }}}
+- name: Test for existence {{{ path }}}
   stat:
-    path: "{{{ FILEPATH }}}"
+    path: "{{{ path }}}"
   register: file_exists
   
-- name: Ensure permission {{{ FILEMODE }}} on {{{ FILEPATH }}}
+- name: Ensure permission {{{ FILEMODE }}} on {{{ path }}}
   file:
-    path: "{{{ FILEPATH }}}"
+    path: "{{{ path }}}"
     mode: "{{{ FILEMODE }}}"
   when: file_exists.stat is defined and file_exists.stat.exists
 
 {{% endif %}}
+{{% endfor %}}
diff --git a/shared/templates/file_permissions/bash.template b/shared/templates/file_permissions/bash.template
index af9cf4e..e0d8fe9 100644
--- a/shared/templates/file_permissions/bash.template
+++ b/shared/templates/file_permissions/bash.template
@@ -4,13 +4,17 @@
 # complexity = low
 # disruption = low
 
+{{% for path in FILEPATH %}}
 {{% if IS_DIRECTORY and FILE_REGEX %}}
-readarray -t files < <(find {{{ FILEPATH }}})
+readarray -t files < <(find {{{ path }}})
 for file in "${files[@]}"; do
-    if basename $file | grep -q '{{{ FILE_REGEX }}}'; then
+    if basename $file | grep -qE '{{{ FILE_REGEX[loop.index0] }}}'; then
         chmod {{{ FILEMODE }}} $file
     fi    
 done
+{{% elif IS_DIRECTORY and RECURSIVE %}}
+find -L {{{ path }}} -type d -exec chmod {{{ FILEMODE }}} {} \;
 {{% else %}}
-chmod {{{ FILEMODE }}} {{{ FILEPATH }}}
+chmod {{{ FILEMODE }}} {{{ path }}}
 {{% endif %}}
+{{% endfor %}}
diff --git a/shared/templates/file_permissions/oval.template b/shared/templates/file_permissions/oval.template
index f570ff8..89083e8 100644
--- a/shared/templates/file_permissions/oval.template
+++ b/shared/templates/file_permissions/oval.template
@@ -16,31 +16,47 @@
 {{%- endif -%}}
 <def-group>
   <definition class="compliance" id="{{{ _RULE_ID }}}" version="1">
-    {{{ oval_metadata("This test makes sure that " + FILEPATH + " has mode " + FILEMODE + ".
+  {{% if FILEPATH is not string %}}
+    {{{ oval_metadata("This test makes sure that FILEPATH has mode " + FILEMODE + ".
+      If the target file or directory has an extended ACL, then it will fail the mode check.
+      ") }}}
+    <criteria>
+  {{% for filepath in FILEPATH %}}
+      <criterion comment="Check file mode of {{{ filepath }}}" test_ref="test_file_permissions{{{ FILEID }}}_{{{ loop.index0 }}}"{{{ ' negate="true"' if ALLOW_STRICTER_PERMISSIONS }}}/>
+  {{% endfor %}}
+  {{% else %}}
+    {{{ oval_metadata("This test makes sure that " +  FILEPATH + " has mode " + FILEMODE + ".
       If the target file or directory has an extended ACL, then it will fail the mode check.
       ") }}}
     <criteria>
       <criterion comment="Check file mode of {{{ FILEPATH }}}" test_ref="test_file_permissions{{{ FILEID }}}"{{{ ' negate="true"' if ALLOW_STRICTER_PERMISSIONS }}}/>
+  {{% endif %}}
     </criteria>
   </definition>
-      <unix:file_test check="all" check_existence="{{{ FILE_EXISTENCE }}}" comment="Testing mode of {{{ FILEPATH }}}" id="test_file_permissions{{{ FILEID }}}" version="2">
-      <unix:object object_ref="object_file_permissions{{{ FILEID }}}" />
-      <unix:state state_ref="state_file_permissions{{{ FILEID }}}_mode_{{{ 'not_' if ALLOW_STRICTER_PERMISSIONS }}}{{{ FILEMODE }}}" />
-    </unix:file_test>
-    <unix:file_state id="state_file_permissions{{{ FILEID }}}_mode_{{{ 'not_' if ALLOW_STRICTER_PERMISSIONS }}}{{{ FILEMODE }}}"{{{ ' operator="OR"' if ALLOW_STRICTER_PERMISSIONS }}} version="2">
+
+  {{% for filepath in FILEPATH %}}
+  <unix:file_test check="all" check_existence="{{{ FILE_EXISTENCE }}}" comment="Testing mode of {{{ filepath }}}" id="test_file_permissions{{{ FILEID }}}_{{{ loop.index0 }}}" version="2">
+    <unix:object object_ref="object_file_permissions{{{ FILEID }}}_{{{ loop.index0 }}}" />
+    <unix:state state_ref="state_file_permissions{{{ FILEID }}}_{{{ loop.index0 }}}_mode_{{{ 'not_' if ALLOW_STRICTER_PERMISSIONS }}}{{{ FILEMODE }}}" />
+  </unix:file_test>
+  <unix:file_state id="state_file_permissions{{{ FILEID }}}_{{{ loop.index0 }}}_mode_{{{ 'not_' if ALLOW_STRICTER_PERMISSIONS }}}{{{ FILEMODE }}}"{{{ ' operator="OR"' if ALLOW_STRICTER_PERMISSIONS }}} version="2">
       {{{ STATEMODE | indent(6) }}}
-    </unix:file_state>
-    <unix:file_object comment="{{{ FILEPATH }}}" id="object_file_permissions{{{ FILEID }}}" version="1">
+  </unix:file_state>
+  <unix:file_object comment="{{{ filepath }}}" id="object_file_permissions{{{ FILEID }}}_{{{ loop.index0 }}}" version="1">
 
     {{%- if IS_DIRECTORY %}}
-      <unix:path>{{{ FILEPATH }}}</unix:path>
       {{%- if FILE_REGEX %}}
-      <unix:filename operation="pattern match">{{{ FILE_REGEX }}}</unix:filename>
+      <unix:path>{{{ filepath[:-1] }}}</unix:path>
+      <unix:filename operation="pattern match">{{{ FILE_REGEX[loop.index0] }}}</unix:filename>
+      {{%- elif RECURSIVE %}}
+      <unix:path operation="pattern match">{{{ filepath[:-1] }}}</unix:path>
+      <unix:filename xsi:nil="true" />
       {{%- else %}}
+      <unix:path>{{{ filepath[:-1] }}}</unix:path>
       <unix:filename xsi:nil="true" />
       {{%- endif %}}
     {{%- else %}}
-      <unix:filepath{{% if FILEPATH_IS_REGEX %}} operation="pattern match"{{% endif %}}>{{{ FILEPATH }}}</unix:filepath>
+      <unix:filepath{{% if FILEPATH_IS_REGEX %}} operation="pattern match"{{% endif %}}>{{{ filepath }}}</unix:filepath>
     {{%- endif %}}
 
     {{%- if ALLOW_STRICTER_PERMISSIONS %}}
@@ -49,8 +65,8 @@
           https://github.com/OpenSCAP/openscap/pull/1709 but this line should be kept until the
           fix is widely available. The fix is expected to be part of OpenSCAP >= 1.3.5.
       #}}
-      <filter action="include">state_file_permissions{{{ FILEID }}}_mode_not_{{{ FILEMODE }}}</filter>
+      <filter action="include">state_file_permissions{{{ FILEID }}}_{{{ loop.index0 }}}_mode_not_{{{ FILEMODE }}}</filter>
     {{%- endif %}}
-
-    </unix:file_object>
+  </unix:file_object>
+  {{% endfor %}}
 </def-group>
diff --git a/shared/templates/file_permissions/template.py b/shared/templates/file_permissions/template.py
index 677e083..6e20a62 100644
--- a/shared/templates/file_permissions/template.py
+++ b/shared/templates/file_permissions/template.py
@@ -1,12 +1,25 @@
-from ssg.utils import parse_template_boolean_value
+from ssg.utils import parse_template_boolean_value, check_conflict_regex_directory
 
 def _file_owner_groupowner_permissions_regex(data):
-    data["is_directory"] = data["filepath"].endswith("/")
-    if "file_regex" in data and not data["is_directory"]:
-        raise ValueError(
-            "Used 'file_regex' key in rule '{0}' but filepath '{1}' does not "
-            "specify a directory. Append '/' to the filepath or remove the "
-            "'file_regex' key.".format(data["_rule_id"], data["filepath"]))
+    # this avoids code duplicates
+    if isinstance(data["filepath"], str):
+        data["filepath"] = [data["filepath"]]
+
+    if "file_regex" in data:
+        # we can have a list of filepaths, but only one regex
+        # instead of declaring the same regex multiple times
+        if isinstance(data["file_regex"], str):
+            data["file_regex"] = [data["file_regex"]] * len(data["filepath"])
+
+        # if the length of filepaths and file_regex are not the same, then error.
+        # in case we have multiple regexes for just one filepath, than we need
+        # to declare that filepath multiple times
+        if len(data["filepath"]) != len(data["file_regex"]):
+            raise ValueError(
+                "You should have one file_path per file_regex. Please check "
+                "rule '{0}'".format(data["_rule_id"]))
+
+    check_conflict_regex_directory(data)
 
 
 def preprocess(data, lang):
@@ -16,6 +29,10 @@ def preprocess(data, lang):
 
     data["missing_file_pass"] = parse_template_boolean_value(data, parameter="missing_file_pass", default_value=False)
 
+    data["recursive"] = parse_template_boolean_value(data,
+                                                     parameter="recursive",
+                                                     default_value=False)
+
     if lang == "oval":
         data["fileid"] = data["_rule_id"].replace("file_permissions", "")
         # build the state that describes our mode
diff --git a/ssg/utils.py b/ssg/utils.py
index b0ded09..2248b1e 100644
--- a/ssg/utils.py
+++ b/ssg/utils.py
@@ -303,3 +303,25 @@ def parse_template_boolean_value(data, parameter, default_value):
         raise ValueError(
             "Template parameter {} used in rule {} cannot accept the "
             "value {}".format(parameter, data["_rule_id"], value))
+
+
+def check_conflict_regex_directory(data):
+    """
+    Validate that either all path are directories OR file_regex exists.
+
+    Throws ValueError.
+    """
+    for f in data["filepath"]:
+        if "is_directory" in data and data["is_directory"] != f.endswith("/"):
+            raise ValueError(
+                "If passing a list of filepaths, all of them need to be "
+                "either directories or files. Mixing is not possible. "
+                "Please fix rules '{0}' filepath '{1}'".format(data["_rule_id"], f))
+
+        data["is_directory"] = f.endswith("/")
+
+        if "file_regex" in data and not data["is_directory"]:
+            raise ValueError(
+                "Used 'file_regex' key in rule '{0}' but filepath '{1}' does not "
+                "specify a directory. Append '/' to the filepath or remove the "
+                "'file_regex' key.".format(data["_rule_id"], f))