|
1
|
|
|
from __future__ import absolute_import |
|
2
|
|
|
from __future__ import print_function |
|
3
|
|
|
|
|
4
|
|
|
import os |
|
5
|
|
|
import sys |
|
6
|
|
|
from collections import namedtuple |
|
7
|
|
|
|
|
8
|
|
|
from .shims import subprocess_check_output, Queue |
|
9
|
|
|
from .xccdf import get_profile_choices_for_input, get_profile_short_id |
|
10
|
|
|
from .xccdf import PROFILE_ID_SKIPLIST |
|
11
|
|
|
from .constants import OSCAP_DS_STRING, OSCAP_PATH |
|
12
|
|
|
|
|
13
|
|
|
|
|
14
|
|
|
def get_path_args(args): |
|
15
|
|
|
""" |
|
16
|
|
|
Return a namedtuple of (input_path, input_basename, path_base, |
|
17
|
|
|
output_dir) from an argparse containing args.input and args.output. |
|
18
|
|
|
""" |
|
19
|
|
|
|
|
20
|
|
|
paths = namedtuple('paths', ['input_path', 'input_basename', |
|
21
|
|
|
'path_base', 'output_dir']) |
|
22
|
|
|
|
|
23
|
|
|
input_path = os.path.abspath(args.input) |
|
24
|
|
|
|
|
25
|
|
|
output_dir = os.path.abspath(args.output) |
|
26
|
|
|
input_basename = os.path.basename(input_path) |
|
27
|
|
|
|
|
28
|
|
|
path_base, _ = os.path.splitext(input_basename) |
|
29
|
|
|
# avoid -ds and -xccdf suffices in guide filenames |
|
30
|
|
|
if path_base.endswith("-ds"): |
|
31
|
|
|
path_base = path_base[:-3] |
|
32
|
|
|
elif path_base.endswith("-xccdf"): |
|
33
|
|
|
path_base = path_base[:-6] |
|
34
|
|
|
|
|
35
|
|
|
return paths(input_path, input_basename, path_base, output_dir) |
|
36
|
|
|
|
|
37
|
|
|
|
|
38
|
|
|
def generate_for_input_content(input_content, benchmark_id, profile_id): |
|
39
|
|
|
""" |
|
40
|
|
|
Returns HTML guide for given input_content and profile_id |
|
41
|
|
|
combination. This function assumes only one Benchmark exists |
|
42
|
|
|
in given input_content! |
|
43
|
|
|
""" |
|
44
|
|
|
|
|
45
|
|
|
args = [OSCAP_PATH, "xccdf", "generate", "guide"] |
|
46
|
|
|
if benchmark_id != "": |
|
47
|
|
|
args.extend(["--benchmark-id", benchmark_id]) |
|
48
|
|
|
if profile_id != "": |
|
49
|
|
|
args.extend(["--profile", profile_id]) |
|
50
|
|
|
args.append(input_content) |
|
51
|
|
|
|
|
52
|
|
|
return subprocess_check_output(args).decode("utf-8") |
|
53
|
|
|
|
|
54
|
|
|
|
|
55
|
|
|
def builder(queue): |
|
56
|
|
|
""" |
|
57
|
|
|
Fetch from a queue of tasks, process tasks until the queue is empty. |
|
58
|
|
|
Each task is processed with generate_for_input_content, and the |
|
59
|
|
|
guide is written as output. |
|
60
|
|
|
|
|
61
|
|
|
Raises: when an error occurred when processing a task. |
|
62
|
|
|
""" |
|
63
|
|
|
while True: |
|
64
|
|
|
try: |
|
65
|
|
|
benchmark_id, profile_id, input_path, guide_path = \ |
|
66
|
|
|
queue.get(False) |
|
67
|
|
|
|
|
68
|
|
|
guide_html = generate_for_input_content( |
|
69
|
|
|
input_path, benchmark_id, profile_id |
|
70
|
|
|
) |
|
71
|
|
|
|
|
72
|
|
|
with open(guide_path, "wb") as guide_file: |
|
73
|
|
|
guide_file.write(guide_html.encode("utf-8")) |
|
74
|
|
|
|
|
75
|
|
|
queue.task_done() |
|
76
|
|
|
except Queue.Empty: |
|
77
|
|
|
break |
|
78
|
|
|
except Exception as error: |
|
79
|
|
|
sys.stderr.write( |
|
80
|
|
|
"Fatal error encountered when generating guide '%s'. " |
|
81
|
|
|
"Error details:\n%s\n\n" % (guide_path, error) |
|
82
|
|
|
) |
|
83
|
|
|
with queue.mutex: |
|
84
|
|
|
queue.queue.clear() |
|
85
|
|
|
raise error |
|
86
|
|
|
|
|
87
|
|
|
|
|
88
|
|
|
def _benchmark_profile_pair_sort_key(benchmark_id, profile_id, profile_title): |
|
89
|
|
|
# The "base" benchmarks come first |
|
90
|
|
|
if (benchmark_id.endswith("_RHEL-7") or |
|
91
|
|
|
benchmark_id.endswith("_RHEL-6") or |
|
92
|
|
|
benchmark_id.endswith("_RHEL-5")): |
|
93
|
|
|
benchmark_id = "AAA" + benchmark_id |
|
94
|
|
|
|
|
95
|
|
|
# The default profile comes last |
|
96
|
|
|
if not profile_id: |
|
97
|
|
|
profile_title = "zzz(default)" |
|
98
|
|
|
|
|
99
|
|
|
return (benchmark_id, profile_title) |
|
100
|
|
|
|
|
101
|
|
|
|
|
102
|
|
|
def get_benchmark_profile_pairs(input_tree, benchmarks): |
|
103
|
|
|
benchmark_profile_pairs = [] |
|
104
|
|
|
|
|
105
|
|
|
for benchmark_id in benchmarks.keys(): |
|
106
|
|
|
profiles = get_profile_choices_for_input(input_tree, benchmark_id, |
|
107
|
|
|
None) |
|
108
|
|
|
for profile_id in profiles: |
|
109
|
|
|
pair = (benchmark_id, profile_id, profiles[profile_id]) |
|
110
|
|
|
benchmark_profile_pairs.append(pair) |
|
111
|
|
|
|
|
112
|
|
|
return sorted(benchmark_profile_pairs, key=lambda x: |
|
113
|
|
|
_benchmark_profile_pair_sort_key(x[0], x[1], x[2])) |
|
114
|
|
|
|
|
115
|
|
|
|
|
116
|
|
|
def _is_skipped_profile(profile_id): |
|
117
|
|
|
for skipped_id in PROFILE_ID_SKIPLIST: |
|
118
|
|
|
if profile_id.endswith(skipped_id): |
|
119
|
|
|
return True |
|
120
|
|
|
return False |
|
121
|
|
|
|
|
122
|
|
|
|
|
123
|
|
|
def _get_guide_filename(path_base, profile_id, benchmark_id, benchmarks): |
|
124
|
|
|
profile_id_for_path = "default" if not profile_id else profile_id |
|
125
|
|
|
benchmark_id_for_path = benchmark_id |
|
126
|
|
|
if benchmark_id_for_path.startswith(OSCAP_DS_STRING): |
|
127
|
|
|
benchmark_id_for_path = \ |
|
128
|
|
|
benchmark_id_for_path[len(OSCAP_DS_STRING):] |
|
129
|
|
|
|
|
130
|
|
|
if len(benchmarks) == 1 or len(benchmark_id_for_path) == len("RHEL-X"): |
|
131
|
|
|
# treat the base RHEL benchmark as a special case to preserve |
|
132
|
|
|
# old guide paths and old URLs that people may be relying on |
|
133
|
|
|
return "%s-guide-%s.html" % (path_base, |
|
134
|
|
|
get_profile_short_id(profile_id_for_path)) |
|
135
|
|
|
|
|
136
|
|
|
return "%s-%s-guide-%s.html" % \ |
|
137
|
|
|
(path_base, benchmark_id_for_path, |
|
138
|
|
|
get_profile_short_id(profile_id_for_path)) |
|
139
|
|
|
|
|
140
|
|
|
|
|
141
|
|
|
def get_output_guide_paths(benchmarks, benchmark_profile_pairs, path_base, |
|
142
|
|
|
output_dir): |
|
143
|
|
|
""" |
|
144
|
|
|
Return a list of guide paths containing guides for each non-skipped |
|
145
|
|
|
profile_id in a benchmark. |
|
146
|
|
|
""" |
|
147
|
|
|
|
|
148
|
|
|
guide_paths = [] |
|
149
|
|
|
|
|
150
|
|
|
for benchmark_id, profile_id, _ in benchmark_profile_pairs: |
|
151
|
|
|
if _is_skipped_profile(profile_id): |
|
152
|
|
|
continue |
|
153
|
|
|
|
|
154
|
|
|
guide_filename = _get_guide_filename(path_base, profile_id, |
|
155
|
|
|
benchmark_id, benchmarks) |
|
156
|
|
|
guide_path = os.path.join(output_dir, guide_filename) |
|
157
|
|
|
|
|
158
|
|
|
guide_paths.append(guide_path) |
|
159
|
|
|
|
|
160
|
|
|
return guide_paths |
|
161
|
|
|
|
|
162
|
|
|
|
|
163
|
|
|
def fill_queue(benchmarks, benchmark_profile_pairs, input_path, path_base, |
|
164
|
|
|
output_dir): |
|
165
|
|
|
""" |
|
166
|
|
|
For each benchmark and profile in the benchmark, create a queue of |
|
167
|
|
|
tasks for later processing. A task is a named tuple (benchmark_id, |
|
168
|
|
|
profile_id, input_path, guide_path). |
|
169
|
|
|
|
|
170
|
|
|
Returns: queue of tasks. |
|
171
|
|
|
""" |
|
172
|
|
|
|
|
173
|
|
|
index_links = [] |
|
174
|
|
|
index_options = {} |
|
175
|
|
|
index_initial_src = None |
|
176
|
|
|
queue = Queue.Queue() |
|
177
|
|
|
|
|
178
|
|
|
task = namedtuple('task', ['benchmark_id', 'profile_id', 'input_path', 'guide_path']) |
|
179
|
|
|
|
|
180
|
|
|
for benchmark_id, profile_id, profile_title in benchmark_profile_pairs: |
|
181
|
|
|
if _is_skipped_profile(profile_id): |
|
182
|
|
|
continue |
|
183
|
|
|
|
|
184
|
|
|
guide_filename = _get_guide_filename(path_base, profile_id, |
|
185
|
|
|
benchmark_id, benchmarks) |
|
186
|
|
|
guide_path = os.path.join(output_dir, guide_filename) |
|
187
|
|
|
|
|
188
|
|
|
index_links.append( |
|
189
|
|
|
"<a target=\"guide\" href=\"%s\">%s</a>" % |
|
190
|
|
|
(guide_filename, "%s in %s" % (profile_title, benchmark_id)) |
|
191
|
|
|
) |
|
192
|
|
|
|
|
193
|
|
|
if benchmark_id not in index_options: |
|
194
|
|
|
index_options[benchmark_id] = [] |
|
195
|
|
|
|
|
196
|
|
|
index_options[benchmark_id].append( |
|
197
|
|
|
"<option value=\"%s\" data-benchmark-id=\"%s\" data-profile-id=\"%s\">%s</option>" % |
|
198
|
|
|
(guide_filename, |
|
199
|
|
|
"" if len(benchmarks) == 1 else benchmark_id, profile_id, |
|
200
|
|
|
profile_title) |
|
201
|
|
|
) |
|
202
|
|
|
|
|
203
|
|
|
if index_initial_src is None: |
|
204
|
|
|
index_initial_src = guide_filename |
|
205
|
|
|
|
|
206
|
|
|
queue.put(task(benchmark_id, profile_id, input_path, guide_path)) |
|
207
|
|
|
|
|
208
|
|
|
return index_links, index_options, index_initial_src, queue |
|
209
|
|
|
|
|
210
|
|
|
|
|
211
|
|
|
def build_index(benchmarks, input_basename, index_links, index_options, |
|
212
|
|
|
index_initial_src): |
|
213
|
|
|
index_select_options = "" |
|
214
|
|
|
if len(index_options.keys()) > 1: |
|
215
|
|
|
# we sort by length of the benchmark_id to make sure the "default" |
|
216
|
|
|
# comes up first in the list |
|
217
|
|
|
for benchmark_id in sorted(index_options.keys(), |
|
218
|
|
|
key=lambda val: (len(val), val)): |
|
219
|
|
|
index_select_options += "<optgroup label=\"benchmark: %s\">\n" \ |
|
220
|
|
|
% (benchmark_id) |
|
221
|
|
|
index_select_options += "\n".join(index_options[benchmark_id]) |
|
222
|
|
|
index_select_options += "</optgroup>\n" |
|
223
|
|
|
else: |
|
224
|
|
|
index_select_options += "\n".join(list(index_options.values())[0]) |
|
225
|
|
|
|
|
226
|
|
|
return "".join([ |
|
227
|
|
|
"<!DOCTYPE html>\n", |
|
228
|
|
|
"<html lang=\"en\">\n", |
|
229
|
|
|
"\t<head>\n", |
|
230
|
|
|
"\t\t<meta charset=\"utf-8\">\n", |
|
231
|
|
|
"\t\t<title>%s</title>\n" % (list(benchmarks.values())[0]), |
|
232
|
|
|
"\t\t<script>\n", |
|
233
|
|
|
"\t\t\tfunction change_profile(option_element)\n", |
|
234
|
|
|
"\t\t\t{\n", |
|
235
|
|
|
"\t\t\t\tvar benchmark_id=option_element.getAttribute('data-benchmark-id');\n", |
|
236
|
|
|
"\t\t\t\tvar profile_id=option_element.getAttribute('data-profile-id');\n", |
|
237
|
|
|
"\t\t\t\tvar eval_snippet=document.getElementById('eval_snippet');\n", |
|
238
|
|
|
"\t\t\t\tvar input_path='/usr/share/xml/scap/ssg/content/%s';\n" % (input_basename), |
|
239
|
|
|
"\t\t\t\tif (profile_id == '')\n", |
|
240
|
|
|
"\t\t\t\t{\n", |
|
241
|
|
|
"\t\t\t\t\tif (benchmark_id == '')\n", |
|
242
|
|
|
"\t\t\t\t\t\teval_snippet.innerHTML='# oscap xccdf eval ' + input_path;\n", |
|
243
|
|
|
"\t\t\t\t\telse\n", |
|
244
|
|
|
"\t\t\t\t\t\teval_snippet.innerHTML='# oscap xccdf eval --benchmark-id ' + benchmark_id + ' \<br/>' + input_path;\n", |
|
245
|
|
|
"\t\t\t\t}\n", |
|
246
|
|
|
"\t\t\t\telse\n", |
|
247
|
|
|
"\t\t\t\t{\n", |
|
248
|
|
|
"\t\t\t\t\tif (benchmark_id == '')\n", |
|
249
|
|
|
"\t\t\t\t\t\teval_snippet.innerHTML='# oscap xccdf eval --profile ' + profile_id + ' \<br/>' + input_path;\n", |
|
250
|
|
|
"\t\t\t\t\telse\n", |
|
251
|
|
|
"\t\t\t\t\t\teval_snippet.innerHTML='# oscap xccdf eval --benchmark-id ' + benchmark_id + ' \<br/>--profile ' + profile_id + ' \<br/>' + input_path;\n", |
|
252
|
|
|
"\t\t\t\t}\n", |
|
253
|
|
|
"\t\t\t\twindow.open(option_element.value, 'guide');\n", |
|
254
|
|
|
"\t\t\t}\n", |
|
255
|
|
|
"\t\t</script>\n", |
|
256
|
|
|
"\t\t<style>\n", |
|
257
|
|
|
"\t\t\thtml, body { margin: 0; height: 100% }\n", |
|
258
|
|
|
"\t\t\t#js_switcher { position: fixed; right: 30px; top: 10px; padding: 2px; background: #ddd; border: 1px solid #999 }\n", |
|
259
|
|
|
"\t\t\t#guide_div { margin: auto; width: 99%; height: 99% }\n", |
|
260
|
|
|
"\t\t</style>\n", |
|
261
|
|
|
"\t</head>\n", |
|
262
|
|
|
"\t<body onload=\"document.getElementById('js_switcher').style.display = 'block'\">\n", |
|
263
|
|
|
"\t\t<noscript>\n", |
|
264
|
|
|
"Profiles: ", |
|
265
|
|
|
", ".join(index_links) + "\n", |
|
266
|
|
|
"\t\t</noscript>\n", |
|
267
|
|
|
"\t\t<div id=\"js_switcher\" style=\"display: none\">\n", |
|
268
|
|
|
"\t\t\tProfile: \n", |
|
269
|
|
|
"\t\t\t<select style=\"margin-bottom: 5px\" ", |
|
270
|
|
|
"onchange=\"change_profile(this.options[this.selectedIndex]);\"", |
|
271
|
|
|
">\n", |
|
272
|
|
|
"\n", index_select_options, "\n", |
|
273
|
|
|
"\t\t\t</select>\n", |
|
274
|
|
|
"\t\t\t<div id='eval_snippet' style='background: #eee; padding: 3px; border: 1px solid #000'>", |
|
275
|
|
|
"select a profile to display its guide and a command line snippet needed to use it", |
|
276
|
|
|
"</div>\n", |
|
277
|
|
|
"\t\t</div>\n", |
|
278
|
|
|
"\t\t<div id=\"guide_div\">\n", |
|
279
|
|
|
"\t\t\t<iframe src=\"%s\" name=\"guide\" " % (index_initial_src), |
|
280
|
|
|
"width=\"100%\" height=\"100%\">\n", |
|
281
|
|
|
"\t\t\t</iframe>\n", |
|
282
|
|
|
"\t\t</div>\n", |
|
283
|
|
|
"\t</body>\n", |
|
284
|
|
|
"</html>\n" |
|
285
|
|
|
]) |
|
286
|
|
|
|