Skip to content

Commit 2c04d93

Browse files
authored
Merge pull request #1411 from DominiqueDevinci/contrib_python
python binding (fixes + tests)
2 parents 1357582 + abdccd0 commit 2c04d93

13 files changed

+930
-107
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.gdb_history
55
.libs/
66
.*.swp
7+
*.pyc
78
tags
89
*.pyc
910

swig/openscap_api.py

Lines changed: 228 additions & 106 deletions
Large diffs are not rendered by default.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python
2+
3+
# Author:
4+
# Dominique Blaze <[email protected]>
5+
6+
import os
7+
from import_handler import oscap, result2str, get_path
8+
9+
10+
'''
11+
Import and browse benchmark results
12+
13+
'''
14+
15+
benchmark = oscap.xccdf.benchmark_import(get_path("samples/xccdf_sample_results.xml"))
16+
if benchmark.instance is None:
17+
raise Exception("Cannot import the benchmark {0}"
18+
.format(get_path("samples/xccdf_sample_results.xml")))
19+
else:
20+
print("Benchmark id: ", benchmark.get_id())
21+
results = benchmark.get_results()
22+
test_result = results.pop()
23+
print(test_result.get_id())
24+
25+
for rs in test_result.get_rule_results():
26+
idref, result = rs.get_idref(), rs.get_result()
27+
print(idref, result2str(result))
28+
29+
if (idref == "R-SHOULD_PASS" and
30+
result != oscap.xccdf.XCCDF_RESULT_PASS):
31+
32+
raise Exception("Rule result for {0} should be PASS but is currently {1}."
33+
.format(idref,
34+
result2str(result)))
35+
36+
elif (idref == "R-SHOULD_FAIL" and
37+
result != oscap.xccdf.XCCDF_RESULT_FAIL):
38+
39+
raise Exception("Rule result for {0} should be FAIL but is currently {1}."
40+
.format(idref,
41+
result2str(result)))
42+
43+
44+
# Now ensure that benchmark_import return None if the file doesn't exists
45+
46+
benchmark = oscap.xccdf.benchmark_import("../../oval_details/file_doesnt_exists.xml")
47+
if benchmark.instance is not None:
48+
raise Exception("benchmark_import should return None if it can't open the file")
49+
else:
50+
print("benchmark_import on a non existing file returns None.")
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python
2+
3+
# Author:
4+
# Dominique Blaze <[email protected]>
5+
6+
import sys
7+
import os
8+
import openscap_api as oscap
9+
10+
11+
'''
12+
Ensure that the openscap_api is really imported from the desired install dir
13+
Python doesn't directly use env(PYTHONPATH), but sys.path which aggregate
14+
potential paths. So we check that the import path is in PYTHONPATH.
15+
16+
To learn more about sys.path:
17+
https://stackoverflow.com/questions/897792/where-is-pythons-sys-path-initialized-from/38403654#38403654
18+
'''
19+
if os.path.dirname(oscap.__file__) not in os.getenv('PYTHONPATH'):
20+
raise Exception("openscap_api is loaded but from the local env "
21+
"instead of the tested environment.\n"
22+
"Loaded module path = {0}".format(oscap.__file__))
23+
else:
24+
print("openscap_api loaded from "+oscap.__file__)
25+
pass # import is loaded from the right env
26+
27+
28+
'''
29+
Return the string corresponding to the oscap result (PASS, FAIL etc.)
30+
'''
31+
32+
33+
def result2str(result):
34+
if result == oscap.xccdf.XCCDF_RESULT_PASS:
35+
return "PASS"
36+
elif result == oscap.xccdf.XCCDF_RESULT_FAIL:
37+
return "FAIL"
38+
elif result == oscap.xccdf.XCCDF_RESULT_ERROR:
39+
return "ERROR"
40+
elif result == oscap.xccdf.XCCDF_RESULT_UNKNOWN:
41+
return "UNKNOWN"
42+
elif result == oscap.xccdf.XCCDF_RESULT_NOT_APPLICABLE:
43+
return "NOT_APPLICABLE"
44+
elif result == oscap.xccdf.XCCDF_RESULT_NOT_CHECKED:
45+
return "NOT_CHECKED"
46+
elif result == oscap.xccdf.XCCDF_RESULT_NOT_SELECTED:
47+
return "NOT_SELECTED"
48+
elif result == oscap.xccdf.XCCDF_RESULT_INFORMATIONAL:
49+
return "INFORMATIONAL"
50+
elif result == oscap.xccdf.XCCDF_RESULT_FIXED:
51+
return "FIXED"
52+
53+
54+
'''
55+
Return the absolute path of a relative path (located in this folder)
56+
Required because the tests are run from the build directory,
57+
so you need to use this functions for all relative paths.
58+
'''
59+
60+
61+
def get_path(path_str):
62+
return os.path.join(os.path.dirname(__file__), path_str)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env python
2+
3+
# Author:
4+
# Dominique Blaze <[email protected]>
5+
#
6+
# Description:
7+
# Test introspection features of Python API
8+
# - Test the presence or the absence of expected builtins functions
9+
# - Indentify a constant name by its value and prefix (prefix XCCDF_RESULT
10+
# with value 1 (usually) should identify XCCDF_RESULT_PASS
11+
# - Check presence of constants filtered by a prefix (XCCDF_RESULT with
12+
# value = None should returns all XCCDF_RESULT_* constants).
13+
#
14+
15+
16+
from import_handler import oscap
17+
from pprint import pprint
18+
19+
oval_funcs = oscap.oval.introspect_functions()
20+
21+
if oval_funcs.get('oval_variable_get_values') is None:
22+
raise Exception("method 'oval_variable_get_values' not found in builtins"
23+
"functions of oval object (should be in oscap.oval.introspect_functions())")
24+
25+
if oval_funcs.get('xccdf_check_get_id') is not None:
26+
raise Exception("method 'xccdf_check_get_id' should not be found in "
27+
"oscap.oval.introspect_functions(), because the prefix isn't 'oval')")
28+
29+
if oscap.oval.introspect_all().get('xccdf_check_get_id') is None:
30+
raise Exception("method 'xccdf_check_get_id' should be present in "
31+
"oscap.oval.introspect_all(), which returns all available builtins functions")
32+
33+
print("Introspection of builtins functions seems working.")
34+
35+
36+
# should returns {'XCCDF_RESULT_PASS': 1}
37+
# but it's not impossible that the numeric value change (in fact we don't care of it)
38+
pass_val = oscap.oval.XCCDF_RESULT_PASS # is usually 1
39+
40+
var1 = oscap.oval.introspect_constants(pass_val, "XCCDF_RESULT")
41+
if len(var1) == 1 and list(var1.keys())[0] == "XCCDF_RESULT_PASS":
42+
print("Introspection of oscap.oval.XCCDF_RESULT_PASS ok")
43+
44+
45+
xccdf_results = oscap.oval.introspect_constants(None, "XCCDF_RESULT")
46+
# should returns something like {
47+
# 'XCCDF_RESULT_ERROR': 3,
48+
# 'XCCDF_RESULT_FAIL': 2,
49+
# 'XCCDF_RESULT_FIXED': 9,
50+
# 'XCCDF_RESULT_INFORMATIONAL': 8,
51+
# 'XCCDF_RESULT_NOT_APPLICABLE': 5,
52+
# 'XCCDF_RESULT_NOT_CHECKED': 6,
53+
# 'XCCDF_RESULT_NOT_SELECTED': 7,
54+
# 'XCCDF_RESULT_PASS': 1,
55+
# 'XCCDF_RESULT_UNKNOWN': 4}
56+
57+
expected_constants = ('XCCDF_RESULT_ERROR', 'XCCDF_RESULT_FAIL', 'XCCDF_RESULT_FIXED',
58+
'XCCDF_RESULT_INFORMATIONAL', 'XCCDF_RESULT_NOT_APPLICABLE',
59+
'XCCDF_RESULT_NOT_CHECKED', 'XCCDF_RESULT_PASS',
60+
'XCCDF_RESULT_NOT_SELECTED', 'XCCDF_RESULT_UNKNOWN')
61+
62+
for c in expected_constants:
63+
if c not in xccdf_results:
64+
raise Exception("oscap.oval.introspect_constants(None, 'XCCDF_RESULT) should"
65+
"contains the constant {0}.".format(c))
66+
67+
print("Introspection of constants ok with prefix XCCDF_RESULT_*")

