Passed
Push — main ( 5a7e80...db113e )
by Jan
06:22 queued 14s
created

tests.unit_tests.test_scap_result_parser   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 258
dl 0
loc 399
rs 10
c 0
b 0
f 0
wmc 25

16 Functions

Rating   Name   Duplication   Size   Complexity  
A get_parser() 0 5 2
A get_root() 0 5 2
A get_benchmark() 0 5 2
B test_oval_definition_in_rules() 0 50 1
A test_rationale() 0 43 1
A test_warnings() 0 35 1
A test_description() 0 46 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 27 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 35 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_benchmark(root):
45
    benchmark_el = root.find(".//xccdf:Benchmark", NAMESPACES)
46
    if "Benchmark" in root.tag:
47
        benchmark_el = root
48
    return benchmark_el
49
50
51
def get_test_results(root):
52
    return root.find('.//xccdf:TestResult', NAMESPACES)
53
54
55
def get_ref_values(root):
56
    return {
57
        ref_value.get("idref"): ref_value.text
58
        for ref_value in root.findall('.//xccdf:set-value', NAMESPACES)
59
    }
60
61
62
def get_rules(file_path=PATH_TO_ARF):
63
    root = get_root(file_path)
64
    test_results = get_test_results(root)
65
    ref_values = get_ref_values(root)
66
    rule_parser = RuleParser(root, test_results, ref_values)
67
    return rule_parser.get_rules()
68
69
70
DEFAULT_RULES = get_rules()
71
DEFAULT_REPORT = get_parser(PATH_TO_ARF).parse_report()
72
73
74
@pytest.mark.unit_test
75
@pytest.mark.parametrize("file_path, result", [
76
    (PATH_TO_ARF, True),
77
    (PATH_TO_SIMPLE_RULE_PASS_ARF, True),
78
    (PATH_TO_SIMPLE_RULE_FAIL_ARF, True),
79
    (PATH_TO_RULE_AND_CPE_CHECK_ARF, True),
80
    (PATH_TO_ARF_WITHOUT_INFO, True),
81
    (PATH_TO_ARF_WITHOUT_SYSTEM_DATA, True),
82
    (PATH_TO_ARF_SCANNED_ON_CONTAINER, True),
83
    (PATH_TO_ARF_WITH_OS_CPE_CHECK, True),
84
    (PATH_TO_XCCDF, False),
85
    (PATH_TO_SIMPLE_RULE_PASS_XCCDF, False),
86
    (PATH_TO_SIMPLE_RULE_FAIL_XCCDF, False),
87
    (PATH_TO_RULE_AND_CPE_CHECK_XCCDF, False),
88
    (PATH_TO_XCCDF_WITHOUT_INFO, False),
89
    (PATH_TO_XCCDF_WITHOUT_SYSTEM_DATA, False),
90
    (PATH_TO_EMPTY_XML_FILE, False),
91
])
92
def test_validation(file_path, result):
93
    parser = get_parser(file_path)
94
    assert parser.validate(parser.arf_schemas_path) == result
95
96
97
@pytest.mark.unit_test
98
@pytest.mark.parametrize("file_path, number_of_cpe_platforms, os_cpe_platform", [
99
    (PATH_TO_ARF, 13, {"cpe:/o:fedoraproject:fedora:32": True}),
100
    (PATH_TO_XCCDF, 13, {"cpe:/o:fedoraproject:fedora:32": True}),
101
    (PATH_TO_SIMPLE_RULE_PASS_ARF, 0, {}),
102
    (PATH_TO_SIMPLE_RULE_FAIL_ARF, 0, {}),
103
    (PATH_TO_ARF_WITHOUT_INFO, 0, {}),
104
    (PATH_TO_ARF_WITHOUT_SYSTEM_DATA, 0, {}),
105
    (PATH_TO_ARF_SCANNED_ON_CONTAINER, 6, {
106
        'cpe:/o:fedoraproject:fedora:35': True,
107
        'cpe:/o:fedoraproject:fedora:34': True,
108
        'cpe:/o:fedoraproject:fedora:33': True
109
    }),
110
    (PATH_TO_RULE_AND_CPE_CHECK_ARF, 1, {}),
111
    (PATH_TO_ARF_WITH_OS_CPE_CHECK, 0, {"cpe:/o:fedoraproject:fedora:1": False}),
112
    (PATH_TO_SIMPLE_RULE_PASS_XCCDF, 0, {}),
113
    (PATH_TO_SIMPLE_RULE_FAIL_XCCDF, 0, {}),
114
    (PATH_TO_XCCDF_WITHOUT_INFO, 0, {}),
115
    (PATH_TO_XCCDF_WITHOUT_SYSTEM_DATA, 0, {}),
116
    (PATH_TO_RULE_AND_CPE_CHECK_XCCDF, 1, {}),
117
])
118
def test_get_profile_info(file_path, number_of_cpe_platforms, os_cpe_platform):
119
    root = get_root(file_path)
