Completed
Push — master ( d462aa...30f501 )
by
unknown
11s
created

org_fedora_oscap.common.extract_data()   F

Complexity

Conditions 14

Size

Total Lines 57
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 28
nop 3
dl 0
loc 57
rs 3.2833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like org_fedora_oscap.common.extract_data() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#
2
# Copyright (C) 2013  Red Hat, Inc.
3
#
4
# This copyrighted material is made available to anyone wishing to use,
5
# modify, copy, or redistribute it subject to the terms and conditions of
6
# the GNU General Public License v.2, or (at your option) any later version.
7
# This program is distributed in the hope that it will be useful, but WITHOUT
8
# ANY WARRANTY expressed or implied, including the implied warranties of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
10
# Public License for more details.  You should have received a copy of the
11
# GNU General Public License along with this program; if not, write to the
12
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
13
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
14
# source code or documentation are not subject to the GNU General Public
15
# License and may only be used or replicated with the express permission of
16
# Red Hat, Inc.
17
#
18
# Red Hat Author(s): Vratislav Podzimek <[email protected]>
19
#
20
21
"""
22
Module with various classes and functions needed by the OSCAP addon that are
23
not specific to any installation mode (tui, gui, ks).
24
25
"""
26
27
import os
28
import tempfile
29
import subprocess
30
import zipfile
31
import tarfile
32
from _ast import Sub
33
34
import cpioarchive
35
import re
36
import logging
37
38
from collections import namedtuple
39
import gettext
40
from functools import wraps
41
from pyanaconda.core import constants
42
from pyanaconda import nm
43
from pyanaconda.threading import threadMgr, AnacondaThread
44
from org_fedora_oscap import utils
45
from org_fedora_oscap.data_fetch import fetch_data
46
47
log = logging.getLogger("anaconda")
48
49
50
# mimick pyanaconda/core/i18n.py
51
def _(string):
52
    if string:
53
        return gettext.translation("oscap-anaconda-addon", fallback=True).gettext(string)
54
    else:
55
        return ""
56
57
58
def N_(string): return string
59
60
61
# everything else should be private
62
__all__ = ["run_oscap_remediate", "get_fix_rules_pre",
63
           "wait_and_fetch_net_data", "extract_data", "strip_content_dir",
64
           "OSCAPaddonError"]
65
66
INSTALLATION_CONTENT_DIR = "/tmp/openscap_data/"
67
TARGET_CONTENT_DIR = "/root/openscap_data/"
68
69
SSG_DIR = "/usr/share/xml/scap/ssg/content/"
70
SSG_CONTENT = "ssg-rhel7-ds.xml"
71
if constants.shortProductName != 'anaconda':
72
    if constants.shortProductName == 'fedora':
73
        SSG_CONTENT  = "ssg-fedora-ds.xml"
74
    else:
75
        SSG_CONTENT = "ssg-%s%s-ds.xml" % (constants.shortProductName,
76
                                            constants.productVersion.strip(".")[0])
77
78
RESULTS_PATH = utils.join_paths(TARGET_CONTENT_DIR,
79
                                "eval_remediate_results.xml")
80
REPORT_PATH = utils.join_paths(TARGET_CONTENT_DIR,
81
                               "eval_remediate_report.html")
82
83
PRE_INSTALL_FIX_SYSTEM_ATTR = "urn:redhat:anaconda:pre"
84
85
THREAD_FETCH_DATA = "AnaOSCAPdataFetchThread"
86
87
SUPPORTED_ARCHIVES = (".zip", ".tar", ".tar.gz", ".tar.bz2", )
88
89
# buffer size for reading and writing out data (in bytes)
90
IO_BUF_SIZE = 2 * 1024 * 1024
91
92
93
class OSCAPaddonError(Exception):
94
    """Exception class for OSCAP addon related errors."""
95
96
    pass
97
98
99
class OSCAPaddonNetworkError(OSCAPaddonError):
100
    """Exception class for OSCAP addon related network errors."""
