Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Mihai Dinca
committed
Feb 17, 2017
1 parent
8b8ab8e
commit 9d13422
Showing
2 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
) |