120
    report_parser = ReportParser(root, get_test_results(root), get_benchmark(root))
121
    report = report_parser.get_report()
122
    assert len(report.scan_result.cpe_platforms) == number_of_cpe_platforms
123
    assert report.profile_info.cpe_platforms_for_profile == os_cpe_platform
124
125
126
@pytest.mark.unit_test
127
@pytest.mark.parametrize("file_path, number_of_rules", [
128
    (PATH_TO_ARF, 714),
129
    (PATH_TO_XCCDF, 714),
130
    (PATH_TO_ARF_SCANNED_ON_CONTAINER, 121),
131
    (PATH_TO_SIMPLE_RULE_PASS_ARF, 1),
132
    (PATH_TO_SIMPLE_RULE_FAIL_ARF, 1),
133
    (PATH_TO_ARF_WITHOUT_INFO, 1),
134
    (PATH_TO_ARF_WITHOUT_SYSTEM_DATA, 1),
135
    (PATH_TO_ARF_WITH_OS_CPE_CHECK, 1),
136
    (PATH_TO_RULE_AND_CPE_CHECK_ARF, 3),
137
    (PATH_TO_SIMPLE_RULE_PASS_XCCDF, 1),
138
    (PATH_TO_SIMPLE_RULE_FAIL_XCCDF, 1),
139
    (PATH_TO_XCCDF_WITHOUT_INFO, 1),
140
    (PATH_TO_XCCDF_WITHOUT_SYSTEM_DATA, 1),
141
    (PATH_TO_RULE_AND_CPE_CHECK_XCCDF, 3),
142
])
143
def test_get_info_about_rules_in_profile(file_path, number_of_rules):
144
    rules = get_rules(file_path)
145
    assert len(rules.keys()) == number_of_rules
146
    for rule in rules.values():
147
        assert isinstance(rule, Rule)
148
149
150
@pytest.mark.unit_test
151
@pytest.mark.parametrize("file_path, contains_oval_tree", [
152
    (PATH_TO_ARF, True),
153
    (PATH_TO_XCCDF, False),
154
])
155
def test_parse_report(file_path, contains_oval_tree):
156
    parser = get_parser(file_path)
157
    report = parser.parse_report()
158
    assert isinstance(report, Report)
159
    assert report.profile_info.profile_id is not None
160
    assert report.rules is not None
161
    rule_id = "xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny"
162
    assert isinstance(report.rules[rule_id], Rule)
163
    assert (report.rules[rule_id].oval_definition is not None) == contains_oval_tree
164
165
166
@pytest.mark.unit_test
167
@pytest.mark.parametrize("file_path, contains_rules_some_multi_check_rule", [
168
    (PATH_TO_ARF, False),
169
    (PATH_TO_XCCDF, False),
170
    (PATH_TO_XCCDF_WITH_MULTI_CHECK, True),
171
    (PATH_TO_ARF_WITH_MULTI_CHECK, True),
172
])
173
def test_multi_check(file_path, contains_rules_some_multi_check_rule):
174
    rules = get_rules(file_path)
175
    result = False
176
    for rule in rules.values():
177
        if rule.multi_check:
178
            result = True
179
    assert result == contains_rules_some_multi_check_rule
