Passed
Push — master ( 7a880f...a8ac27 )
by
unknown
27:41 queued 21:44
created

tests.unit_tests.test_scap_result_parser   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 336
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 214
dl 0
loc 336
rs 10
c 0
b 0
f 0
wmc 22

14 Functions

Rating   Name   Duplication   Size   Complexity  
A get_parser() 0 5 2
A test_rationale() 0 44 1
A get_root() 0 5 2
A test_warnings() 0 36 1
A test_description() 0 47 1
A get_ref_values() 0 4 1
A test_validation() 0 21 1
A test_parse_report() 0 14 1
A get_rules() 0 6 1
A test_get_profile_info() 0 23 1
A get_test_results() 0 2 1
A test_multi_check() 0 14 3
A test_get_info_about_rules_in_profile() 0 22 2
A test_remediations() 0 36 4
1
# Copyright 2022, Red Hat, Inc.
2
# SPDX-License-Identifier: LGPL-2.1-or-later
3
4
import pytest
5
from lxml import etree
6
7
from openscap_report.scap_results_parser import SCAPResultsParser
8
from openscap_report.scap_results_parser.data_structures import Report, Rule
9
from openscap_report.scap_results_parser.namespaces import NAMESPACES
10
from openscap_report.scap_results_parser.parsers import (ReportParser,
11
                                                         RuleParser)
12
13
from ..constants import (PATH_TO_ARF, PATH_TO_ARF_SCANNED_ON_CONTAINER,
14
                         PATH_TO_ARF_WITH_MULTI_CHECK,
15
                         PATH_TO_ARF_WITH_OS_CPE_CHECK,
16
                         PATH_TO_ARF_WITHOUT_INFO,
17
                         PATH_TO_ARF_WITHOUT_SYSTEM_DATA,
18
                         PATH_TO_EMPTY_XML_FILE, PATH_TO_REMEDIATIONS_SCRIPTS,
19
                         PATH_TO_RULE_AND_CPE_CHECK_ARF,
20
                         PATH_TO_RULE_AND_CPE_CHECK_XCCDF,
21
                         PATH_TO_SIMPLE_RULE_FAIL_ARF,
22
                         PATH_TO_SIMPLE_RULE_FAIL_XCCDF,
23
                         PATH_TO_SIMPLE_RULE_PASS_ARF,
24
                         PATH_TO_SIMPLE_RULE_PASS_XCCDF, PATH_TO_XCCDF,
25
                         PATH_TO_XCCDF_WITH_MULTI_CHECK,
26
                         PATH_TO_XCCDF_WITHOUT_INFO,
27
                         PATH_TO_XCCDF_WITHOUT_SYSTEM_DATA)
28
29
30
def get_parser(file_path):
31
    xml_data = None
32
    with open(file_path, "r", encoding="utf-8") as xml_report:
33
        xml_data = xml_report.read().encode()
34
    return SCAPResultsParser(xml_data)
35
36
37
def get_root(file_path):
38
    xml_data = None
39
    with open(file_path, "r", encoding="utf-8") as xml_report:
40
        xml_data = xml_report.read().encode()
41
    return etree.XML(xml_data)
42
43
44
def get_test_results(root):
45
    return root.find('.//xccdf:TestResult', NAMESPACES)
46
47
48
def get_ref_values(root):
49
    return {
50
        ref_value.get("idref"): ref_value.text
51
        for ref_value in root.findall('.//xccdf:set-value', NAMESPACES)
52
    }
53
54
55
def get_rules(file_path=PATH_TO_ARF):
56
    root = get_root(file_path)
57
    test_results = get_test_results(root)
58
    ref_values = get_ref_values(root)
59
    rule_parser = RuleParser(root, test_results, ref_values)
60
    return rule_parser.get_rules()
