ssg.build_guides   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 285
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 193
dl 0
loc 285
ccs 0
cts 107
cp 0
rs 9.0399
c 0
b 0
f 0
wmc 42

10 Functions

Rating   Name   Duplication   Size   Complexity  
A get_path_args() 0 22 3
A generate_for_input_content() 0 15 3
B builder() 0 31 6
A _benchmark_profile_pair_sort_key() 0 12 5
A get_output_guide_paths() 0 20 3
A get_benchmark_profile_pairs() 0 12 4
B fill_queue() 0 46 6
A _get_guide_filename() 0 16 5
B build_index() 0 74 4
A _is_skipped_profile() 0 5 3

How to fix   Complexity   

Complexity

Complex classes like ssg.build_guides often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 + ' &#92;<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 + ' &#92;<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 + ' &#92;<br/>--profile ' + profile_id + ' &#92;<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