180
181
182
@pytest.mark.unit_test
183
@pytest.mark.parametrize("rule, result", [
184
    (
185
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
186
        "Prefer installation of 64-bit operating systems when the CPU supports it."
187
    ),
188
    (
189
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
190
        (
191
            "\nTo activate locking of the screensaver in the GNOME3 desktop"
192
            " when it is activated,\nadd or set <code>lock-enabled</code>"
193
            " to <code>true</code> in\n<code>/etc/dconf/db/local.d/00-security-settings</code>."
194
            " For example:\n<pre>[org/gnome/desktop/screensaver]\nlock-enabled=true\n</pre>\n"
195
            "Once the settings have been added, add a lock to\n"
196
            "<code>/etc/dconf/db/local.d/locks/00-security-settings-lock</code> "
197
            "to prevent user modification.\nFor example:\n"
198
            "<pre>/org/gnome/desktop/screensaver/lock-enabled</pre>\n"
199
            "After the settings have been set, run <code>dconf update</code>."
200
        )
201
    ),
202
    (
203
        "xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct",
204
        (
205
            "The <code>auditd</code> service can be configured to send email to\n"
206
            "a designated account in certain situations. Add or correct the following line\n"
207
            "in <code>/etc/audit/auditd.conf</code> to ensure that administrators are notified\n"
208
            "via email for those situations:\n<pre>action_mail_acct = root</pre>"
209
        )
210
    ),
211
    (
212
        "xccdf_org.ssgproject.content_rule_chronyd_specify_remote_server",
213
        (
214
            "<code>Chrony</code> is a daemon which implements"
215
            " the Network Time Protocol (NTP). It is designed to\n"
216
            "synchronize system clocks across a variety of systems and"
217
            " use a source that is highly\naccurate. More information on"
218
            " <code>chrony</code> can be found at\n\n    "
219
            "<a href=\"http://chrony.tuxfamily.org/\">http://chrony.tuxfamily.org/</a>.\n"
220
            "<code>Chrony</code> can be configured to be a client and/or a server.\n"
221
            "Add or edit server or pool lines to <code>/etc/chrony.conf</code> as appropriate:\n"
222
            "<pre>server &lt;remote-server&gt;</pre>\nMultiple servers may be configured."
223
        )
224
    ),
225
])
226
def test_description(rule, result):
227
    assert DEFAULT_RULES[rule].description == result
228
229
230
@pytest.mark.unit_test
231
@pytest.mark.parametrize("rule, result", [
232
    (
233
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
234
        (
235
            "Use of a 64-bit operating system offers a few advantages, "
236
            "like a larger address space range for\nAddress Space Layout"
237
            " Randomization (ASLR) and systematic presence of No eXecute"
238
            " and Execute Disable (NX/XD) protection bits."
239
        )
240
    ),
241
    (
242
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
243
        (
244
            "A session lock is a temporary action taken when a user stops work and"
245
            " moves away from the immediate physical vicinity\nof the information "
246
            "system but does not want to logout because of the temporary nature of the absense."
247
        )
248
    ),
249
    (
250
        "xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct",
251
        (
252
            "Email sent to the root account is typically aliased to the\n"
253
            "administrators of the system, who can take appropriate action."
254
        )
255
    ),
256
    (
257
        "xccdf_org.ssgproject.content_rule_sudoers_explicit_command_args",
258
        (
259
            "Any argument can modify quite significantly the behavior of a"
260
            " program, whether regarding the\nrealized operation (read, write, delete, etc.)"
261
            " or accessed resources (path in a file system tree). To\navoid any possibility of"
262
            " misuse of a command by a user, the ambiguities must be removed at the\nlevel of its"
263
            " specification.\n\nFor example, on some systems, the kernel messages are only "
264
            "accessible by root.\nIf a user nevertheless must have the privileges to read them,"
265
            " the argument of the dmesg command has to be restricted\nin order to prevent "
266
            "the user from flushing the buffer through the -c option:\n"
267
            "<pre>\nuser ALL = dmesg &quot;&quot;\n</pre>"
268
        )
269
    )
270
])
271
def test_rationale(rule, result):
272
    assert DEFAULT_RULES[rule].rationale == result
273
274
275
@pytest.mark.unit_test
276
@pytest.mark.parametrize("rule, result", [
277
    (
278
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
279
        ["There is no remediation besides installing a 64-bit operating system."]
280
    ),
281
    (
282
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
283
        []
284
    ),
285
    (
286
        "xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct",
287
        []
288
    ),
289
    (
290
        "xccdf_org.ssgproject.content_rule_sudoers_explicit_command_args",
291
        [
292
            (
293
                "This rule doesn&#x27;t come with a remediation, as absence of arguments in"
294
                " the user spec doesn&#x27;t mean that the command is intended to be executed "
295
                "with no arguments."
296
            ),
297
            (
298
                "The rule can produce false findings when an argument contains a"
299
                " comma - sudoers syntax allows comma escaping using backslash, but"
300
                " the check doesn&#x27;t support that. For example,"
301
                " <code>root ALL=(ALL) echo 1\\,2</code> allows root to execute"
302
                " <code>echo 1,2</code>, but the check would interpret it as two commands "
303
                "<code>echo 1\\</code> and <code>2</code>."
304
            )
305
        ]
306
    )
307
])
308
def test_warnings(rule, result):
309
    assert DEFAULT_RULES[rule].warnings == result
