Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 1 | import collections |
| 2 | import datetime |
| 3 | import json |
Ivan Suzdal | 184c4e3 | 2018-06-06 13:55:30 +0400 | [diff] [blame] | 4 | from lxml.etree import Element, SubElement, tostring |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 5 | import os.path |
| 6 | import re |
Ivan Suzdal | 184c4e3 | 2018-06-06 13:55:30 +0400 | [diff] [blame] | 7 | from subprocess import Popen, PIPE |
| 8 | import shlex |
Ivan Suzdal | 184c4e3 | 2018-06-06 13:55:30 +0400 | [diff] [blame] | 9 | |
| 10 | import salt.ext.six as six |
| 11 | |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 12 | from oscap import untangle |
| 13 | |
| 14 | |
Ivan Suzdal | 184c4e3 | 2018-06-06 13:55:30 +0400 | [diff] [blame] | 15 | def normalize_id(id, |
| 16 | xccdf_version='1.2', |
| 17 | typeof='profile', |
| 18 | vendor='mirantis'): |
| 19 | |
| 20 | if xccdf_version == '1.2': |
| 21 | if not re.match('^xccdf_[^_]+_{}_.+'.format(typeof), id): |
| 22 | return 'xccdf_org.{0}.content_{1}_{2}'.format(vendor, typeof, id) |
| 23 | return id |
| 24 | |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 25 | |
Ivan Suzdal | 184c4e3 | 2018-06-06 13:55:30 +0400 | [diff] [blame] | 26 | def build_tailoring(data, id): |
| 27 | xccdf_version = data.get('xccdf_version', '1.2') |
| 28 | ns = {None: 'http://checklists.nist.gov/xccdf/{}'.format(xccdf_version)} |
| 29 | tid = normalize_id(id, xccdf_version, typeof='tailoring') |
| 30 | pid = normalize_id(data['profile'], xccdf_version, vendor='customer') |
| 31 | ext = normalize_id(data['extends'], xccdf_version) |
| 32 | tailoring = Element('Tailoring', nsmap=ns, id=tid) |
| 33 | tailoring.append(Element('benchmark', {'href': ext})) |
| 34 | |
| 35 | now = datetime.datetime.now().isoformat() |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 36 | SubElement(tailoring, 'version', time=now).text = '1' |
Ivan Suzdal | 184c4e3 | 2018-06-06 13:55:30 +0400 | [diff] [blame] | 37 | |
| 38 | profile = SubElement(tailoring, 'Profile', id=pid, extends=ext) |
| 39 | |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 40 | SubElement(profile, 'title').text = 'Extends {}'.format(ext) |
Ivan Suzdal | 184c4e3 | 2018-06-06 13:55:30 +0400 | [diff] [blame] | 41 | |
| 42 | for key, value in six.iteritems(data.get('values', {})): |
| 43 | idref = normalize_id(key, xccdf_version, typeof='value') |
| 44 | elem = SubElement(profile, 'set-value', idref=idref) |
| 45 | elem.text = str(value) |
| 46 | return tostring(tailoring, pretty_print=True) |
| 47 | |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 48 | |
Ivan Suzdal | 184c4e3 | 2018-06-06 13:55:30 +0400 | [diff] [blame] | 49 | def run(cmd, cwd=None): |
| 50 | # The Popen used here because the __salt__['cmd.run'] returns only stdout |
| 51 | proc = Popen(shlex.split(cmd), stdout=PIPE, stderr=PIPE, cwd=cwd) |
| 52 | (stdout, stderr) = proc.communicate() |
| 53 | return stdout, stderr, proc.returncode |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 54 | |
| 55 | |
| 56 | def _get_flatten_groups(document, groups=None): |
| 57 | groups = groups if groups else [] |
| 58 | if hasattr(document, 'Group'): |
| 59 | for group in document.Group: |
| 60 | groups.append(group) |
| 61 | groups = _get_flatten_groups(group, groups) |
| 62 | return groups |
| 63 | |
| 64 | |
| 65 | def _get_rules(groups): |
| 66 | rules = {} |
| 67 | for group in groups: |
| 68 | if hasattr(group, 'Rule'): |
| 69 | for rule in group.Rule: |
| 70 | rules[rule['id']] = { |
| 71 | 'title': rule.title.cdata, |
| 72 | 'severity': rule['severity'], |
Pavlo Shchelokovskyy | 5ccb636 | 2018-10-10 15:16:14 +0300 | [diff] [blame^] | 73 | 'ident': rule.ident.cdata, |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 74 | 'description': rule.description.cdata} |
| 75 | return rules |
| 76 | |
| 77 | |
| 78 | def _parse_xccdf_doc(document): |
| 79 | groups = _get_flatten_groups(document.Benchmark) |
| 80 | rules = _get_rules(groups) |
| 81 | |
| 82 | results = [] |
| 83 | for result in document.Benchmark.TestResult.rule_result: |
| 84 | results.append({ |
| 85 | 'rule': result['idref'], |
| 86 | 'result': result.result.cdata, |
| 87 | 'severity': result['severity'], |
| 88 | 'weight': result['weight'], |
| 89 | 'title': rules[result['idref']]['title'], |
Pavlo Shchelokovskyy | 5ccb636 | 2018-10-10 15:16:14 +0300 | [diff] [blame^] | 90 | 'cis_code': rules[result['idref']]['ident'], |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 91 | 'description': rules[result['idref']]['title'] |
| 92 | }) |
| 93 | |
| 94 | return results |
| 95 | |
| 96 | |
| 97 | def _sanitize_xccdf_xml(data): |
| 98 | data = data.replace( |
| 99 | '<html:code xmlns:html="http://www.w3.org/1999/xhtml">', '') |
| 100 | data = data.replace('</html:code>', '') |
| 101 | data = data.replace( |
| 102 | '<html:pre xmlns:html="http://www.w3.org/1999/xhtml">', '') |
| 103 | data = data.replace('<html:pre>', '') |
| 104 | data = data.replace('</html:pre>', '') |
| 105 | data = data.replace('<html:code>', '') |
| 106 | data = data.replace('<html:li>', '') |
| 107 | data = data.replace('</html:li>', '') |
| 108 | data = data.replace( |
| 109 | '<html:pre xmlns:html="http://www.w3.org/1999/xhtml" ' |
| 110 | 'xmlns:ns0="http://checklists.nist.gov/xccdf/1.1">', |
| 111 | '') |
| 112 | data = data.replace( |
| 113 | '<html:br xmlns:html="http://www.w3.org/1999/xhtml"/>', '') |
| 114 | data = data.replace( |
| 115 | '<html:code xmlns:html="http://www.w3.org/1999/xhtml" ' |
| 116 | 'xmlns:ns0="http://checklists.nist.gov/xccdf/1.1">', |
| 117 | '') |
| 118 | return data |
| 119 | |
| 120 | |
| 121 | def xccdf_xml_to_json(xml_file): |
| 122 | with open(xml_file) as in_file: |
| 123 | raw_xml = in_file.read() |
| 124 | doc = untangle.parse(_sanitize_xccdf_xml(raw_xml)) |
| 125 | results = _parse_xccdf_doc(doc) |
| 126 | with open(os.path.splitext(xml_file)[0] + '.json', 'w') as json_file: |
| 127 | # NOTE(pas-ha) the src/com/mirantis/mk.Common.parseJSON method |
| 128 | # from mk/pipeline-library that is used in our Jenkins pipelines |
| 129 | # can not parse the string representation of a list! |
| 130 | # only dict structure is supported as a top-level one there |
| 131 | json.dump({"results": results}, json_file) |
| 132 | |
| 133 | |
| 134 | def _parse_oval_definitions(document): |
| 135 | definitions = {} |
| 136 | def_list = document.oval_results.oval_definitions.definitions.definition |
| 137 | for definition in def_list: |
| 138 | try: |
| 139 | definitions[definition['id']] = {'class': definition['class']} |
| 140 | def_dict = definitions[definition['id']] |
| 141 | def_dict['title'] = definition.metadata.title.cdata |
| 142 | def_dict['description'] = definition.metadata.description.cdata |
| 143 | def_dict['ref_id'] = definition.metadata.reference['ref_id'] |
| 144 | def_dict['link'] = definition.metadata.reference['ref_url'] |
| 145 | def_dict['severity'] = definition.metadata.advisory.severity.cdata |
Pavlo Shchelokovskyy | 016de2d | 2018-10-09 16:17:51 +0300 | [diff] [blame] | 146 | except (AttributeError, IndexError): |
| 147 | # NOTE(e0ne): inventory does't have some element |
Pavlo Shchelokovskyy | 4a8f1c1 | 2018-09-21 19:17:19 +0300 | [diff] [blame] | 148 | pass |
| 149 | |
| 150 | return definitions |
| 151 | |
| 152 | |
| 153 | def oval_xml_to_json(xml_file): |
| 154 | document = untangle.parse(xml_file) |
| 155 | definitions = _parse_oval_definitions(document) |
| 156 | results = [] |
| 157 | for defn in document.oval_results.results.system.definitions.definition: |
| 158 | res = collections.defaultdict(lambda: None) |
| 159 | res['id'] = defn['definition_id'] |
| 160 | res['result'] = defn['result'] |
| 161 | res.update(definitions[defn['definition_id']]) |
| 162 | results.append(res) |
| 163 | with open(os.path.splitext(xml_file)[0] + '.json', 'w') as json_file: |
| 164 | # NOTE(pas-ha) the src/com/mirantis/mk.Common.parseJSON method |
| 165 | # from mk/pipeline-library that is used in our Jenkins pipelines |
| 166 | # can not parse the string representation of a list! |
| 167 | # only dict structure is supported as a top-level one there |
| 168 | json.dump({"results": results}, json_file) |