Skip to content

Commit

Permalink
OpenSCAP module
Browse files Browse the repository at this point in the history
  • Loading branch information
Mihai Dinca committed Feb 17, 2017
1 parent 8b8ab8e commit 9d13422
Show file tree
Hide file tree
Showing 2 changed files with 312 additions and 0 deletions.
105 changes: 105 additions & 0 deletions 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)
207 changes: 207 additions & 0 deletions 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
}
)

0 comments on commit 9d13422

Please sign in to comment.