61
62
63
@pytest.mark.unit_test
64
@pytest.mark.parametrize("file_path, result", [
65
    (PATH_TO_ARF, True),
66
    (PATH_TO_SIMPLE_RULE_PASS_ARF, True),
67
    (PATH_TO_SIMPLE_RULE_FAIL_ARF, True),
68
    (PATH_TO_RULE_AND_CPE_CHECK_ARF, True),
69
    (PATH_TO_ARF_WITHOUT_INFO, True),
70
    (PATH_TO_ARF_WITHOUT_SYSTEM_DATA, True),
71
    (PATH_TO_ARF_SCANNED_ON_CONTAINER, True),
72
    (PATH_TO_ARF_WITH_OS_CPE_CHECK, True),
73
    (PATH_TO_XCCDF, False),
74
    (PATH_TO_SIMPLE_RULE_PASS_XCCDF, False),
75
    (PATH_TO_SIMPLE_RULE_FAIL_XCCDF, False),
76
    (PATH_TO_RULE_AND_CPE_CHECK_XCCDF, False),
77
    (PATH_TO_XCCDF_WITHOUT_INFO, False),
78
    (PATH_TO_XCCDF_WITHOUT_SYSTEM_DATA, False),
79
    (PATH_TO_EMPTY_XML_FILE, False),
80
])
81
def test_validation(file_path, result):
82
    parser = get_parser(file_path)
83
    assert parser.validate(parser.arf_schemas_path) == result
84
85
86
@pytest.mark.unit_test
87
@pytest.mark.parametrize("file_path, number_of_cpe_platforms, os_cpe_platform", [
88
    (PATH_TO_ARF, 13, "cpe:/o:fedoraproject:fedora:32"),
89
    (PATH_TO_XCCDF, 13, "cpe:/o:fedoraproject:fedora:32"),
90
    (PATH_TO_SIMPLE_RULE_PASS_ARF, 0, ""),
91
    (PATH_TO_SIMPLE_RULE_FAIL_ARF, 0, ""),
92
    (PATH_TO_ARF_WITHOUT_INFO, 0, ""),
93
    (PATH_TO_ARF_WITHOUT_SYSTEM_DATA, 0, ""),
94
    (PATH_TO_ARF_SCANNED_ON_CONTAINER, 6, "cpe:/o:fedoraproject:fedora:35"),
95
    (PATH_TO_RULE_AND_CPE_CHECK_ARF, 1, "cpe:/o:example:applicable:5"),
96
    (PATH_TO_ARF_WITH_OS_CPE_CHECK, 0, "cpe:/o:fedoraproject:fedora:1"),
97
    (PATH_TO_SIMPLE_RULE_PASS_XCCDF, 0, ""),
98
    (PATH_TO_SIMPLE_RULE_FAIL_XCCDF, 0, ""),
99
    (PATH_TO_XCCDF_WITHOUT_INFO, 0, ""),
100
    (PATH_TO_XCCDF_WITHOUT_SYSTEM_DATA, 0, ""),
101
    (PATH_TO_RULE_AND_CPE_CHECK_XCCDF, 1, "cpe:/o:example:applicable:5"),
102
])
103
def test_get_profile_info(file_path, number_of_cpe_platforms, os_cpe_platform):
104
    root = get_root(file_path)
105
    report_parser = ReportParser(root, get_test_results(root))
106
    report = report_parser.get_report()
107
    assert len(report.cpe_platforms) == number_of_cpe_platforms
108
    assert report.platform == os_cpe_platform
109
110
111
@pytest.mark.unit_test
112
@pytest.mark.parametrize("file_path, number_of_rules", [
113
    (PATH_TO_ARF, 714),
114
    (PATH_TO_XCCDF, 714),
115
    (PATH_TO_ARF_SCANNED_ON_CONTAINER, 121),
116
    (PATH_TO_SIMPLE_RULE_PASS_ARF, 1),
117
    (PATH_TO_SIMPLE_RULE_FAIL_ARF, 1),
118
    (PATH_TO_ARF_WITHOUT_INFO, 1),
119
    (PATH_TO_ARF_WITHOUT_SYSTEM_DATA, 1),
120
    (PATH_TO_ARF_WITH_OS_CPE_CHECK, 1),
121
    (PATH_TO_RULE_AND_CPE_CHECK_ARF, 3),
122
    (PATH_TO_SIMPLE_RULE_PASS_XCCDF, 1),
123
    (PATH_TO_SIMPLE_RULE_FAIL_XCCDF, 1),
124
    (PATH_TO_XCCDF_WITHOUT_INFO, 1),
125
    (PATH_TO_XCCDF_WITHOUT_SYSTEM_DATA, 1),
126
    (PATH_TO_RULE_AND_CPE_CHECK_XCCDF, 3),
127
])
128
def test_get_info_about_rules_in_profile(file_path, number_of_rules):
129
    rules = get_rules(file_path)
