Merge "Add auditd.py module"
diff --git a/README.rst b/README.rst
index ec2992d..b9df62f 100644
--- a/README.rst
+++ b/README.rst
@@ -18,10 +18,17 @@
 of binaries which have suid/sgid bit for all mounted file systems which do not
 have **nosuid** or **noexec** mount option (except the *special* file systems
 such as **sysfs**, **nsfs**, **cgroup**, **proc** and so one).
+The list of such *special* file systems can be configured
+with auditd:rules:filter_fs pillar.
+
 It was done because it is nearly impossible to create that list manually. It
 always will differ from one installation to another.
 This behavior can not be changed but it can be extended manually by putting
-necessary rules into the **rule_list** list).
+necessary rules into the **rule_list** list.
+
+Also it is possible to add paths which will be filtered in search. It implements
+the idea of *white lists* but on a directory level, not for a particular file.
+It can be configured with auditd:rules:filter_paths pillar.
 
 
 Sample Metadata
diff --git a/_modules/auditd.py b/_modules/auditd.py
new file mode 100644
index 0000000..d5445a8
--- /dev/null
+++ b/_modules/auditd.py
@@ -0,0 +1,90 @@
+"""
+Module for finding executable files with suid or sgid bit.
+It was implemented in opposite to using __salt__[cmd.run](find)
+due to complexity of filter logic in audit rules template.
+"""
+
+import multiprocessing.dummy
+import functools
+import logging
+import stat
+import os
+import salt.utils
+
+log = logging.getLogger(__name__)
+
+def __virtual__():
+    if salt.utils.is_windows():
+        return False
+    return 'auditd'
+
+def _find_privileged_files(path, filtered=None, mounts=None):
+    """Find executable files with suid or sbig bit.
+    The function accepts list of paths wich will be skiped in search.
+    """
+
+    mounts = mounts or []
+    filtered = filtered or []
+    e_bits = stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
+    s_bits = stat.S_ISUID | stat.S_ISGID
+
+    for root, dirs, files in os.walk(path, topdown=True):
+        if (root in mounts and root != path) or root in filtered:
+            # Filter all mount points which lies down of the path
+            # (implements the `-xdev` of the find command),
+            # as well as all "filtered" paths (`-prune`/`-not`).
+            dirs[:] = []
+            files[:] = []
+        for fname in files:
+            fpath = os.path.join(root, fname)
+            if os.path.islink(fpath):
+                continue
+            mode = os.stat(fpath).st_mode
+            if (mode & e_bits) and (mode & s_bits):
+                yield fpath
+
+def find_privileged(filter_fs=None, filter_paths=None, filter_mounts=True):
+
+    filter_fs = filter_fs or []
+    filtered = filter_paths or []
+    mounts = []
+
+    for part, mount_opts in __salt__['mount.active']().items():
+        if mount_opts['fstype'] in filter_fs:
+            # Must be skipped according to the fstype filter.
+            log.debug("Add '%s' to filtered (filtered fs)", part)
+            filtered.append(part)
+            continue
+        if filter_mounts:
+            if set(['noexec', 'nosuid']) & set(mount_opts['opts']):
+                # It has noexec/nosuid option in mount options.
+                # Must be skipped.
+                log.debug("Add '%s' to filtered (mount options)", part)
+                filtered.append(part)
+                continue
+        if part == '/':
+            continue
+        mounts.append(part)
+
+    # Collect all directories for parallel scanning
+    scan_dirs = ['/'+d for d in os.listdir('/') if os.path.isdir('/'+d)]
+
+    # Add all mount points into the scan dirs list
+    for mount_point in mounts:
+        if mount_point in scan_dirs:
+            # It is already in the scan list
+            continue
+        if any([mount_point.startswith(f) for f in filtered]):
+            # Such mount point must be skipped
+            # (implements `-prune`/`-not` behavior of the find command)
+            continue
+        scan_dirs.append(mount_point)
+
+    pool = multiprocessing.dummy.Pool(len(scan_dirs))
+    find = functools.partial(_find_privileged_files,
+                             filtered=filtered,
+                             mounts=mounts)
+
+    p = pool.map_async(find, scan_dirs)
+
+    return sorted([item for res in p.get() for item in res])
diff --git a/auditd/files/auditd.rules.conf b/auditd/files/auditd.rules.conf
index 2920129..8012bcb 100644
--- a/auditd/files/auditd.rules.conf
+++ b/auditd/files/auditd.rules.conf
@@ -1,9 +1,6 @@
 {%- from "auditd/map.jinja" import rules with context %}
