Create openscap formula

This formula allows to install openscap schemas and utils.
Also, here is a simple oscap execution module.

Change-Id: Ib113f9a739deafbc4cf85c97b071636d0161cb54
Closes-PROD: https://mirantis.jira.com/browse/PROD-20392
diff --git a/_modules/oscap/__init__.py b/_modules/oscap/__init__.py
new file mode 100644
index 0000000..88cdcba
--- /dev/null
+++ b/_modules/oscap/__init__.py
@@ -0,0 +1,106 @@
+from __future__ import absolute_import
+from oscap.commands import xccdf, oval
+import logging
+import os
+import salt.utils
+import shutil
+import stat
+
+_OSCAP_XCCDF_EXIT_CODES_MAP = {
+    0: True,  # all rules pass
+    1: False,  # there is an error during evaluation
+    2: True  # there is at least one rule with either fail or unknown result
+}
+
+DIR_MODE = '0750'
+
+log = logging.getLogger(__name__)
+
+def __virtual__():
+    if not salt.utils.which('oscap'):
+        return (False, 'oscap is required.')
+    return 'oscap'
+
+def eval(evaltype,
+         benchmark,
+         profile='default',
+         xccdf_version='1.2',
+         tailoring_id=None,
+         fetch_from_master=False,
+         benchmark_url=None,
+         benchmark_basepath='output',
+         saltenv='base',
+         results_dir=None):
+    '''
+    `oscap eval` wrapper
+    :param  evaltype:           what to evaluate, can be `xccdf` or `oval`
+    :param  profile:            xccdf profile (default 'default')
+    :param  xccdf_version       xccdf benchmark version (default 1.2)
+    :param  tailoring_id        id of your tailoring data (default None)
+    :param  fetch_from_master:  fetch oscap input data from the master (default False)
+    :param  benchmark_basepath: basepath where the benchmark will be searched (default 'output')
+    :param  saltenv:            saltenv, used for cached files (default 'base')
+    :param  results_dir:        directory for storing results (default {{__opts__[cachedir]}}/files/openscap/latest)
+
+    :return:                    success,returncode,results_dir, optionally stderr
+
+    Usage example:
+
+        salt MINION oscap.eval xccdf /tmp/myxccdf.xml tailoring_id=myenv_oscap_variables profile='anssi_nt28_high'
+    '''
+    success = False
+    stderr = None
+    returncode = None
+    pillar_data = None
+
+    latest_dir = os.path.join(__opts__['cachedir'], 'files', 'openscap', 'latest')
+    results_dir = os.path.normpath(results_dir) if results_dir else latest_dir
+
+    benchmark_url = benchmark_url if benchmark_url else \
+                        'salt://{}/{}'.format(benchmark_basepath,
+                                              os.path.dirname(benchmark))
+
+    if fetch_from_master:
+        _ret_ = __salt__['cp.cache_dir'](benchmark_url)
+        benchmark = benchmark[1:] if os.path.isabs(benchmark) else benchmark
+        benchmark = os.path.join(__opts__['cachedir'], 'files',
+                                 saltenv, benchmark_basepath, benchmark)
+
+    if evaltype == 'xccdf':
+        if tailoring_id:
+            pillar_data = __salt__['pillar.get']('openscap:tailoring:{}'\
+                                                 .format(tailoring_id))
+        (stdout, stderr, rc, temp_dir) = xccdf(benchmark,
+                                              profile=profile,
+                                              pillar_data=pillar_data,
+                                              tailoring_id=tailoring_id,
+                                              xccdf_version=xccdf_version)
+
+        success = _OSCAP_XCCDF_EXIT_CODES_MAP[rc]
+    elif evaltype == 'oval':
+        (stdout, stderr, rc, temp_dir) = oval(benchmark)
+        success = not rc
+    else:
+        raise RuntimeError('Unsupported oscap command "{}"'.format(evaltype))
+
+    ret = {
+        'success': success,
+        'returncode': rc,
+        'results_dir': results_dir
+        }
+
+    if success:
+        mode = int(str(stat.S_IMODE(int(DIR_MODE))), 8)
+        base_dir = os.path.dirname(results_dir)
+        if os.path.isdir(results_dir):
+            shutil.rmtree(results_dir)
+        if not os.path.isdir(base_dir):
+            os.makedirs(base_dir)
+            os.chmod(base_dir, mode)
+        os.rename(temp_dir, latest_dir)
+        os.chmod(latest_dir, mode)
+
+    if stderr:
+        ret['stderr'] = stderr
+
+    return ret
diff --git a/_modules/oscap/commands.py b/_modules/oscap/commands.py
new file mode 100644
index 0000000..44c703d
--- /dev/null
+++ b/_modules/oscap/commands.py
@@ -0,0 +1,51 @@
+from __future__ import absolute_import
+import tempfile
+import os
+from oscap.utils import build_tailoring, normalize_id, run
+
+def oscap_has_sce():
+    (stdout, _, _) = run('oscap -V')
+    return any([x for x in stdout.splitlines() if x.startswith('SCE Vers')])
+
+def xccdf(benchmark,
+          pillar_data=None,
+          xccdf_version='1.2',
+          profile='default',
+          tailoring_id=None):
+
+    tailoring_file = None
+    profile = normalize_id(profile, xccdf_version=xccdf_version)
+
+    tempdir = tempfile.mkdtemp(prefix='oscap-')
+
+    if pillar_data:
+        if not tailoring_id:
+            raise Exception('Tailoring id must be set!')
+        profile = normalize_id(pillar_data['profile'], typeof='profile')
+        tailoring_file = os.path.join(tempdir, 'tailoring.xml')
+
+    cmd = 'oscap xccdf eval --profile {profile} ' +\
+          '--results results.xml --report report.html'
+    if oscap_has_sce():
+        cmd += ' --sce-results'
+    if tailoring_file:
+        cmd += ' --tailoring-file {tailoring_file}'
+    cmd += ' {benchmark}'
+    cmd = cmd.format(profile=profile,
+                     tailoring_file=tailoring_file,
+                     benchmark=benchmark)
+
+    if tailoring_file:
+        with open(tailoring_file, 'w') as f:
+            f.write(build_tailoring(pillar_data, tailoring_id))
+
+    stdout, stderr, rc = run(cmd, tempdir)
+    return stdout, stderr, rc, tempdir
+
+def oval(benchmark):
+    tempdir = tempfile.mkdtemp(prefix='oscap-')
+    cmd = 'oscap oval eval --results results.xml --report report.html {}'
+    cmd = cmd.format(benchmark)
+
+    stdout, stderr, rc = run(cmd, tempdir)
+    return stdout, stderr, rc, tempdir
diff --git a/_modules/oscap/utils.py b/_modules/oscap/utils.py
new file mode 100644
index 0000000..164949d
--- /dev/null
+++ b/_modules/oscap/utils.py
@@ -0,0 +1,46 @@
+from lxml.etree import Element, SubElement, tostring
+from subprocess import Popen, PIPE
+import shlex
+import re
+import datetime
+
+import salt.ext.six as six
+
+def normalize_id(id,
+                 xccdf_version='1.2',
+                 typeof='profile',
+                 vendor='mirantis'):
+
+    if xccdf_version == '1.2':
+        if not re.match('^xccdf_[^_]+_{}_.+'.format(typeof), id):
+            return 'xccdf_org.{0}.content_{1}_{2}'.format(vendor, typeof, id)
+    return id
+
+def build_tailoring(data, id):
+    xccdf_version = data.get('xccdf_version', '1.2')
+    ns = {None: 'http://checklists.nist.gov/xccdf/{}'.format(xccdf_version)}
+    tid = normalize_id(id, xccdf_version, typeof='tailoring')
+    pid = normalize_id(data['profile'], xccdf_version, vendor='customer')
+    ext = normalize_id(data['extends'], xccdf_version)
+    tailoring = Element('Tailoring', nsmap=ns, id=tid)
+    tailoring.append(Element('benchmark', {'href': ext}))
+
+    now = datetime.datetime.now().isoformat()
+    version = SubElement(tailoring, 'version', time=now).text = '1'
+
+    profile = SubElement(tailoring, 'Profile', id=pid, extends=ext)
+
+    title = SubElement(profile, 'title').text = \
+        'Extends {}'.format(ext)
+
+    for key, value in six.iteritems(data.get('values', {})):
+        idref = normalize_id(key, xccdf_version, typeof='value')
+        elem = SubElement(profile, 'set-value', idref=idref)
+        elem.text = str(value)
+    return tostring(tailoring, pretty_print=True)
+
+def run(cmd, cwd=None):
+    # The Popen used here because the __salt__['cmd.run'] returns only stdout
+    proc = Popen(shlex.split(cmd), stdout=PIPE, stderr=PIPE, cwd=cwd)
+    (stdout, stderr) = proc.communicate()
+    return stdout, stderr, proc.returncode