101
102
    pass
103
104
105
class ExtractionError(OSCAPaddonError):
106
    """Exception class for the extraction errors."""
107
108
    pass
109
110
111
MESSAGE_TYPE_FATAL = 0
112
MESSAGE_TYPE_WARNING = 1
113
MESSAGE_TYPE_INFO = 2
114
115
# namedtuple for messages returned from the rules evaluation
116
#   origin -- class (inherited from RuleHandler) that generated the message
117
#   type -- one of the MESSAGE_TYPE_* constants defined above
118
#   text -- the actual message that should be displayed, logged, ...
119
RuleMessage = namedtuple("RuleMessage", ["origin", "type", "text"])
120
121
122
class SubprocessLauncher(object):
123
    def __init__(self, args):
124
        self.args = args
125
        self.stdout = ""
126
        self.stderr = ""
127
        self.messages = []
128
        self.returncode = None
129
130
    def execute(self, ** kwargs):
131
        try:
132
            proc = subprocess.Popen(self.args, stdout=subprocess.PIPE,
133
                                    stderr=subprocess.PIPE, ** kwargs)
134
        except OSError as oserr:
135
            msg = "Failed to run the oscap tool: %s" % oserr
136
            raise OSCAPaddonError(msg)
137
138
        (stdout, stderr) = proc.communicate()
139
        self.stdout = stdout.decode()
140
        self.stderr = stderr.decode()
141
        self.messages = re.findall(r'OpenSCAP Error:.*', self.stderr)
142
143
        self.returncode = proc.returncode
144
145
    def log_messages(self):
146
        for message in self.messages:
147
            log.warning("OSCAP addon: " + message)
148
149
150
def get_fix_rules_pre(profile, fpath, ds_id="", xccdf_id="", tailoring=""):
151
    """
152
    Get fix rules for the pre-installation environment for a given profile in a
153
    given datastream and checklist in a given file.
154
155
    :see: run_oscap_remediate
156
    :see: _run_oscap_gen_fix
157
    :return: fix rules for a given profile
158
    :rtype: str
159
160
    """
161
162
    return _run_oscap_gen_fix(profile, fpath, PRE_INSTALL_FIX_SYSTEM_ATTR,
163
                              ds_id=ds_id, xccdf_id=xccdf_id,
164
                              tailoring=tailoring)
165
166
167
def _run_oscap_gen_fix(profile, fpath, template, ds_id="", xccdf_id="",
168
                       tailoring=""):
169
    """
170
    Run oscap tool on a given file to get the contents of fix elements with the
171
    'system' attribute equal to a given template for a given datastream,
172
    checklist and profile.
173
174
    :see: run_oscap_remediate
175
    :param template: the value of the 'system' attribute of the fix elements
176
    :type template: str
177
    :return: oscap tool's stdout
178
    :rtype: str
179
180
    """
181
182
    if not profile:
183
        return ""
184
185
    args = ["oscap", "xccdf", "generate", "fix"]
186
    args.append("--template=%s" % template)
187
188
    # oscap uses the default profile by default
189
    if profile.lower() != "default":
190
        args.append("--profile=%s" % profile)
191
    if ds_id:
192
        args.append("--datastream-id=%s" % ds_id)
193
    if xccdf_id:
194
        args.append("--xccdf-id=%s" % xccdf_id)
195
    if tailoring:
196
        args.append("--tailoring-file=%s" % tailoring)
197
198
    args.append(fpath)
199
200
    proc = SubprocessLauncher(args)
201
    proc.execute()
202
    proc.log_messages()
203
    if proc.returncode != 0:
204
        msg = "Failed to generate fix rules with the oscap tool: %s" % proc.stderr
205
        raise OSCAPaddonError(msg)
206
207
    return proc.stdout
208
209
210
def run_oscap_remediate(profile, fpath, ds_id="", xccdf_id="", tailoring="",
211
                        chroot=""):