-{%- set filtered_fs = [
-'binfmt_misc', 'cgroup', 'configfs', 'debugfs',
-'devpts', 'mqueue', 'nsfs', 'proc', 'pstore',
-'securityfs', 'sysfs','tracefs'
-]-%}
+{%- set filter_fs = rules.get('filter_fs', []) -%}
+{%- set filter_paths = rules.get('filter_paths', []) -%}
 
 -D
 {%- set bufsize = rules.get('options', {}).get('bufsize', 8192) %}
@@ -22,26 +19,17 @@
 {%- endfor %}
 {%- endif %}
 
-{# Dynamically generated list of binaries which has sgid/suid bit #}
-{# It can be extended via auditd.rules.rules.privileged.rule_list #}
+{# Dynamically generated list of binaries which have sgid/suid bit #}
+{# It can be extended via the auditd.rules.rules.privileged.rule_list #}
 {%- if pillar.auditd.rules.rules.privileged is defined -%}
 {%-   if ruledict.privileged.get('enabled') -%}
-{%-     for partition, options in salt['mount.active']().items() -%}
-{%-         if ('noexec' not in options.opts) and
-               ('nosuid' not in options.opts) and
-               options.fstype not in filtered_fs -%}
-{%-           set cmd = 'find '+ partition +' -xdev \( -perm -4000 -o -perm -2000 \) -type f' %}
-{%-           set binaries = salt['cmd.shell'](cmd).splitlines() -%}
-{%-           if binaries|length > 0 %}
-# Dinamically generated privileged list for {{ partition }}
-{%-           endif %}
-{%-           for bin in binaries -%}
-{%-             set rule = '-a always,exit -F path='+ bin +' -F perm=x -F auid>=1000 -F auid!=4294967295' -%}
-{%-             if rule not in ruledict.get('privileged', {}).get('rule_list', []) %}
+{%-     set privileged_list = salt['auditd.find_privileged'](filter_fs=filter_fs,filter_paths=filter_paths) -%}
+# Dinamically generated list of privileged files
+{%-     for bin in privileged_list -%}
+{%-         set rule = '-a always,exit -F path='+ bin +' -F perm=x -F auid>=1000 -F auid!=4294967295' -%}
+{%-         if rule not in ruledict.get('privileged', {}).get('rule_list', []) %}
 {{ rule + ' -k privileged' }}
-{%-             endif %}
-{%-           endfor -%}
-{%-       endif -%}
+{%-         endif %}
 {%-     endfor -%}
 {%-   endif -%}
 {%- endif %}
diff --git a/metadata/service/rules/ciscat.yml b/metadata/service/rules/ciscat.yml
index 1596dd5..2417e1c 100644
--- a/metadata/service/rules/ciscat.yml
+++ b/metadata/service/rules/ciscat.yml
@@ -5,6 +5,22 @@
 parameters:
   auditd:
     rules:
+      filter_fs:
+      - binfmt_misc
+      - cgroup
+      - debugfs
+      - devpts
+      - devtmpfs
+      - fusectl
+      - hugetlbfs
+      - mqueue
+      - nsfs
+      - proc
+      - pstore
+      - securityfs
+      - sysfs
+      filter_paths:
+      - /var/lib/docker
       options:
         enabled: 2
         bufsize: 8192