ComplianceAsCode/content

View on GitHub
ssg/oval.py

Summary

Maintainability
Test Coverage
"""
Common functions for processing OVAL in SSG
"""

from __future__ import absolute_import
from __future__ import print_function

import sys
import os

from .constants import oval_footer as footer
from .constants import oval_namespace as ovalns
from .xml import ElementTree as ET
from .xml import oval_generated_header

from .jinja import process_file_with_macros
from .products import _get_implied_properties


ASSUMED_OVAL_VERSION_STRING = "5.11"
# globals, to make recursion easier in case we encounter extend_definition
try:
    ET.register_namespace("oval", ovalns)
except AttributeError:
    # Legacy Python 2.6 fix, see e.g.
    # https://www.programcreek.com/python/example/57552/xml.etree.ElementTree._namespace_map
    from xml.etree import ElementTree as ET
    ET._namespace_map[ovalns] = "oval"


def applicable_platforms(oval_file, oval_version_string=None):
    """
    Returns the applicable platforms for a given OVAL file.

    This function processes an OVAL file to extract the platforms it applies to. It uses a
    specified OVAL version string or a default version if none is provided. The function
    constructs an XML tree from the OVAL file and extracts platform information from it.

    Args:
        oval_file (str): The path to the OVAL file to be processed.
        oval_version_string (str, optional): The OVAL version string to be used.
                                             If not provided, a default version is used.

    Returns:
        list: A list of platforms that the OVAL file applies to.

    Raises:
        Exception: If there is an error while parsing the OVAL file.
    """
    platforms = []

    if not oval_version_string:
        oval_version_string = ASSUMED_OVAL_VERSION_STRING
    header = oval_generated_header("applicable_platforms", oval_version_string, "0.0.1")

    oval_version_list = [int(num) for num in oval_version_string.split(".")]
    subst_dict = dict(target_oval_version=oval_version_list)

    oval_filename_components = oval_file.split(os.path.sep)
    if len(oval_filename_components) > 3:
        subst_dict["rule_id"] = oval_filename_components[-3]
    else:
        msg = "Unable to get rule ID from OVAL path '{path}'".format(path=oval_file)
        print(msg, file=sys.stderr)

    subst_dict = _get_implied_properties(subst_dict)
    subst_dict['target_oval_version'] = [999, 999.999]

    body = process_file_with_macros(oval_file, subst_dict)

    try:
        oval_tree = ET.fromstring(header + body + footer)
    except Exception as e:
        msg = "Error while loading " + oval_file
        print(msg, file=sys.stderr)
        raise e

    element_path = "./{%s}def-group/{%s}definition/{%s}metadata/{%s}affected/{%s}platform"
    element_ns_path = element_path % (ovalns, ovalns, ovalns, ovalns, ovalns)
    for node in oval_tree.findall(element_ns_path):
        platforms.append(node.text)

    return platforms


def parse_affected(oval_contents):
    """
    Returns the tuple (start_affected, end_affected, platform_indents) for the passed OVAL file contents.

    Args:
    oval_contents (list of str): The contents of the OVAL file, where each element is a line from
                                 the file.

    Returns:
        tuple: A tuple containing:
            - start_affected (int): The line number of the starting <affected> tag.
            - end_affected (int): The line number of the closing </affected> tag.
            - platform_indents (str): The indenting characters before the contents of the
              <affected> element.

    Raises:
        ValueError: If the OVAL file does not contain a single <affected> element, if the start
                    tag is after the end tag, or if the tags contain other elements.
    """
    start_affected = list(filter(lambda x: "<affected" in oval_contents[x],
                                 range(0, len(oval_contents))))
    if len(start_affected) != 1:
        raise ValueError("OVAL file does not contain a single <affected> "
                         "element; counted %d in:\n%s\n\n" %
                         (len(start_affected), "\n".join(oval_contents)))

    start_affected = start_affected[0]

    end_affected = list(filter(lambda x: "</affected" in oval_contents[x],
                               range(0, len(oval_contents))))
    if len(end_affected) != 1:
        raise ValueError("Malformed OVAL file does not contain a single "
                         "closing </affected>; counted %d in:\n%s\n\n" %
                         (len(start_affected), "\n".join(oval_contents)))
    end_affected = end_affected[0]

    if start_affected >= end_affected:
        raise ValueError("Malformed OVAL file: start affected tag begins "
                         "on the same line or after ending affected tag: "
                         "start:%d vs end:%d:\n%s\n\n" %
                         (start_affected, end_affected, oval_contents))

    # Validate that start_affected contains only a starting <affected> tag;
    # otherwise, using this information to update the <platform> subelements
    # would fail.
    start_line = oval_contents[start_affected]
    start_line = start_line.strip()

    if not start_line.startswith('<affected'):
        raise ValueError("Malformed OVAL file: line with starting affected "
                         "tag contains other elements: line:%s\n%s\n\n" %
                         (start_line, oval_contents))
    if '<' in start_line[1:]:
        raise ValueError("Malformed OVAL file: line with starting affected "
                         "tag contains other elements: line:%s\n%s\n\n" %
                         (start_line, oval_contents))

    # Validate that end_affected contains only an ending </affected> tag;
    # otherwise, using this information to update the <platform> subelements
    # would fail.
    end_line = oval_contents[end_affected]
    end_line = end_line.strip()

    if not end_line.startswith('</affected>'):
        raise ValueError("Malformed OVAL file: line with ending affected "
                         "tag contains other elements: line:%s\n%s\n\n" %
                         (end_line, oval_contents))
    if '<' in end_line[1:]:
        raise ValueError("Malformed OVAL file: line with ending affected "
                         "tag contains other elements: line:%s\n%s\n\n" %
                         (end_line, oval_contents))

    indents = ""
    if start_affected+1 == end_affected:
        # Since the affected element is present but empty, the indents should
        # be two more spaces than that of the starting <affected> element.
        start_index = oval_contents[start_affected].index('<')
        indents = oval_contents[start_affected][0:start_index]
        indents += "  "
    else:
        # Otherwise, grab the indents off the next line unmodified, as this is
        # likely a platform element tag. We don't validate here that this is
        # indeed the case, as other parts of the build infrastructure will
        # validate this for us.
        start_index = oval_contents[start_affected+1].index('<')
        indents = oval_contents[start_affected+1][0:start_index]

    return start_affected, end_affected, indents