212
    """
213
    Run the evaluation and remediation with the oscap tool on a given file,
214
    doing the remediation as defined in a given profile defined in a given
215
    checklist that is a part of a given datastream. If requested, run in
216
    chroot.
217
218
    :param profile: id of the profile that will drive the remediation
219
    :type profile: str
220
    :param fpath: path to a file with SCAP content
221
    :type fpath: str
222
    :param ds_id: ID of the datastream that contains the checklist defining
223
                  the profile
224
    :type ds_id: str
225
    :param xccdf_id: ID of the checklist that defines the profile
226
    :type xccdf_id: str
227
    :param tailoring: path to a tailoring file
228
    :type tailoring: str
229
    :param chroot: path to the root the oscap tool should be run in
230
    :type chroot: str
231
    :return: oscap tool's stdout (summary of the rules, checks and fixes)
232
    :rtype: str
233
234
    """
235
236
    if not profile:
237
        return ""
238
239
    def do_chroot():
240
        """Helper function doing the chroot if requested."""
241
        if chroot and chroot != "/":
242
            os.chroot(chroot)
243
            os.chdir("/")
244
245
    # make sure the directory for the results exists
246
    results_dir = os.path.dirname(RESULTS_PATH)
247
    if chroot:
248
        results_dir = os.path.normpath(chroot + "/" + results_dir)
249
    utils.ensure_dir_exists(results_dir)
250
251
    args = ["oscap", "xccdf", "eval"]
252
    args.append("--remediate")
253
    args.append("--results=%s" % RESULTS_PATH)
254
    args.append("--report=%s" % REPORT_PATH)
255
256
    # oscap uses the default profile by default
257
    if profile.lower() != "default":
258
        args.append("--profile=%s" % profile)
259
    if ds_id:
260
        args.append("--datastream-id=%s" % ds_id)
261
    if xccdf_id:
262
        args.append("--xccdf-id=%s" % xccdf_id)
263
    if tailoring:
264
        args.append("--tailoring-file=%s" % tailoring)
265
266
    args.append(fpath)
267
268
    proc = SubprocessLauncher(args)
269
    proc.execute(preexec_fn=do_chroot)
270
    proc.log_messages()
271
272
    if proc.returncode not in (0, 2):
273
        # 0 -- success; 2 -- no error, but checks/remediation failed
274
        msg = "Content evaluation and remediation with the oscap tool "\
275
            "failed: %s" % proc.stderr
276
        raise OSCAPaddonError(msg)
277
278
    return proc.stdout
279
280
281
def wait_and_fetch_net_data(url, out_file, ca_certs=None):
282
    """
283
    Function that waits for network connection and starts a thread that fetches
284
    data over network.
285
286
    :see: org_fedora_oscap.data_fetch.fetch_data
287
    :return: the name of the thread running fetch_data
288
    :rtype: str
289
290
    """
291
292
    # get thread that tries to establish a network connection
293
    nm_conn_thread = threadMgr.get(constants.THREAD_WAIT_FOR_CONNECTING_NM)
294
    if nm_conn_thread:
295
        # NM still connecting, wait for it to finish
296
        nm_conn_thread.join()
297
298
    if not nm.nm_is_connected():
299
        raise OSCAPaddonNetworkError("Network connection needed to fetch data.")
300
301
    fetch_data_thread = AnacondaThread(name=THREAD_FETCH_DATA,
302
                                       target=fetch_data,
303
                                       args=(url, out_file, ca_certs),
304
                                       fatal=False)
305
306
    # register and run the thread
307
    threadMgr.add(fetch_data_thread)
308
309
    return THREAD_FETCH_DATA
310
311
312
def extract_data(archive, out_dir, ensure_has_files=None):
313
    """
314
    Fuction that extracts the given archive to the given output directory. It
315
    tries to find out the archive type by the file name.
316
317
    :param archive: path to the archive file that should be extracted
318
    :type archive: str
319
    :param out_dir: output directory the archive should be extracted to
320
    :type out_dir: str
321
    :param ensure_has_files: relative paths to the files that must exist in the
322
                             archive
323
    :type ensure_has_files: iterable of strings or None
324
    :return: a list of files and directories extracted from the archive
325
    :rtype: [str]
326
327
    """
