From 9d13422ac14f7e81769a60ec7179b6edc2064d7c Mon Sep 17 00:00:00 2001 From: Mihai Dinca Date: Fri, 10 Feb 2017 09:22:17 +0100 Subject: [PATCH] OpenSCAP module --- salt/modules/openscap.py | 105 ++++++++++++++ tests/unit/modules/openscap_test.py | 207 ++++++++++++++++++++++++++++ 2 files changed, 312 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 000000000000..b50ede7c286e --- /dev/null +++ b/salt/modules/openscap.py @@ -0,0 +1,105 @@ +# -*- 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 + + 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, stderrdata) = proc.communicate() + success = _OSCAP_EXIT_CODES_MAP[proc.returncode] + if success: + caller = Caller() + caller.cmd('cp.push_dir', tempdir) + shutil.rmtree(tempdir, ignore_errors=True) + upload_dir = tempdir + else: + error = stderrdata + + return dict(success=success, upload_dir=upload_dir, error=error) diff --git a/tests/unit/modules/openscap_test.py b/tests/unit/modules/openscap_test.py new file mode 100644 index 000000000000..c883975690d0 --- /dev/null +++ b/tests/unit/modules/openscap_test.py @@ -0,0 +1,207 @@ +# -*- 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': None, 'success': True + } + ) + + @patch( + 'salt.modules.openscap.Popen', + MagicMock( + return_value=Mock( + **{'returncode': 2, 'communicate.return_value': ('', '')} + ) + ) + ) + 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': None, + 'success': True + } + ) + + 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 + } + ) + + @patch( + 'salt.modules.openscap.Popen', + MagicMock( + return_value=Mock( + **{'returncode': 2, 'communicate.return_value': ('', '')} + ) + ) + ) + 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': None, + 'success': True + } + ) + 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 + } + ) + + @patch( + 'salt.modules.openscap.Popen', + MagicMock( + return_value=Mock(**{ + 'returncode': 1, + 'communicate.return_value': ('', 'evaluation error') + }) + ) + ) + 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 + } + )