310
311
312
@pytest.mark.unit_test
313
@pytest.mark.parametrize("rule, remediation_id, scripts", [
314
    (
315
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
316
        None,
317
        {}
318
    ),
319
    (
320
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
321
        "dconf_gnome_screensaver_lock_enabled",
322
        {
323
            "urn:xccdf:fix:script:ansible": "dconf_gnome_screensaver_lock_enabled_ansible.txt",
324
            "urn:xccdf:fix:script:sh": "dconf_gnome_screensaver_lock_enabled_sh.txt"
325
        }
326
    ),
327
    (
328
        "xccdf_org.ssgproject.content_rule_auditd_data_retention_action_mail_acct",
329
        "auditd_data_retention_action_mail_acct",
330
        {
331
            "urn:xccdf:fix:script:sh": "auditd_data_retention_action_mail_acct_sh.txt",
332
            "urn:xccdf:fix:script:ansible": "auditd_data_retention_action_mail_acct_ansible.txt"
333
        }
334
    )
335
])
336
def test_remediations(rule, remediation_id, scripts):
337
    if DEFAULT_RULES[rule].remediations == scripts is None:
338
        return
339
340
    for remediation in DEFAULT_RULES[rule].remediations:
341
        assert remediation.remediation_id == remediation_id
342
        assert remediation.system in scripts
343
        path = PATH_TO_REMEDIATIONS_SCRIPTS / str(scripts[remediation.system])
344
        with open(path, "r", encoding="utf-8") as script:
345
            data = script.read()
346
            assert data == remediation.fix
347
348
349
@pytest.mark.unit_test
350
@pytest.mark.parametrize("rule, oval_def_id, title, description, version", [
351
    (
352
        "xccdf_org.ssgproject.content_rule_prefer_64bit_os",
353
        "oval:ssg-prefer_64bit_os:def:1",
354
        "Prefer to use a 64-bit Operating System when supported",
355
        "Check if the system supports a 64-bit Operating System",
356
        "1"
357
    ),
358
    (
359
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled",
360
        "oval:ssg-dconf_gnome_screensaver_lock_enabled:def:1",
361
        "Enable GNOME3 Screensaver Lock After Idle Period",
362
        "Idle activation of the screen lock should be enabled.",
363
        "2",
364
    ),
365
    (
366
        "xccdf_org.ssgproject.content_rule_installed_OS_is_FIPS_certified",
367
        "oval:ssg-installed_OS_is_FIPS_certified:def:1",
368
        "The Installed Operating System Is FIPS 140-2 Certified",
369
        (
370
            "\n          The operating system installed on the system is"
371
            " a certified operating system that meets FIPS 140-2 requirements.\n      "
372
        ),
373
        "1",
374
    ),
375
    (
376
        "xccdf_org.ssgproject.content_rule_dconf_db_up_to_date",
377
        "oval:ssg-dconf_db_up_to_date:def:1",
378
        "Make sure that the dconf databases are up-to-date with regards to respective keyfiles",
379
        "Make sure that the dconf databases are up-to-date with regards to respective keyfiles.",
380
        "2",
381
    ),
382
    (
383
        "xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_delay",
384
        "oval:ssg-dconf_gnome_screensaver_lock_delay:def:1",
385
        "Set GNOME3 Screensaver Lock Delay After Activation Period",
386
        (
387
            "Idle activation of the screen lock should be enabled immediately or"
388
            "\n      after a delay."
389
        ),
390
        "2",
391
    )
392
])
393
def test_oval_definition_in_rules(rule, oval_def_id, title, description, version):
394
    oval_definition = DEFAULT_REPORT.rules[rule].oval_definition
395
    assert oval_definition.definition_id == oval_def_id
396
    assert oval_definition.title == title
397
    assert oval_definition.version == version
398
    assert oval_definition.description == description
399