328
329
    # get rid of empty file paths
330
    ensure_has_files = [fpath for fpath in ensure_has_files if fpath]
331
332
    if archive.endswith(".zip"):
333
        # ZIP file
334
        try:
335
            zfile = zipfile.ZipFile(archive, "r")
336
        except zipfile.BadZipfile as err:
337
            raise ExtractionError(str(err))
338
339
        # generator for the paths of the files found in the archive (dirs end
340
        # with "/")
341
        files = set(info.filename for info in zfile.filelist
342
                    if not info.filename.endswith("/"))
343
        for fpath in ensure_has_files or ():
344
            if fpath not in files:
345
                msg = "File '%s' not found in the archive '%s'" % (fpath,
346
                                                                   archive)
347
                raise ExtractionError(msg)
348
349
        utils.ensure_dir_exists(out_dir)
350
        zfile.extractall(path=out_dir)
351
        result = [utils.join_paths(out_dir, info.filename) for info in zfile.filelist]
352
        zfile.close()
353
        return result
354
    elif archive.endswith(".tar"):
355
        # plain tarball
356
        return _extract_tarball(archive, out_dir, ensure_has_files, None)
357
    elif archive.endswith(".tar.gz"):
358
        # gzipped tarball
359
        return _extract_tarball(archive, out_dir, ensure_has_files, "gz")
360
    elif archive.endswith(".tar.bz2"):
361
        # bzipped tarball
362
        return _extract_tarball(archive, out_dir, ensure_has_files, "bz2")
363
    elif archive.endswith(".rpm"):
364
        # RPM
365
        return _extract_rpm(archive, out_dir, ensure_has_files)
366
    # elif other types of archives
367
    else:
368
        raise ExtractionError("Unsuported archive type")
369
370
371
def _extract_tarball(archive, out_dir, ensure_has_files, alg):
372
    """
373
    Extract the given TAR archive to the given output directory and make sure
374
    the given file exists in the archive.
375
376
    :see: extract_data
377
    :param alg: compression algorithm used for the tarball
378
    :type alg: str (one of "gz", "bz2") or None
379
    :return: a list of files and directories extracted from the archive
380
    :rtype: [str]
381
382
    """
383
384
    if alg and alg not in ("gz", "bz2",):
385
        raise ExtractionError("Unsupported compression algorithm")
386
387
    mode = "r"
388
    if alg:
389
        mode += ":%s" % alg
390
391
    try:
392
        tfile = tarfile.TarFile.open(archive, mode)
393
    except tarfile.TarError as err:
394
        raise ExtractionError(str(err))
395
396
    # generator for the paths of the files found in the archive
397
    files = set(member.path for member in tfile.getmembers()
398
                if member.isfile())
399
400
    for fpath in ensure_has_files or ():
401
        if fpath not in files:
402
            msg = "File '%s' not found in the archive '%s'" % (fpath, archive)
403
            raise ExtractionError(msg)
404
405
    utils.ensure_dir_exists(out_dir)
406
    tfile.extractall(path=out_dir)
407
    result = [utils.join_paths(out_dir, member.path) for member in tfile.getmembers()]
408
    tfile.close()
409
410
    return result
411
412
413
def _extract_rpm(rpm_path, root="/", ensure_has_files=None):
414
    """
415
    Extract the given RPM into the directory tree given by the root argument
416
    and make sure the given file exists in the archive.
417
418
    :param rpm_path: path to the RPM file that should be extracted
419
    :type rpm_path: str
420
    :param root: root of the directory tree the RPM should be extracted into
421
    :type root: str
422
    :param ensure_has_files: relative paths to the files that must exist in the
423
                             RPM
424
    :type ensure_has_files: iterable of strings or None
425
    :return: a list of files and directories extracted from the archive
426
    :rtype: [str]
427
428
    """
