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
|
|
|
|