tests/bindings/python/oval_eval.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python
2+
3+
# Author:
4+
# Dominique Blaze <[email protected]>
5+
#
6+
7+
'''
8+
Basic OVAL evaluation:
9+
10+
Import an oval file with oscap.oval.definition_model_import_source
11+
and run evalutation of some sample checks using a callback
12+
13+
Tested functions:
14+
- oscap.oval.definition_model_import_source
15+
- oscap.common.source_new_from_file
16+
- TODO oscap.oval.import_model
17+
- oscap.oval.agent_new_session
18+
- oscap.oval.agent_eval_system + associated callback
19+
- oval_result_definition.get_id()
20+
- oval_result_definition.get_result()
21+
- oval_result_definition.get_criteria()
22+
23+
Tested in oval_helpers.browse_criteria:
24+
- oval_result_criteria.get_type() + some of associated constants OVAL_NODETYPE_*
25+
- oval_result_criteria.get_subnodes()
26+
- oval_result_criteria.get_test()
27+
28+
- oval_result_test.get_test()
29+
- oval_result_test.get_result()
30+
31+
- oval_test.get_subtype() + some of associated constants OVAL_SUBYTPE_*
32+
- oval_test.get_family() + some of associated constants
33+
'''
34+
35+
import os
36+
import time
37+
from import_handler import oscap, result2str, get_path
38+
from oval_helpers import browse_criteria
39+
40+
41+
'''
42+
Intermediate functions
43+
'''
44+
45+
46+
# if you return something in callback else than 0, the current session stops
47+
def oval_sample_callback(ovdef, usr):
48+
49+
try:
50+
# .eval or .get_result return the same thing
51+
usr['results'].append(ovdef.get_id() + " => " + result2str(ovdef.get_result()))
52+
53+
# retrieve the tests tree and replace test_results by [subtype => result]
54+
tests_tree = browse_criteria(ovdef.get_criteria(), 1)
55+
56+
'''
57+
Expected tree for foo_pass: [[(7006, 1)]]
58+
= [[(OVAL_INDEPENDENT_TEXT_FILE_CONTENT_54, XCCDF_RESULT_PASS)]]
59+
or [[(OVAL_INDEPENDENT_TEXT_FILE_CONTENT_54, XCCDF_RESULT_PASS)]] for foo_fail
60+
'''
61+
62+
expected_trees = {"oval:foo_pass:def:1": [[
63+
(oscap.oval.OVAL_FAMILY_INDEPENDENT,
64+
oscap.oval.OVAL_INDEPENDENT_TEXT_FILE_CONTENT_54,
65+
oscap.oval.XCCDF_RESULT_PASS)]],
66+
67+
"oval:foo_fail:def:1": [[
68+
(oscap.oval.OVAL_FAMILY_INDEPENDENT,
69+
oscap.oval.OVAL_INDEPENDENT_TEXT_FILE_CONTENT_54,
70+
oscap.oval.XCCDF_RESULT_FAIL)]]
71+
}
72+
73+
expected_tree = expected_trees.get(ovdef.get_id())
74+
75+
if expected_tree is None:
76+
raise ValueError("Unexpected oval def : {0}. No expected tests tree defined for it"
77+
.format(ovdef.get_id()))
78+
79+
elif expected_tree == tests_tree:
80+
usr['results'].append("Tests tree of {0} is like expected : {1}".format(ovdef.get_id(),
81+
tests_tree))
82+
83+
else:
84+
raise ValueError("Tests tree of {0} doesn't match with the expected tree.\n"
85+
"Extracted tree: {1}\nExpected tree: {2}"
86+
.format(ovdef.get_id(), tests_tree, expected_tree))
87+
88+
except Exception as e:
89+
usr['results'].append(e)
90+
91+
return 0
92+
93+
94+
# eval oval defs oval:[foo_pass|fail]:def:1
95+
def oval_eval_sample(oval_defs):
96+
states = {'false': 0, 'true': 0, 'err': 0, 'unknown': 0, 'neval': 0,
97+
'na': 0, 'verbose': True, 'results': []}
98+
99+
sess = oscap.oval.agent_new_session(oval_defs, "")
100+
ret = oscap.oval.agent_eval_system(sess, oval_sample_callback, states)
101+
102+
for tmp in states['results']:
103+
if isinstance(tmp, Exception):
104+
raise tmp
105+
else:
106+
print(tmp)
107+
108+
'''
109+
================ MAIN TEST ====================
110+
'''
111+
112+
113+
print("Opening oval file with original C functions ... ")
114+
oval_defs = oscap.oval.definition_model_import_source(
115+
oscap.common.source_new_from_file(get_path("samples/oval_sample.xml")))
116+
117+
oval_eval_sample(oval_defs)
118+
119+
# TODO: do the same thing with oval_import