429
430
    # run rpm2cpio and process the output with the cpioarchive module
431
    temp_fd, temp_path = tempfile.mkstemp(prefix="oscap_rpm")
432
    proc = subprocess.Popen(["rpm2cpio", rpm_path], stdout=temp_fd)
433
    proc.wait()
434
    if proc.returncode != 0:
435
        msg = "Failed to convert RPM '%s' to cpio archive" % rpm_path
436
        raise ExtractionError(msg)
437
438
    os.close(temp_fd)
439
440
    try:
441
        archive = cpioarchive.CpioArchive(temp_path)
442
    except cpioarchive.CpioError as err:
443
        raise ExtractionError(str(err))
444
445
    # get entries from the archive (supports only iteration over entries)
446
    entries = set(entry for entry in archive)
447
448
    # cpio entry names (paths) start with the dot
449
    entry_names = [entry.name.lstrip(".") for entry in entries]
450
451
    for fpath in ensure_has_files or ():
452
        # RPM->cpio entries have absolute paths
453
        if fpath not in entry_names and \
454
           os.path.join("/", fpath) not in entry_names:
455
            msg = "File '%s' not found in the archive '%s'" % (fpath, rpm_path)
456
            raise ExtractionError(msg)
457
458
    try:
459
        for entry in entries:
460
            dirname = os.path.dirname(entry.name.lstrip("."))
461
            out_dir = os.path.normpath(root + dirname)
462
            utils.ensure_dir_exists(out_dir)
463
464
            out_fpath = os.path.normpath(root + entry.name.lstrip("."))
465
            if os.path.exists(out_fpath):
466
                continue
467
            with open(out_fpath, "wb") as out_file:
468
                buf = entry.read(IO_BUF_SIZE)
469
                while buf:
470
                    out_file.write(buf)
471
                    buf = entry.read(IO_BUF_SIZE)
472
    except (IOError, cpioarchive.CpioError) as e:
473
        raise ExtractionError(e)
474
475
    # cleanup
476
    archive.close()
477
    os.unlink(temp_path)
478
479
    return [os.path.normpath(root + name) for name in entry_names]
480
481
482
def strip_content_dir(fpaths, phase="preinst"):
483
    """
484
    Strip content directory prefix from the file paths for either
485
    pre-installation or post-installation phase.
486
487
    :param fpaths: iterable of file paths to strip content directory prefix
488
                   from
489
    :type fpaths: iterable of strings
490
    :param phase: specifies pre-installation or post-installation phase
491
    :type phase: "preinst" or "postinst"
492
    :return: the same iterable of file paths as given with the content
493
             directory prefix stripped
494
    :rtype: same type as fpaths
495
496
    """
497
498
    if phase == "preinst":
499
        remove_prefix = lambda x: x[len(INSTALLATION_CONTENT_DIR):]
500
    else:
501
        remove_prefix = lambda x: x[len(TARGET_CONTENT_DIR):]
502
503
    return utils.keep_type_map(remove_prefix, fpaths)
504
505
506
def ssg_available(root="/"):
507
    """
508
    Tries to find the SCAP Security Guide under the given root.
509
510
    :return: True if SSG was found under the given root, False otherwise
511
512
    """
513
514
    return os.path.exists(utils.join_paths(root, SSG_DIR + SSG_CONTENT))
515
516
517
def dry_run_skip(func):
518
    """
519
    Decorator that makes sure the decorated function is noop in the dry-run
520
    mode.
521
522
    :param func: a decorated function that needs to have the first parameter an
523
                 object with the _addon_data attribute referencing the OSCAP
524
                 addon's ksdata
525
    """
526
527
    @wraps(func)
528
    def decorated(self, *args, **kwargs):
529
        if self._addon_data.dry_run:
530
            return
531
        else:
532
            return func(self, *args, **kwargs)
533
534
    return decorated
535