tests.unit_tests.test_scap_result_parser   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 391
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 259
dl 0
loc 391
rs 10
c 0
b 0
f 0
wmc 22

11 Functions

Rating   Name   Duplication   Size   Complexity  
B test_oval_definition_in_rules() 0 50 1
A test_rationale() 0 43 1
A test_warnings() 0 37 1
A test_description() 0 46 1
A test_validation() 0 20 1
A test_parse_report() 0 14 1
A test_get_profile_info() 0 27 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
A test_parsers_init() 0 16 3

3 Methods

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