File 0049-OpenSCAP-module.patch of Package salt.4663
From 8d37933334f56766394c215c08110c51abf10b5f Mon Sep 17 00:00:00 2001
From: Mihai Dinca <mdinca@suse.de>
Date: Fri, 10 Feb 2017 09:22:17 +0100
Subject: [PATCH 49/49] OpenSCAP module
Include oscap returncode in response
Always return oscap's stderr
---
salt/modules/openscap.py | 109 +++++++++++++++++++
tests/unit/modules/openscap_test.py | 205 ++++++++++++++++++++++++++++++++++++
2 files changed, 314 insertions(+)
create mode 100644 salt/modules/openscap.py
create mode 100644 tests/unit/modules/openscap_test.py
diff --git a/salt/modules/openscap.py b/salt/modules/openscap.py
new file mode 100644
index 0000000000..2061550012
--- /dev/null
+++ b/salt/modules/openscap.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+import tempfile
+import shlex
+import shutil
+from subprocess import Popen, PIPE
+
+from salt.client import Caller
+
+
+ArgumentParser = object
+
+try:
+ import argparse # pylint: disable=minimum-python-version
+ ArgumentParser = argparse.ArgumentParser
+ HAS_ARGPARSE = True
+except ImportError: # python 2.6
+ HAS_ARGPARSE = False
+
+
+_XCCDF_MAP = {
+ 'eval': {
+ 'parser_arguments': [
+ (('--profile',), {'required': True}),
+ ],
+ 'cmd_pattern': (
+ "oscap xccdf eval "
+ "--oval-results --results results.xml --report report.html "
+ "--profile {0} {1}"
+ )
+ }
+}
+
+
+def __virtual__():
+ return HAS_ARGPARSE, 'argparse module is required.'
+
+
+class _ArgumentParser(ArgumentParser):
+
+ def __init__(self, action=None, *args, **kwargs):
+ super(_ArgumentParser, self).__init__(*args, prog='oscap', **kwargs)
+ self.add_argument('action', choices=['eval'])
+ add_arg = None
+ for params, kwparams in _XCCDF_MAP['eval']['parser_arguments']:
+ self.add_argument(*params, **kwparams)
+
+ def error(self, message, *args, **kwargs):
+ raise Exception(message)
+
+
+_OSCAP_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
+}
+
+
+def xccdf(params):
+ '''
+ Run ``oscap xccdf`` commands on minions.
+ It uses cp.push_dir to upload the generated files to the salt master
+ in the master's minion files cachedir
+ (defaults to ``/var/cache/salt/master/minions/minion-id/files``)
+
+ It needs ``file_recv`` set to ``True`` in the master configuration file.
+
+ CLI Example:
+
+ .. code-block:: bash
+
+ salt '*' openscap.xccdf "eval --profile Default /usr/share/openscap/scap-yast2sec-xccdf.xml"
+ '''
+ params = shlex.split(params)
+ policy = params[-1]
+
+ success = True
+ error = None
+ upload_dir = None
+ action = None
+ returncode = None
+
+ try:
+ parser = _ArgumentParser()
+ action = parser.parse_known_args(params)[0].action
+ args, argv = _ArgumentParser(action=action).parse_known_args(args=params)
+ except Exception as err:
+ success = False
+ error = str(err)
+
+ if success:
+ cmd = _XCCDF_MAP[action]['cmd_pattern'].format(args.profile, policy)
+ tempdir = tempfile.mkdtemp()
+ proc = Popen(
+ shlex.split(cmd), stdout=PIPE, stderr=PIPE, cwd=tempdir)
+ (stdoutdata, error) = proc.communicate()
+ success = _OSCAP_EXIT_CODES_MAP[proc.returncode]
+ returncode = proc.returncode
+ if success:
+ caller = Caller()
+ caller.cmd('cp.push_dir', tempdir)
+ shutil.rmtree(tempdir, ignore_errors=True)
+ upload_dir = tempdir
+
+ return dict(
+ success=success,
+ upload_dir=upload_dir,
+ error=error,
+ returncode=returncode)
diff --git a/tests/unit/modules/openscap_test.py b/tests/unit/modules/openscap_test.py
new file mode 100644
index 0000000000..327af75238
--- /dev/null
+++ b/tests/unit/modules/openscap_test.py
@@ -0,0 +1,205 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from subprocess import PIPE
+
+from salt.modules import openscap
+
+from salttesting import skipIf, TestCase
+from salttesting.mock import (
+ Mock,
+ MagicMock,
+ patch,
+ NO_MOCK,
+ NO_MOCK_REASON
+)
+
+
+@skipIf(NO_MOCK, NO_MOCK_REASON)
+class OpenscapTestCase(TestCase):
+
+ random_temp_dir = '/tmp/unique-name'
+ policy_file = '/usr/share/openscap/policy-file-xccdf.xml'
+
+ def setUp(self):
+ patchers = [
+ patch('salt.modules.openscap.Caller', MagicMock()),
+ patch('salt.modules.openscap.shutil.rmtree', Mock()),
+ patch(
+ 'salt.modules.openscap.tempfile.mkdtemp',
+ Mock(return_value=self.random_temp_dir)
+ ),
+ ]
+ for patcher in patchers:
+ self.apply_patch(patcher)
+
+ def apply_patch(self, patcher):
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ @patch(
+ 'salt.modules.openscap.Popen',
+ MagicMock(
+ return_value=Mock(
+ **{'returncode': 0, 'communicate.return_value': ('', '')}
+ )
+ )
+ )
+ def test_openscap_xccdf_eval_success(self):
+ response = openscap.xccdf(
+ 'eval --profile Default {0}'.format(self.policy_file))
+
+ self.assertEqual(openscap.tempfile.mkdtemp.call_count, 1)
+ expected_cmd = [
+ 'oscap',
+ 'xccdf',
+ 'eval',
+ '--oval-results',
+ '--results', 'results.xml',
+ '--report', 'report.html',
+ '--profile', 'Default',
+ self.policy_file
+ ]
+ openscap.Popen.assert_called_once_with(
+ expected_cmd,
+ cwd=openscap.tempfile.mkdtemp.return_value,
+ stderr=PIPE,
+ stdout=PIPE)
+ openscap.Caller().cmd.assert_called_once_with(
+ 'cp.push_dir', self.random_temp_dir)
+ self.assertEqual(openscap.shutil.rmtree.call_count, 1)
+ self.assertEqual(
+ response,
+ {
+ 'upload_dir': self.random_temp_dir,
+ 'error': '',
+ 'success': True,
+ 'returncode': 0
+ }
+ )
+
+ @patch(
+ 'salt.modules.openscap.Popen',
+ MagicMock(
+ return_value=Mock(
+ **{'returncode': 2, 'communicate.return_value': ('', 'some error')}
+ )
+ )
+ )
+ def test_openscap_xccdf_eval_success_with_failing_rules(self):
+ response = openscap.xccdf(
+ 'eval --profile Default {0}'.format(self.policy_file))
+
+ self.assertEqual(openscap.tempfile.mkdtemp.call_count, 1)
+ expected_cmd = [
+ 'oscap',
+ 'xccdf',
+ 'eval',
+ '--oval-results',
+ '--results', 'results.xml',
+ '--report', 'report.html',
+ '--profile', 'Default',
+ self.policy_file
+ ]
+ openscap.Popen.assert_called_once_with(
+ expected_cmd,
+ cwd=openscap.tempfile.mkdtemp.return_value,
+ stderr=PIPE,
+ stdout=PIPE)
+ openscap.Caller().cmd.assert_called_once_with(
+ 'cp.push_dir', self.random_temp_dir)
+ self.assertEqual(openscap.shutil.rmtree.call_count, 1)
+ self.assertEqual(
+ response,
+ {
+ 'upload_dir': self.random_temp_dir,
+ 'error': 'some error',
+ 'success': True,
+ 'returncode': 2
+ }
+ )
+
+ def test_openscap_xccdf_eval_fail_no_profile(self):
+ response = openscap.xccdf(
+ 'eval --param Default /unknown/param')
+ self.assertEqual(
+ response,
+ {
+ 'error': 'argument --profile is required',
+ 'upload_dir': None,
+ 'success': False,
+ 'returncode': None
+ }
+ )
+
+ @patch(
+ 'salt.modules.openscap.Popen',
+ MagicMock(
+ return_value=Mock(
+ **{'returncode': 2, 'communicate.return_value': ('', 'some error')}
+ )
+ )
+ )
+ def test_openscap_xccdf_eval_success_ignore_unknown_params(self):
+ response = openscap.xccdf(
+ 'eval --profile Default --param Default /policy/file')
+ self.assertEqual(
+ response,
+ {
+ 'upload_dir': self.random_temp_dir,
+ 'error': 'some error',
+ 'success': True,
+ 'returncode': 2
+ }
+ )
+ expected_cmd = [
+ 'oscap',
+ 'xccdf',
+ 'eval',
+ '--oval-results',
+ '--results', 'results.xml',
+ '--report', 'report.html',
+ '--profile', 'Default',
+ '/policy/file'
+ ]
+ openscap.Popen.assert_called_once_with(
+ expected_cmd,
+ cwd=openscap.tempfile.mkdtemp.return_value,
+ stderr=PIPE,
+ stdout=PIPE)
+
+ @patch(
+ 'salt.modules.openscap.Popen',
+ MagicMock(
+ return_value=Mock(**{
+ 'returncode': 1,
+ 'communicate.return_value': ('', 'evaluation error')
+ })
+ )
+ )
+ def test_openscap_xccdf_eval_evaluation_error(self):
+ response = openscap.xccdf(
+ 'eval --profile Default {0}'.format(self.policy_file))
+
+ self.assertEqual(
+ response,
+ {
+ 'upload_dir': None,
+ 'error': 'evaluation error',
+ 'success': False,
+ 'returncode': 1
+ }
+ )
+
+ def test_openscap_xccdf_eval_fail_not_implemented_action(self):
+ response = openscap.xccdf('info {0}'.format(self.policy_file))
+
+ self.assertEqual(
+ response,
+ {
+ 'upload_dir': None,
+ 'error': "argument action: invalid choice: 'info' (choose from 'eval')",
+ 'success': False,
+ 'returncode': None
+ }
+ )
--
2.11.0