130
    assert len(rules.keys()) == number_of_rules
131
    for rule in rules.values():
132
        assert isinstance(rule, Rule)
133
134
135
@pytest.mark.unit_test
136
@pytest.mark.parametrize("file_path, contains_oval_tree", [
137
    (PATH_TO_ARF, True),
138
    (PATH_TO_XCCDF, False),
139
])
140
def test_parse_report(file_path, contains_oval_tree):
141
    parser = get_parser(file_path)
142
    report = parser.parse_report()
143
    assert isinstance(report, Report)
144
    assert report.profile_name is not None
145
    assert report.rules is not None
146
    rule_id = "xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny"
147
    assert isinstance(report.rules[rule_id], Rule)
148
    assert (report.rules[rule_id].oval_tree is not None) == contains_oval_tree
149
150
151
@pytest.mark.unit_test
152
@pytest.mark.parametrize("file_path, contains_rules_some_multi_check_rule", [
153
    (PATH_TO_ARF, False),
154
    (PATH_TO_XCCDF, False),
155
    (PATH_TO_XCCDF_WITH_MULTI_CHECK, True),
156
    (PATH_TO_ARF_WITH_MULTI_CHECK, True),
157
])
158
def test_multi_check(file_path, contains_rules_some_multi_check_rule):
159
    rules = get_rules(file_path)
160
    result = False
161
    for rule in rules.values():
162
        if rule.multi_check:
163
            result = True
164
    assert result == contains_rules_some_multi_check_rule
165
166
167
@pytest.mark.unit_test
168
@pytest.mark.parametrize("rule, result", [
169
    (
170
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
171
        "Prefer installation of 64-bit operating systems when the CPU supports it."
172
    ),
173
    (
174
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
175
        (
176
            "\nTo activate locking of the screensaver in the GNOME3 desktop"
177
            " when it is activated,\nadd or set <code>lock-enabled</code>"
178
            " to <code>true</code> in\n<code>/etc/dconf/db/local.d/00-security-settings</code>."
179
            " For example:\n<pre>[org/gnome/desktop/screensaver]\nlock-enabled=true\n</pre>\n"
180
            "Once the settings have been added, add a lock to\n"
181
            "<code>/etc/dconf/db/local.d/locks/00-security-settings-lock</code> "
182
            "to prevent user modification.\nFor example:\n"
183
            "<pre>/org/gnome/desktop/screensaver/lock-enabled</pre>\n"
184
            "After the settings have been set, run <code>dconf update</code>."
185
        )
186
    ),
187
    (
188
        "xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct",
189
        (
190
            "The <code>auditd</code> service can be configured to send email to\n"
191
            "a designated account in certain situations. Add or correct the following line\n"
192
            "in <code>/etc/audit/auditd.conf</code> to ensure that administrators are notified\n"
193
            "via email for those situations:\n<pre>action_mail_acct = root</pre>"
194
        )
195
    ),
196
    (
197
        "xccdf_org.ssgproject.content_rule_chronyd_specify_remote_server",
198
        (
199
            "<code>Chrony</code> is a daemon which implements"
200
            " the Network Time Protocol (NTP). It is designed to\n"
201
            "synchronize system clocks across a variety of systems and"
202
            " use a source that is highly\naccurate. More information on"
203
            " <code>chrony</code> can be found at\n\n    "
204
            "<a href=\"http://chrony.tuxfamily.org/\">http://chrony.tuxfamily.org/</a>.\n"
205
            "<code>Chrony</code> can be configured to be a client and/or a server.\n"
206
            "Add or edit server or pool lines to <code>/etc/chrony.conf</code> as appropriate:\n"
207
            "<pre>server &lt;remote-server&gt;</pre>\nMultiple servers may be configured."
208
        )
209
    ),
210
])
211
def test_description(rule, result):
212
    rules = get_rules()
213
    assert rules[rule].description == result