tests/bindings/python/oval_helpers.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python
2+
3+
# Author:
4+
# Dominique Blaze <[email protected]>
5+
#
6+
# Description:
7+
# Helpers functions in order to factorize other tests features
8+
# It can potentially be integrated later in the high layer python api
9+
# to provide convenience utilities.
10+
#
11+
12+
from import_handler import oscap
13+
14+
15+
def browse_criteria(crit_node, mode=0):
16+
'''
17+
Browse recursively criteria of an oval test and build a representation of it
18+
First list item is the operator, and the next ones are the tests
19+
If the item is a list, it's a criteria (else a criterion, or string for an
20+
extended defintion , or None for OVAL_NODETYPE_UNKNOWN
21+
22+
Ex: [operator, "extended def", [operator, test_result2, test_result3]]
23+
24+
Mode : with mode = 0, test_result is the swig object oval_test_result
25+
with mode = 1, test result is a a tuple like (test_subtype, test_result)
26+
for instance (oscap.oval.OVAL_LINUX_DPKG_INFO, OVAL_FAMILY_LINUX
27+
oscap.xccdf.XCCDF_RESULT_PASS)
28+
'''
29+
30+
# init the critria list
31+
rs = list()
32+
33+
if crit_node.get_type() == oscap.oval.OVAL_NODETYPE_CRITERIA:
34+
for c in crit_node.get_subnodes():
35+
rs.append(browse_criteria(c, mode))
36+
37+
elif crit_node.get_type() == oscap.oval.OVAL_NODETYPE_CRITERION:
38+
if mode == 0:
39+
rs.append(crit_node.get_test())
40+
elif mode == 1:
41+
rs.append((crit_node.get_test().get_test().get_family(),
42+
crit_node.get_test().get_test().get_subtype(),
43+
crit_node.get_test().get_result()))
44+
else:
45+
raise ValueError("param mode in browse_criteria should be 0 or 1")
46+
47+
elif crit_node.get_type() == oscal.oval.OVAL_NODETYPE_EXTENDDEF: # !!! TODO !!!
48+
rs.append("extended def")
49+
50+
else:
51+
rs.append(None)
52+
53+
return rs

0 commit comments

Comments
 (0)