def to_xml_element(self): el = ET.Element('Profile') el.set("id", self.id_) if self.extends: el.set("extends", self.extends) title = add_sub_element(el, "title", self.title) title.set("override", "true") desc = add_sub_element(el, "description", self.description) desc.set("override", "true") for selection in self.selections: if selection.startswith("!"): unselect = ET.Element("select") unselect.set("idref", selection[1:]) unselect.set("selected", "false") el.append(unselect) elif "=" in selection: refine_value = ET.Element("refine-value") value_id, selector = selection.split("=", 1) refine_value.set("idref", value_id) refine_value.set("selector", selector) el.append(refine_value) else: select = ET.Element("select") select.set("idref", selection) select.set("selected", "true") el.append(select) return el
def add_platforms(xml_tree, multi_platform): for affected in xml_tree.findall(".//{%s}affected" % oval_ns): if affected.get("family") != "unix": continue for plat_elem in affected: try: if plat_elem.text == 'multi_platform_all': for platforms in multi_platform[plat_elem.text]: for plat in multi_platform[platforms]: platform = ElementTree.Element( "{%s}platform" % oval_ns) platform.text = map_name(platforms) + ' ' + plat affected.insert(1, platform) else: for platforms in multi_platform[plat_elem.text]: platform = ElementTree.Element("{%s}platform" % oval_ns) platform.text = map_name(plat_elem.text) + ' ' + platforms affected.insert(0, platform) except KeyError: pass # Remove multi_platform element if re.findall('multi_platform', plat_elem.text): affected.remove(plat_elem) return xml_tree
def add_cpes(elem, namespace, mapping): """ Adds derivative CPEs next to RHEL ones, checks XCCDF elements of given namespace. """ affected = False for child in list(elem): affected = affected or add_cpes(child, namespace, mapping) # precompute this so that we can affect the tree while iterating children = list(elem.findall(".//{%s}platform" % (namespace))) for child in children: idref = child.get("idref") if idref in mapping: new_platform = ElementTree.Element("{%s}platform" % (namespace)) new_platform.set("idref", mapping[idref]) # this is done for the newline and indentation new_platform.tail = child.tail index = list(elem).index(child) # insert it right after the respective RHEL CPE elem.insert(index + 1, new_platform) affected = True return affected
def add_notice(benchmark, namespace, notice, warning): """ Adds derivative notice as the first notice to given benchmark. """ index = -1 prev_element = None existing_notices = list(benchmark.findall("./{%s}notice" % (namespace))) if len(existing_notices) > 0: prev_element = existing_notices[0] # insert before the first notice index = list(benchmark).index(prev_element) else: existing_descriptions = list( benchmark.findall("./{%s}description" % (namespace))) prev_element = existing_descriptions[-1] # insert after the last description index = list(benchmark).index(prev_element) + 1 if index == -1: raise RuntimeError( "Can't find existing notices or description in benchmark '%s'." % (benchmark)) elem = ElementTree.Element("{%s}notice" % (namespace)) elem.set("id", warning) elem.append(notice) # this is done for the newline and indentation elem.tail = prev_element.tail benchmark.insert(index, elem) return True
def replace_external_vars(tree): """Replace external_variables with local_variables, so the definition can be tested independently of an XCCDF file""" # external_variable is a special case: we turn it into a local_variable so # we can test for node in tree.findall(".//{%s}external_variable" % ovalns): print("External_variable with id : " + node.get("id")) extvar_id = node.get("id") # for envkey, envval in os.environ.iteritems(): # print envkey + " = " + envval # sys.exit() if extvar_id not in os.environ.keys(): print( "External_variable specified, but no value provided via " "environment variable", file=sys.stderr) sys.exit(2) # replace tag name: external -> local node.tag = "{%s}local_variable" % ovalns literal = ET.Element("literal_component") literal.text = os.environ[extvar_id] node.append(literal) # TODO: assignment of external_variable via environment vars, for # testing return tree
def _add_cce_id_refs_to_oval_checks(self, idmappingdict): """ For each XCCDF rule ID having <ident> CCE set and having OVAL check implemented (remote OVAL isn't sufficient!) add a new <reference> element into the OVAL definition having the following form: <reference source="CCE" ref_id="CCE-ID" /> where "CCE-ID" is the CCE identifier for that particular rule retrieved from the XCCDF file """ ovalrules = self.tree.findall(".//{0}".format(self._get_checkid_string())) for rule in ovalrules: ovalid = rule.get("id") assert ovalid is not None, \ "An OVAL rule doesn't have an ID" if ovalid not in idmappingdict: continue ovaldesc = rule.find(".//{%s}description" % self.CHECK_NAMESPACE) assert ovaldesc is not None, \ "OVAL rule '{0}' doesn't have a description, which is mandatory".format(ovalid) xccdfcceid = idmappingdict[ovalid] if is_cce_valid(xccdfcceid): # Then append the <reference source="CCE" ref_id="CCE-ID" /> element right # after <description> element of specific OVAL check ccerefelem = ET.Element('reference', ref_id=xccdfcceid, source="CCE") metadata = rule.find(".//{%s}metadata" % self.CHECK_NAMESPACE) metadata.append(ccerefelem)
def add_references(reference, destination): try: reference_root = ET.parse(reference) except IOError as exception: print("INFO: DISA STIG Reference file not found for this platform") sys.exit(0) reference_rules = reference_root.findall('.//{%s}Rule' % XCCDF11_NS) dictionary = {} for rule in reference_rules: version = rule.find('.//{%s}version' % XCCDF11_NS) if version is not None and version.text: dictionary[version.text] = rule.get('id') target_root = ET.parse(destination) target_rules = target_root.findall('.//{%s}Rule' % XCCDF11_NS) for rule in target_rules: refs = rule.findall('.//{%s}reference' % XCCDF11_NS) for ref in refs: if (ref.get('href').startswith(stig_refs) and ref.text in dictionary): index = rule.getchildren().index(ref) new_ref = ET.Element('{%s}reference' % XCCDF11_NS, {'href': stig_ns}) new_ref.text = dictionary[ref.text] new_ref.tail = ref.tail rule.insert(index + 1, new_ref) return target_root
def to_xml_element(self): group = ET.Element('Group') group.set('id', self.id_) if self.prodtype != "all": group.set("prodtype", self.prodtype) title = ET.SubElement(group, 'title') title.text = self.title add_sub_element(group, 'description', self.description) add_warning_elements(group, self.warnings) for v in self.values.values(): group.append(v.to_xml_element()) for g in self.groups.values(): group.append(g.to_xml_element()) for r in self.rules.values(): group.append(r.to_xml_element()) return group
def translate(self, tree, store_defname=False): for element in tree.getiterator(): idname = element.get("id") if idname: # store the old name if requested (for OVAL definitions) if store_defname and \ element.tag == "{%s}definition" % oval_ns: metadata = element.find("{%s}metadata" % oval_ns) if metadata is None: metadata = ElementTree.SubElement(element, "metadata") defnam = ElementTree.Element("reference", ref_id=idname, source=self.content_id) metadata.append(defnam) # set the element to the new identifier element.set("id", self.generate_id(element.tag, idname)) # continue if element.tag == "{%s}filter" % oval_ns: element.text = self.generate_id("{%s}state" % oval_ns, element.text) continue if element.tag == "{%s#independent}var_ref" % oval_ns: element.text = self.generate_id("{%s}variable" % oval_ns, element.text) continue for attr in element.keys(): if attr in ovalrefattr_to_tag.keys(): element.set( attr, self.generate_id( "{%s}%s" % (oval_ns, ovalrefattr_to_tag[attr]), element.get(attr))) if attr in ocilrefattr_to_tag.keys(): element.set( attr, self.generate_id( "{%s}%s" % (ocil_ns, ocilrefattr_to_tag[attr]), element.get(attr))) if element.tag == "{%s}test_action_ref" % ocil_ns: element.text = self.generate_id("{%s}action" % ocil_ns, element.text) return tree
def to_xml_element(self): root = ET.Element('Benchmark') root.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance') root.set('xmlns:xhtml', 'http://www.w3.org/1999/xhtml') root.set('xmlns:dc', 'http://purl.org/dc/elements/1.1/') root.set('id', 'product-name') root.set('xsi:schemaLocation', 'http://checklists.nist.gov/xccdf/1.1 xccdf-1.1.4.xsd') root.set('style', 'SCAP_1.1') root.set('resolved', 'false') root.set('xml:lang', 'en-US') status = ET.SubElement(root, 'status') status.set('date', datetime.date.today().strftime("%Y-%m-%d")) status.text = self.status add_sub_element(root, "title", self.title) add_sub_element(root, "description", self.description) notice = add_sub_element(root, "notice", self.notice_description) notice.set('id', self.notice_id) add_sub_element(root, "front-matter", self.front_matter) add_sub_element(root, "rear-matter", self.rear_matter) for idref in self.cpes: plat = ET.SubElement(root, "platform") plat.set("idref", idref) version = ET.SubElement(root, 'version') version.text = self.version ET.SubElement(root, "metadata") for profile in self.profiles: if profile is not None: root.append(profile.to_xml_element()) for v in self.values.values(): root.append(v.to_xml_element()) if self.bash_remediation_fns_group is not None: root.append(self.bash_remediation_fns_group) for g in self.groups.values(): root.append(g.to_xml_element()) for r in self.rules.values(): root.append(r.to_xml_element()) return root
def add_missing_check_exports(self, check, checkcontentref): check_name = checkcontentref.get("name") if check_name is None: return oval_def = self.oval_groups["definitions"].get(check_name) if oval_def is None: return all_vars = set() for def_id in self.get_nested_definitions(check_name): extended_def = self.oval_groups["definitions"].get(def_id) if extended_def is None: print("WARNING: Definition '%s' was not found, can't figure " "out which variables it needs." % (def_id), file=sys.stderr) continue all_vars |= resolve_definition(self.oval_groups, extended_def) for varname in all_vars: export = ET.Element("{%s}check-export" % XCCDF11_NS) export.attrib["export-name"] = varname export.attrib["value-id"] = varname check.insert(0, export)
def to_xml_element(self): value = ET.Element('Value') value.set('id', self.id_) value.set('type', self.type_) if self.operator != "equals": # equals is the default value.set('operator', self.operator) if self.interactive: # False is the default value.set('interactive', 'true') title = ET.SubElement(value, 'title') title.text = self.title add_sub_element(value, 'description', self.description) add_warning_elements(value, self.warnings) for selector, option in self.options.items(): # do not confuse Value with big V with value with small v # value is child element of Value value_small = ET.SubElement(value, 'value') # by XCCDF spec, default value is value without selector if selector != "default": value_small.set('selector', str(selector)) value_small.text = str(option) return value
def to_xml_element(self): rule = ET.Element('Rule') rule.set('id', self.id_) if self.prodtype != "all": rule.set("prodtype", self.prodtype) rule.set('severity', self.severity) add_sub_element(rule, 'title', self.title) add_sub_element(rule, 'description', self.description) add_sub_element(rule, 'rationale', self.rationale) main_ident = ET.Element('ident') for ident_type, ident_val in self.identifiers.items(): if '@' in ident_type: # the ident is applicable only on some product # format : 'policy@product', eg. 'stigid@product' # for them, we create a separate <ref> element policy, product = ident_type.split('@') ident = ET.SubElement(rule, 'ident') ident.set(policy, ident_val) ident.set('prodtype', product) else: main_ident.set(ident_type, ident_val) if main_ident.attrib: rule.append(main_ident) main_ref = ET.Element('ref') for ref_type, ref_val in self.references.items(): if '@' in ref_type: # the reference is applicable only on some product # format : 'policy@product', eg. 'stigid@product' # for them, we create a separate <ref> element policy, product = ref_type.split('@') ref = ET.SubElement(rule, 'ref') ref.set(policy, ref_val) ref.set('prodtype', product) else: main_ref.set(ref_type, ref_val) if main_ref.attrib: rule.append(main_ref) if self.external_oval: check = ET.SubElement(rule, 'check') check.set("system", "http://oval.mitre.org/XMLSchema/oval-definitions-5") external_content = ET.SubElement(check, "check-content-ref") external_content.set("href", self.external_oval) else: # TODO: This is pretty much a hack, oval ID will be the same as rule ID # and we don't want the developers to have to keep them in sync. # Therefore let's just add an OVAL ref of that ID. oval_ref = ET.SubElement(rule, "oval") oval_ref.set("id", self.id_) if self.ocil or self.ocil_clause: ocil = add_sub_element(rule, 'ocil', self.ocil if self.ocil else "") if self.ocil_clause: ocil.set("clause", self.ocil_clause) add_warning_elements(rule, self.warnings) return rule
def main(): global definitions global tests global objects global states global variables global silent_mode args = parse_options() silent_mode = args.silent_mode oval_version = args.oval_version testfile = args.xmlfile header = oval_generated_header("testoval.py", oval_version, "0.0.1") testfile = find_testfile(testfile) body = read_ovaldefgroup_file(testfile) defname = _add_elements(body, header) if defname is None: print("Error while evaluating oval: defname not set; missing " "definitions section?") sys.exit(1) ovaltree = ET.fromstring(header + footer) # append each major element type, if it has subelements for element in [definitions, tests, objects, states, variables]: if list(element) > 0: ovaltree.append(element) # re-map all the element ids from meaningful names to meaningless # numbers testtranslator = IDTranslator("scap-security-guide.testing") ovaltree = testtranslator.translate(ovaltree) (ovalfile, fname) = tempfile.mkstemp(prefix=defname, suffix=".xml") os.write(ovalfile, ET.tostring(ovaltree)) os.close(ovalfile) if not silent_mode: print("Evaluating with OVAL tempfile: " + fname) print("OVAL Schema Version: %s" % oval_version) print("Writing results to: " + fname + "-results") cmd = "oscap oval eval --results " + fname + "-results " + fname oscap_child = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) cmd_out = oscap_child.communicate()[0] if not silent_mode: print(cmd_out) if oscap_child.returncode != 0: if not silent_mode: print("Error launching 'oscap' command: \n\t" + cmd) sys.exit(2) if 'false' in cmd_out: # at least one from the evaluated OVAL definitions evaluated to # 'false' result, exit with '1' to indicate OVAL scan FAIL result sys.exit(1) # perhaps delete tempfile? definitions = ET.Element("definitions") tests = ET.Element("tests") objects = ET.Element("objects") states = ET.Element("states") variables = ET.Element("variables") # 'false' keyword wasn't found in oscap's command output # exit with '0' to indicate OVAL scan TRUE result sys.exit(0)
import os import re import argparse import tempfile import subprocess from ssg._constants import oval_footer as footer from ssg._constants import oval_namespace as ovalns from ssg._xml import ElementTree as ET from ssg._xml import oval_generated_header from ssg._id_translate import IDTranslator SHARED_OVAL = re.sub('shared.*', 'shared', __file__) + '/checks/oval/' # globals, to make recursion easier in case we encounter extend_definition definitions = ET.Element("definitions") tests = ET.Element("tests") objects = ET.Element("objects") states = ET.Element("states") variables = ET.Element("variables") silent_mode = False # append new child ONLY if it's not a duplicate def append(element, newchild): global silent_mode newid = newchild.get("id") existing = element.find(".//*[@id='" + newid + "']") if existing is not None: if not silent_mode: sys.stderr.write("Notification: this ID is used more than once " +
def expand_xccdf_subs(fix, remediation_type, remediation_functions): """For those remediation scripts utilizing some of the internal SCAP Security Guide remediation functions expand the selected shell variables and remediation functions calls with <xccdf:sub> element This routine translates any instance of the 'populate' function call in the form of: populate variable_name into variable_name="<sub idref="variable_name"/>" Also transforms any instance of the 'ansible-populate' function call in the form of: (ansible-populate variable_name) into <sub idref="variable_name"/> Also transforms any instance of some other known remediation function (e.g. 'replace_or_append' etc.) from the form of: function_name "arg1" "arg2" ... "argN" into: <sub idref="function_function_name"/> function_name "arg1" "arg2" ... "argN" """ if remediation_type == "ansible": fix_text = fix.text if "(ansible-populate " in fix_text: raise RuntimeError( "(ansible-populate VAR) has been deprecated. Please use " "(xccdf-var VAR) instead. Keep in mind that the latter will " "make an ansible variable out of XCCDF Value as opposed to " "substituting directly." ) fix_text = re.sub( r"- \(xccdf-var\s+(\S+)\)", r"- name: XCCDF Value \1 # promote to variable\n" r" set_fact:\n" r" \1: (ansible-populate \1)\n" r" tags:\n" r" - always", fix_text ) pattern = r'\(ansible-populate\s*(\S+)\)' # we will get list what looks like # [text, varname, text, varname, ..., text] parts = re.split(pattern, fix_text) fix.text = parts[0] # add first "text" for index in range(1, len(parts), 2): varname = parts[index] text_between_vars = parts[index + 1] # we cannot combine elements and text easily # so text is in ".tail" of element xccdfvarsub = ElementTree.SubElement(fix, "sub", idref=varname) xccdfvarsub.tail = text_between_vars return elif remediation_type == "puppet": pattern = r'\(puppet-populate\s*(\S+)\)' # we will get list what looks like # [text, varname, text, varname, ..., text] parts = re.split(pattern, fix.text) fix.text = parts[0] # add first "text" for index in range(1, len(parts), 2): varname = parts[index] text_between_vars = parts[index + 1] # we cannot combine elements and text easily # so text is in ".tail" of element xccdfvarsub = ElementTree.SubElement(fix, "sub", idref=varname) xccdfvarsub.tail = text_between_vars return elif remediation_type == "anaconda": pattern = r'\(anaconda-populate\s*(\S+)\)' # we will get list what looks like # [text, varname, text, varname, ..., text] parts = re.split(pattern, fix.text) fix.text = parts[0] # add first "text" for index in range(1, len(parts), 2): varname = parts[index] text_between_vars = parts[index + 1] # we cannot combine elements and text easily # so text is in ".tail" of element xccdfvarsub = ElementTree.SubElement(fix, "sub", idref=varname) xccdfvarsub.tail = text_between_vars return elif remediation_type == "bash": # This remediation script doesn't utilize internal remediation functions # Skip it without any further processing if 'remediation_functions' not in fix.text: return # This remediation script utilizes some of internal remediation functions # Expand shell variables and remediation functions calls with <xccdf:sub> # elements pattern = '\n+(\s*(?:' + '|'.join(remediation_functions) + ')[^\n]*)\n' patcomp = re.compile(pattern, re.DOTALL) fixparts = re.split(patcomp, fix.text) if fixparts[0] is not None: # Split the portion of fix.text from fix start to first call of # remediation function into two parts: # * head to hold inclusion of the remediation functions # * tail to hold part of the fix.text after inclusion, # but before first call of remediation function try: rfpattern = '(.*remediation_functions)(.*)' rfpatcomp = re.compile(rfpattern, re.DOTALL) _, head, tail, _ = re.split(rfpatcomp, fixparts[0], maxsplit=2) except ValueError: sys.stderr.write("Processing fix.text for: %s rule\n" % fix.get('rule')) sys.stderr.write("Unable to extract part of the fix.text " "after inclusion of remediation functions." " Aborting..\n") sys.exit(1) # If the 'tail' is not empty, make it new fix.text. # Otherwise use '' fix.text = tail if tail is not None else '' # Drop the first element of 'fixparts' since it has been processed fixparts.pop(0) # Perform sanity check on new 'fixparts' list content (to continue # successfully 'fixparts' has to contain even count of elements) if len(fixparts) % 2 != 0: sys.stderr.write("Error performing XCCDF expansion on " "remediation script: %s\n" % fix.get("rule")) sys.stderr.write("Invalid count of elements. Exiting!\n") sys.exit(1) # Process remaining 'fixparts' elements in pairs # First pair element is remediation function to be XCCDF expanded # Second pair element (if not empty) is the portion of the original # fix text to be used in newly added sublement's tail for idx in range(0, len(fixparts), 2): # We previously removed enclosing newlines when creating # fixparts list. Add them back and reuse the above 'pattern' fixparts[idx] = "\n%s\n" % fixparts[idx] # Sanity check (verify the first field truly contains call of # some of the remediation functions) if re.match(pattern, fixparts[idx], re.DOTALL) is not None: # This chunk contains call of 'populate' function if "populate" in fixparts[idx]: varname, fixtextcontrib = get_populate_replacement( remediation_type, fixparts[idx]) # Define new XCCDF <sub> element for the variable xccdfvarsub = ElementTree.Element("sub", idref=varname) # If this is first sub element, # the textcontribution needs to go to fix text # otherwise, append to last subelement nfixchildren = len(list(fix)) if nfixchildren == 0: fix.text += fixtextcontrib else: previouselem = fix[nfixchildren-1] previouselem.tail += fixtextcontrib # If second pair element is not empty, append it as # tail for the subelement (prefixed with closing '"') if fixparts[idx + 1] is not None: xccdfvarsub.tail = '"' + '\n' + fixparts[idx + 1] # Otherwise append just enclosing '"' else: xccdfvarsub.tail = '"' + '\n' # Append the new subelement to the fix element fix.append(xccdfvarsub) # This chunk contains call of other remediation function else: # Extract remediation function name funcname = re.search('\n\s*(\S+)(| .*)\n', fixparts[idx], re.DOTALL).group(1) # Define new XCCDF <sub> element for the function xccdffuncsub = ElementTree.Element( "sub", idref='function_%s' % funcname) # Append original function call into tail of the # subelement xccdffuncsub.tail = fixparts[idx] # If the second element of the pair is not empty, # append it to the tail of the subelement too if fixparts[idx + 1] is not None: xccdffuncsub.tail += fixparts[idx + 1] # Append the new subelement to the fix element fix.append(xccdffuncsub) # Ensure the newly added <xccdf:sub> element for the # function will be always inserted at newline # If xccdffuncsub is the first <xccdf:sub> element # being added as child of <fix> and fix.text doesn't # end up with newline character, append the newline # to the fix.text if list(fix).index(xccdffuncsub) == 0: if re.search('.*\n$', fix.text) is None: fix.text += '\n' # If xccdffuncsub isn't the first child (first # <xccdf:sub> being added), and tail of previous # child doesn't end up with newline, append the newline # to the tail of previous child else: previouselem = fix[list(fix).index(xccdffuncsub) - 1] if re.search('.*\n$', previouselem.tail) is None: previouselem.tail += '\n' # Perform a sanity check if all known remediation function calls have been # properly XCCDF substituted. Exit with failure if some wasn't # First concat output form of modified fix text (including text appended # to all children of the fix) modfix = [fix.text] for child in fix.getchildren(): if child is not None and child.text is not None: modfix.append(child.text) modfixtext = "".join(modfix) for f in remediation_functions: # Then efine expected XCCDF sub element form for this function funcxccdfsub = "<sub idref=\"function_%s\"" % f # Finally perform the sanity check -- if function was properly XCCDF # substituted both the original function call and XCCDF <sub> element # for that function need to be present in the modified text of the fix # Otherwise something went wrong, thus exit with failure if f in modfixtext and funcxccdfsub not in modfixtext: sys.stderr.write("Error performing XCCDF <sub> substitution " "for function %s in %s fix. Exiting...\n" % (f, fix.get("rule"))) sys.exit(1) else: sys.stderr.write("Unknown remediation type '%s'\n" % (remediation_type)) sys.exit(1)