214
215
216
@pytest.mark.unit_test
217
@pytest.mark.parametrize("rule, result", [
218
    (
219
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
220
        (
221
            "Use of a 64-bit operating system offers a few advantages, "
222
            "like a larger address space range for\nAddress Space Layout"
223
            " Randomization (ASLR) and systematic presence of No eXecute"
224
            " and Execute Disable (NX/XD) protection bits."
225
        )
226
    ),
227
    (
228
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
229
        (
230
            "A session lock is a temporary action taken when a user stops work and"
231
            " moves away from the immediate physical vicinity\nof the information "
232
            "system but does not want to logout because of the temporary nature of the absense."
233
        )
234
    ),
235
    (
236
        "xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct",
237
        (
238
            "Email sent to the root account is typically aliased to the\n"
239
            "administrators of the system, who can take appropriate action."
240
        )
241
    ),
242
    (
243
        "xccdf_org.ssgproject.content_rule_sudoers_explicit_command_args",
244
        (
245
            "Any argument can modify quite significantly the behavior of a"
246
            " program, whether regarding the\nrealized operation (read, write, delete, etc.)"
247
            " or accessed resources (path in a file system tree). To\navoid any possibility of"
248
            " misuse of a command by a user, the ambiguities must be removed at the\nlevel of its"
249
            " specification.\n\nFor example, on some systems, the kernel messages are only "
250
            "accessible by root.\nIf a user nevertheless must have the privileges to read them,"
251
            " the argument of the dmesg command has to be restricted\nin order to prevent "
252
            "the user from flushing the buffer through the -c option:\n"
253
            "<pre>\nuser ALL = dmesg &quot;&quot;\n</pre>"
254
        )
255
    )
256
])
257
def test_rationale(rule, result):
258
    rules = get_rules()
259
    assert rules[rule].rationale == result
260
261
262
@pytest.mark.unit_test
263
@pytest.mark.parametrize("rule, result", [
264
    (
265
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
266
        ["There is no remediation besides installing a 64-bit operating system."]
267
    ),
268
    (
269
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
270
        []
271
    ),
272
    (
273
        "xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct",
274
        []
275
    ),
276
    (
277
        "xccdf_org.ssgproject.content_rule_sudoers_explicit_command_args",
278
        [
279
            (
280
                "This rule doesn&#x27;t come with a remediation, as absence of arguments in"
281
                " the user spec doesn&#x27;t mean that the command is intended to be executed "
282
                "with no arguments."
283
            ),
284
            (
285
                "The rule can produce false findings when an argument contains a"
286
                " comma - sudoers syntax allows comma escaping using backslash, but"
287
                " the check doesn&#x27;t support that. For example,"
288
                " <code>root ALL=(ALL) echo 1\\,2</code> allows root to execute"
289
                " <code>echo 1,2</code>, but the check would interpret it as two commands "
290
                "<code>echo 1\\</code> and <code>2</code>."
291
            )
292
        ]
293
    )
294
])
295
def test_warnings(rule, result):
296
    rules = get_rules()
297
    assert rules[rule].warnings == result
298
299
300
@pytest.mark.unit_test
301
@pytest.mark.parametrize("rule, remediation_id, scripts", [
302
    (
303
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
304
        None,
305
        {}
306
    ),
307
    (
308
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
309
        "dconf_gnome_screensaver_lock_enabled",
310
        {
311
            "urn:xccdf:fix:script:ansible": "dconf_gnome_screensaver_lock_enabled_ansible.txt",
312
            "urn:xccdf:fix:script:sh": "dconf_gnome_screensaver_lock_enabled_sh.txt"
313
        }
314
    ),
315
    (
316
        "xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct",
317
        "auditd_data_retention_action_mail_acct",
318
        {
319
            "urn:xccdf:fix:script:sh": "auditd_data_retention_action_mail_acct_sh.txt",
320
            "urn:xccdf:fix:script:ansible": "auditd_data_retention_action_mail_acct_ansible.txt"
321
        }
322
    )
323
])
324
def test_remediations(rule, remediation_id, scripts):
325
    rules = get_rules()
326
    if rules[rule].remediations == scripts is None:
327
        return
328
329
    for remediation in rules[rule].remediations:
330
        assert remediation.remediation_id == remediation_id
331
        assert remediation.system in scripts
332
        path = PATH_TO_REMEDIATIONS_SCRIPTS / str(scripts[remediation.system])
333
        with open(path, "r", encoding="utf-8") as script:
334
            data = script.read()
335
            assert data == remediation.fix
336