1 | from __future__ import absolute_import |
||
2 | from __future__ import print_function |
||
3 | |||
4 | import os |
||
5 | import os.path |
||
6 | import sys |
||
7 | import re |
||
8 | from copy import deepcopy |
||
9 | import collections |
||
10 | |||
11 | from .build_yaml import Rule, DocumentationNotComplete |
||
12 | from .constants import oval_namespace as oval_ns |
||
13 | from .constants import oval_footer |
||
14 | from .constants import oval_header |
||
15 | from .constants import MULTI_PLATFORM_LIST |
||
16 | from .id_translate import IDTranslator |
||
17 | from .jinja import process_file_with_macros |
||
18 | from .rule_yaml import parse_prodtype |
||
19 | from .rules import get_rule_dir_id, get_rule_dir_ovals, find_rule_dirs_in_paths |
||
20 | from . import utils, products |
||
21 | from .utils import mkdir_p |
||
22 | from .xml import ElementTree, oval_generated_header |
||
23 | |||
24 | |||
25 | def _create_subtree(shorthand_tree, category): |
||
26 | parent_tag = "{%s}%ss" % (oval_ns, category) |
||
27 | parent = ElementTree.Element(parent_tag) |
||
28 | for node in shorthand_tree.findall(".//{%s}def-group/*" % oval_ns): |
||
29 | if node.tag is ElementTree.Comment: |
||
30 | continue |
||
31 | elif node.tag.endswith(category): |
||
32 | append(parent, node) |
||
33 | return parent |
||
34 | |||
35 | |||
36 | def expand_shorthand(shorthand_path, oval_path, env_yaml): |
||
37 | shorthand_file_content = process_file_with_macros(shorthand_path, env_yaml) |
||
38 | wrapped_shorthand = (oval_header + shorthand_file_content + oval_footer) |
||
39 | shorthand_tree = ElementTree.fromstring(wrapped_shorthand.encode("utf-8")) |
||
40 | header = oval_generated_header("test", "5.11", "1.0") |
||
41 | skeleton = header + oval_footer |
||
42 | root = ElementTree.fromstring(skeleton.encode("utf-8")) |
||
43 | for category in ["definition", "test", "object", "state", "variable"]: |
||
44 | subtree = _create_subtree(shorthand_tree, category) |
||
45 | if list(subtree): |
||
46 | root.append(subtree) |
||
47 | id_translator = IDTranslator("test") |
||
48 | root_translated = id_translator.translate(root) |
||
49 | |||
50 | ElementTree.ElementTree(root_translated).write(oval_path) |
||
51 | |||
52 | |||
53 | def _check_is_applicable_for_product(oval_check_def, product): |
||
54 | """Based on the <platform> specifier of the OVAL check determine if this |
||
55 | OVAL check is applicable for this product. Return 'True' if so, 'False' |
||
56 | otherwise""" |
||
57 | |||
58 | product, product_version = utils.parse_name(product) |
||
59 | |||
60 | # Define general platforms |
||
61 | multi_platforms = ['<platform>multi_platform_all', |
||
62 | '<platform>multi_platform_' + product] |
||
63 | |||
64 | # First test if OVAL check isn't for 'multi_platform_all' or |
||
65 | # 'multi_platform_' + product |
||
66 | for multi_prod in multi_platforms: |
||
67 | if multi_prod in oval_check_def and product in MULTI_PLATFORM_LIST: |
||
68 | return True |
||
69 | |||
70 | # Current SSG checks aren't unified which element of '<platform>' |
||
71 | # and '<product>' to use as OVAL AffectedType metadata element, |
||
72 | # e.g. Chromium content uses both of them across the various checks |
||
73 | # Thus for now check both of them when checking concrete platform / product |
||
74 | affected_type_elements = ['<platform>', '<product>'] |
||
75 | |||
76 | for afftype in affected_type_elements: |
||
77 | # Get official name for product (prefixed with content of afftype) |
||
78 | product_name = afftype + utils.map_name(product) |
||
79 | # Append the product version to the official name |
||
80 | if product_version is not None: |
||
81 | # Some product versions have a dot in between the numbers |
||
82 | # While the prodtype doesn't have the dot, the full product name does |
||
83 | if product == "ubuntu" or product == "macos": |
||
84 | product_version = product_version[:2] + "." + product_version[2:] |
||
85 | product_name += ' ' + product_version |
||
86 | |||
87 | # Test if this OVAL check is for the concrete product version |
||
88 | if product_name in oval_check_def: |
||
89 | return True |
||
90 | |||
91 | # OVAL check isn't neither a multi platform one, nor isn't applicable |
||
92 | # for this product => return False to indicate that |
||
93 | |||
94 | return False |
||
95 | |||
96 | |||
97 | def finalize_affected_platforms(xml_tree, env_yaml): |
||
98 | """Depending on your use-case of OVAL you may not need the <affected> |
||
99 | element. Such use-cases including using OVAL as a check engine for XCCDF |
||
100 | benchmarks. Since the XCCDF Benchmarks use cpe:platform with CPE IDs, |
||
101 | the affected element in OVAL definitions is redundant and just bloats the |
||
102 | files. This function removes all *irrelevant* affected platform elements |
||
103 | from given OVAL tree. It then adds one platform of the product we are |
||
104 | building. |
||
105 | """ |
||
106 | |||
107 | for affected in xml_tree.findall(".//{%s}affected" % (oval_ns)): |
||
108 | for platform in affected.findall("./{%s}platform" % (oval_ns)): |
||
109 | affected.remove(platform) |
||
110 | for product in affected.findall("./{%s}product" % (oval_ns)): |
||
111 | affected.remove(product) |
||
112 | |||
113 | final = ElementTree.SubElement( |
||
114 | affected, "{%s}%s" % (oval_ns, utils.required_key(env_yaml, "type"))) |
||
115 | final.text = utils.required_key(env_yaml, "full_name") |
||
116 | |||
117 | return xml_tree |
||
118 | |||
119 | |||
120 | def oval_entities_are_identical(firstelem, secondelem): |
||
121 | """Check if OVAL entities represented by XML elements are identical |
||
122 | Return: True if identical, False otherwise |
||
123 | Based on: http://stackoverflow.com/a/24349916""" |
||
124 | |||
125 | # Per https://github.com/ComplianceAsCode/content/pull/1343#issuecomment-234541909 |
||
126 | # and https://github.com/ComplianceAsCode/content/pull/1343#issuecomment-234545296 |
||
127 | # ignore the differences in 'comment', 'version', 'state_operator', and |
||
128 | # 'deprecated' attributes. Also ignore different nsmap, since all these |
||
129 | # don't affect the semantics of the OVAL entities |
||
130 | |||
131 | # Operate on copies of the elements (since we will modify |
||
132 | # some attributes). Deepcopy will also reset the namespace map |
||
133 | # on copied elements for us |
||
134 | firstcopy = deepcopy(firstelem) |
||
135 | secondcopy = deepcopy(secondelem) |
||
136 | |||
137 | # Ignore 'comment', 'version', 'state_operator', and 'deprecated' |
||
138 | # attributes since they don't change the semantics of an element |
||
139 | for copy in [firstcopy, secondcopy]: |
||
140 | for key in copy.keys(): |
||
141 | if key in ["comment", "version", "state_operator", |
||
142 | "deprecated"]: |
||
143 | del copy.attrib[key] |
||
144 | |||
145 | # Compare the equality of the copies |
||
146 | if firstcopy.tag != secondcopy.tag: |
||
147 | return False |
||
148 | if firstcopy.text != secondcopy.text: |
||
149 | return False |
||
150 | if firstcopy.tail != secondcopy.tail: |
||
151 | return False |
||
152 | if firstcopy.attrib != secondcopy.attrib: |
||
153 | return False |
||
154 | if len(firstcopy) != len(secondcopy): |
||
155 | return False |
||
156 | |||
157 | return all(oval_entities_are_identical( |
||
158 | fchild, schild) for fchild, schild in zip(firstcopy, secondcopy)) |
||
159 | |||
160 | |||
161 | def oval_entity_is_extvar(elem): |
||
162 | """Check if OVAL entity represented by XML element is OVAL |
||
163 | <external_variable> element |
||
164 | Return: True if <external_variable>, False otherwise""" |
||
165 | |||
166 | return elem.tag == '{%s}external_variable' % oval_ns |
||
167 | |||
168 | |||
169 | element_child_cache = collections.defaultdict(dict) |
||
170 | |||
171 | |||
172 | def append(element, newchild): |
||
173 | """Append new child ONLY if it's not a duplicate""" |
||
174 | |||
175 | global element_child_cache |
||
176 | |||
177 | newid = newchild.get("id") |
||
178 | existing = element_child_cache[element].get(newid, None) |
||
179 | |||
180 | if existing is not None: |
||
181 | # ID is identical and OVAL entities are identical |
||
182 | if oval_entities_are_identical(existing, newchild): |
||
183 | # Moreover the entity is OVAL <external_variable> |
||
184 | if oval_entity_is_extvar(newchild): |
||
185 | # If OVAL entity is identical to some already included |
||
186 | # in the benchmark and represents an OVAL <external_variable> |
||
187 | # it's safe to ignore this ID (since external variables are |
||
188 | # in multiple checks for clarity reasons) |
||
189 | pass |
||
190 | # Some other OVAL entity |
||
191 | else: |
||
192 | # If OVAL entity is identical, but not external_variable, the |
||
193 | # implementation should be rewritten each entity to be present |
||
194 | # just once |
||
195 | sys.stderr.write("ERROR: OVAL ID '%s' is used multiple times " |
||
196 | "and should represent the same elements.\n" |
||
197 | % (newid)) |
||
198 | sys.stderr.write("Rewrite the OVAL checks. Place the identical " |
||
199 | "IDs into their own definition and extend " |
||
200 | "this definition by it.\n") |
||
201 | sys.exit(1) |
||
202 | # ID is identical, but OVAL entities are semantically difference => |
||
203 | # report and error and exit with failure |
||
204 | # Fixes: https://github.com/ComplianceAsCode/content/issues/1275 |
||
205 | else: |
||
206 | if not oval_entity_is_extvar(existing) and \ |
||
207 | not oval_entity_is_extvar(newchild): |
||
208 | # This is an error scenario - since by skipping second |
||
209 | # implementation and using the first one for both references, |
||
210 | # we might evaluate wrong requirement for the second entity |
||
211 | # => report an error and exit with failure in that case |
||
212 | # See |
||
213 | # https://github.com/ComplianceAsCode/content/issues/1275 |
||
214 | # for a reproducer and what could happen in this case |
||
215 | sys.stderr.write("ERROR: it's not possible to use the " + |
||
216 | "same ID: %s " % newid + "for two " + |
||
217 | "semantically different OVAL entities:\n") |
||
218 | sys.stderr.write("First entity %s\n" % ElementTree.tostring(existing)) |
||
219 | sys.stderr.write("Second entity %s\n" % ElementTree.tostring(newchild)) |
||
220 | sys.stderr.write("Use different ID for the second entity!!!\n") |
||
221 | sys.exit(1) |
||
222 | else: |
||
223 | element.append(newchild) |
||
224 | element_child_cache[element][newid] = newchild |
||
225 | |||
226 | |||
227 | def check_oval_version(oval_version): |
||
228 | """Not necessary, but should help with typos""" |
||
229 | |||
230 | supported_versions = ["5.11"] |
||
231 | if oval_version not in supported_versions: |
||
232 | supported_versions_str = ", ".join(supported_versions) |
||
233 | sys.stderr.write( |
||
234 | "Suspicious oval version \"%s\", one of {%s} is " |
||
235 | "expected.\n" % (oval_version, supported_versions_str)) |
||
236 | sys.exit(1) |
||
237 | |||
238 | |||
239 | def _check_is_loaded(loaded_dict, filename, version): |
||
240 | if filename in loaded_dict: |
||
241 | if loaded_dict[filename] >= version: |
||
242 | return True |
||
243 | |||
244 | # Should rather fail, than override something unwanted |
||
245 | sys.stderr.write( |
||
246 | "You cannot override generic OVAL file in version '%s' " |
||
247 | "by more specific one in older version '%s'" % |
||
248 | (version, loaded_dict[filename]) |
||
249 | ) |
||
250 | sys.exit(1) |
||
251 | |||
252 | return False |
||
253 | |||
254 | |||
255 | def _create_oval_tree_from_string(xml_content): |
||
256 | try: |
||
257 | argument = oval_header + xml_content + oval_footer |
||
258 | oval_file_tree = ElementTree.fromstring(argument) |
||
259 | except ElementTree.ParseError as error: |
||
260 | line, column = error.position |
||
261 | lines = argument.splitlines() |
||
262 | before = '\n'.join(lines[:line]) |
||
263 | column_pointer = ' ' * (column - 1) + '^' |
||
264 | sys.stderr.write( |
||
265 | "%s\n%s\nError when parsing OVAL file.\n" % |
||
266 | (before, column_pointer)) |
||
267 | sys.exit(1) |
||
268 | return oval_file_tree |
||
269 | |||
270 | |||
271 | def _check_oval_version_from_oval(oval_file_tree, oval_version): |
||
272 | for defgroup in oval_file_tree.findall("./{%s}def-group" % oval_ns): |
||
273 | file_oval_version = defgroup.get("oval_version") |
||
274 | |||
275 | if file_oval_version is None: |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
276 | # oval_version does not exist in <def-group/> |
||
277 | # which means the OVAL is supported for any version. |
||
278 | # By default, that version is 5.11 |
||
279 | file_oval_version = "5.11" |
||
280 | |||
281 | if tuple(oval_version.split(".")) >= tuple(file_oval_version.split(".")): |
||
282 | return True |
||
283 | |||
284 | |||
285 | def _check_rule_id(oval_file_tree, rule_id): |
||
286 | for definition in oval_file_tree.findall( |
||
287 | "./{%s}def-group/{%s}definition" % (oval_ns, oval_ns)): |
||
288 | definition_id = definition.get("id") |
||
289 | return definition_id == rule_id |
||
290 | return False |
||
291 | |||
292 | |||
293 | def _list_full_paths(directory): |
||
294 | full_paths = [os.path.join(directory, x) for x in os.listdir(directory)] |
||
295 | return sorted(full_paths) |
||
296 | |||
297 | |||
298 | class OVALBuilder: |
||
299 | def __init__( |
||
300 | self, env_yaml, product_yaml_path, shared_directories, |
||
301 | build_ovals_dir): |
||
302 | self.env_yaml = env_yaml |
||
303 | self.product_yaml = products.Product(product_yaml_path) |
||
304 | self.shared_directories = shared_directories |
||
305 | self.build_ovals_dir = build_ovals_dir |
||
306 | self.already_loaded = dict() |
||
307 | self.oval_version = utils.required_key( |
||
308 | env_yaml, "target_oval_version_str") |
||
309 | self.product = utils.required_key(env_yaml, "product") |
||
310 | |||
311 | def build_shorthand(self, include_benchmark): |
||
312 | if self.build_ovals_dir: |
||
313 | mkdir_p(self.build_ovals_dir) |
||
314 | all_checks = [] |
||
315 | if include_benchmark: |
||
316 | all_checks += self._get_checks_from_benchmark() |
||
317 | all_checks += self._get_checks_from_shared_directories() |
||
318 | document_body = "".join(all_checks) |
||
319 | return document_body |
||
320 | |||
321 | def _get_checks_from_benchmark(self): |
||
322 | product_dir = self.product_yaml["product_dir"] |
||
323 | relative_guide_dir = utils.required_key(self.env_yaml, "benchmark_root") |
||
324 | guide_dir = os.path.abspath( |
||
325 | os.path.join(product_dir, relative_guide_dir)) |
||
326 | additional_content_directories = self.env_yaml.get( |
||
327 | "additional_content_directories", []) |
||
328 | dirs_to_scan = [guide_dir] |
||
329 | for rd in additional_content_directories: |
||
330 | abspath = os.path.abspath(os.path.join(product_dir, rd)) |
||
331 | dirs_to_scan.append(abspath) |
||
332 | rule_dirs = list(find_rule_dirs_in_paths(dirs_to_scan)) |
||
333 | oval_checks = self._process_directories(rule_dirs, True) |
||
334 | return oval_checks |
||
335 | |||
336 | def _get_checks_from_shared_directories(self): |
||
337 | # earlier directory has higher priority |
||
338 | reversed_dirs = self.shared_directories[::-1] |
||
339 | oval_checks = self._process_directories(reversed_dirs, False) |
||
340 | return oval_checks |
||
341 | |||
342 | def _process_directories(self, directories, from_benchmark): |
||
343 | oval_checks = [] |
||
344 | for directory in directories: |
||
345 | if not os.path.exists(directory): |
||
346 | continue |
||
347 | oval_checks += self._process_directory(directory, from_benchmark) |
||
348 | return oval_checks |
||
349 | |||
350 | def _get_list_of_oval_files(self, directory, from_benchmark): |
||
351 | if from_benchmark: |
||
352 | oval_files = get_rule_dir_ovals(directory, self.product) |
||
353 | else: |
||
354 | oval_files = _list_full_paths(directory) |
||
355 | return oval_files |
||
356 | |||
357 | def _process_directory(self, directory, from_benchmark): |
||
358 | try: |
||
359 | context = self._get_context(directory, from_benchmark) |
||
360 | except DocumentationNotComplete: |
||
361 | return [] |
||
362 | oval_files = self._get_list_of_oval_files(directory, from_benchmark) |
||
363 | oval_checks = self._get_directory_oval_checks( |
||
364 | context, oval_files, from_benchmark) |
||
365 | return oval_checks |
||
366 | |||
367 | def _get_directory_oval_checks(self, context, oval_files, from_benchmark): |
||
368 | oval_checks = [] |
||
369 | for file_path in oval_files: |
||
370 | xml_content = self._process_oval_file( |
||
371 | file_path, from_benchmark, context) |
||
372 | if xml_content is None: |
||
373 | continue |
||
374 | oval_checks.append(xml_content) |
||
375 | return oval_checks |
||
376 | |||
377 | def _read_oval_file(self, file_path, context, from_benchmark): |
||
378 | if from_benchmark or "checks_from_templates" not in file_path: |
||
379 | xml_content = process_file_with_macros(file_path, context) |
||
380 | else: |
||
381 | with open(file_path, "r") as f: |
||
382 | xml_content = f.read() |
||
383 | return xml_content |
||
384 | |||
385 | def _create_key(self, file_path, from_benchmark): |
||
386 | if from_benchmark: |
||
387 | rule_id = os.path.basename( |
||
388 | (os.path.dirname(os.path.dirname(file_path)))) |
||
389 | oval_key = "%s.xml" % rule_id |
||
390 | else: |
||
391 | oval_key = os.path.basename(file_path) |
||
392 | return oval_key |
||
393 | |||
394 | def _process_oval_file(self, file_path, from_benchmark, context): |
||
395 | if not file_path.endswith(".xml"): |
||
396 | return None |
||
397 | oval_key = self._create_key(file_path, from_benchmark) |
||
398 | if _check_is_loaded(self.already_loaded, oval_key, self.oval_version): |
||
399 | return None |
||
400 | xml_content = self._read_oval_file(file_path, context, from_benchmark) |
||
401 | if not self._manage_oval_file_xml_content( |
||
402 | file_path, xml_content, from_benchmark): |
||
403 | return None |
||
404 | self.already_loaded[oval_key] = self.oval_version |
||
405 | return xml_content |
||
406 | |||
407 | def _check_affected(self, tree): |
||
408 | definitions = tree.findall(".//{%s}definition" % (oval_ns)) |
||
409 | for definition in definitions: |
||
410 | def_id = definition.get("id") |
||
411 | affected = definition.findall( |
||
412 | "./{%s}metadata/{%s}affected" % (oval_ns, oval_ns)) |
||
413 | if not affected: |
||
414 | raise ValueError( |
||
415 | "Definition '%s' doesn't contain OVAL 'affected' element" |
||
416 | % (def_id)) |
||
417 | |||
418 | def _manage_oval_file_xml_content( |
||
419 | self, file_path, xml_content, from_benchmark): |
||
420 | oval_file_tree = _create_oval_tree_from_string(xml_content) |
||
421 | self._check_affected(oval_file_tree) |
||
422 | if not _check_is_applicable_for_product(xml_content, self.product): |
||
423 | return False |
||
424 | if not _check_oval_version_from_oval(oval_file_tree, self.oval_version): |
||
425 | return False |
||
426 | if from_benchmark: |
||
427 | self._benchmark_specific_actions( |
||
428 | file_path, xml_content, oval_file_tree) |
||
429 | return True |
||
430 | |||
431 | def _benchmark_specific_actions( |
||
432 | self, file_path, xml_content, oval_file_tree): |
||
433 | rule_id = os.path.basename( |
||
434 | (os.path.dirname(os.path.dirname(file_path)))) |
||
435 | self._store_intermediate_file(rule_id, xml_content) |
||
436 | if not _check_rule_id(oval_file_tree, rule_id): |
||
437 | msg = "ERROR: OVAL definition in '%s' doesn't match rule ID '%s'." % ( |
||
438 | file_path, rule_id) |
||
439 | print(msg, file=sys.stderr) |
||
440 | sys.exit(1) |
||
441 | |||
442 | def _get_context(self, directory, from_benchmark): |
||
443 | if from_benchmark: |
||
444 | rule_path = os.path.join(directory, "rule.yml") |
||
445 | rule = Rule.from_yaml(rule_path, self.env_yaml) |
||
446 | context = self._create_local_env_yaml_for_rule(rule) |
||
447 | else: |
||
448 | context = self.env_yaml |
||
449 | return context |
||
450 | |||
451 | def _create_local_env_yaml_for_rule(self, rule): |
||
452 | local_env_yaml = dict() |
||
453 | local_env_yaml.update(self.env_yaml) |
||
454 | local_env_yaml['rule_id'] = rule.id_ |
||
455 | local_env_yaml['rule_title'] = rule.title |
||
456 | prodtypes = parse_prodtype(rule.prodtype) |
||
457 | local_env_yaml['products'] = prodtypes # default is all |
||
458 | return local_env_yaml |
||
459 | |||
460 | def _store_intermediate_file(self, rule_id, xml_content): |
||
461 | if not self.build_ovals_dir: |
||
462 | return |
||
463 | output_file_name = rule_id + ".xml" |
||
464 | output_filepath = os.path.join(self.build_ovals_dir, output_file_name) |
||
465 | with open(output_filepath, "w") as f: |
||
466 | f.write(xml_content) |
||
467 |