|
1
|
|
|
from __future__ import absolute_import |
|
2
|
|
|
from __future__ import print_function |
|
3
|
|
|
|
|
4
|
|
|
import os |
|
5
|
|
|
import sys |
|
6
|
|
|
import imp |
|
7
|
|
|
import glob |
|
8
|
|
|
|
|
9
|
|
|
import ssg.build_yaml |
|
10
|
|
|
import ssg.utils |
|
11
|
|
|
import ssg.yaml |
|
12
|
|
|
|
|
13
|
|
|
try: |
|
14
|
|
|
from urllib.parse import quote |
|
15
|
|
|
except ImportError: |
|
16
|
|
|
from urllib import quote |
|
17
|
|
|
|
|
18
|
|
|
languages = ["anaconda", "ansible", "bash", "oval", "puppet", "ignition", "kubernetes"] |
|
19
|
|
|
preprocessing_file_name = "template.py" |
|
20
|
|
|
lang_to_ext_map = { |
|
21
|
|
|
"anaconda": ".anaconda", |
|
22
|
|
|
"ansible": ".yml", |
|
23
|
|
|
"bash": ".sh", |
|
24
|
|
|
"oval": ".xml", |
|
25
|
|
|
"puppet": ".pp", |
|
26
|
|
|
"ignition": ".yml", |
|
27
|
|
|
"kubernetes": ".yml" |
|
28
|
|
|
} |
|
29
|
|
|
|
|
30
|
|
|
|
|
31
|
|
|
templates = dict() |
|
32
|
|
|
|
|
33
|
|
|
|
|
34
|
|
|
class Template(): |
|
35
|
|
|
def __init__(self, template_root_directory, name): |
|
36
|
|
|
self.template_root_directory = template_root_directory |
|
37
|
|
|
self.name = name |
|
38
|
|
|
self.template_path = os.path.join(self.template_root_directory, self.name) |
|
39
|
|
|
self.template_yaml_path = os.path.join(self.template_path, "template.yml") |
|
40
|
|
|
self.preprocessing_file_path = os.path.join(self.template_path, preprocessing_file_name) |
|
41
|
|
|
|
|
42
|
|
|
def load(self): |
|
43
|
|
|
if not os.path.exists(self.preprocessing_file_path): |
|
44
|
|
|
self.preprocessing_file_path = None |
|
45
|
|
|
self.langs = [] |
|
46
|
|
|
template_yaml = ssg.yaml.open_raw(self.template_yaml_path) |
|
47
|
|
|
for lang in template_yaml["supported_languages"]: |
|
48
|
|
|
if lang not in languages: |
|
49
|
|
|
raise ValueError("The template {0} declares to support the {1} language," |
|
50
|
|
|
"but this language is not supported by the content.".format(self.name, lang)) |
|
51
|
|
|
langfilename = lang + ".template" |
|
52
|
|
|
if not os.path.exists(os.path.join(self.template_path, langfilename)): |
|
53
|
|
|
raise ValueError("The template {0} declares to support the {1} language," |
|
54
|
|
|
"but the implementation file is missing.".format(self.name, lang)) |
|
55
|
|
|
self.langs.append(lang) |
|
56
|
|
|
|
|
57
|
|
|
def preprocess(self, parameters, lang): |
|
58
|
|
|
# if no template.py file exists, skip this preprocessing part |
|
59
|
|
|
if self.preprocessing_file_path is not None: |
|
60
|
|
|
module_name = "tmpmod" # dummy module name, we don't need it later |
|
61
|
|
|
preprocess_mod = imp.load_source(module_name, self.preprocessing_file_path) |
|
62
|
|
|
parameters = preprocess_mod.preprocess(parameters.copy(), lang) |
|
63
|
|
|
# TODO: Remove this right after the variables in templates are renamed |
|
64
|
|
|
# to lowercase |
|
65
|
|
|
uppercases = dict() |
|
66
|
|
|
for k, v in parameters.items(): |
|
67
|
|
|
uppercases[k.upper()] = v |
|
68
|
|
|
return uppercases |
|
69
|
|
|
|
|
70
|
|
|
def looks_like_template(self): |
|
71
|
|
|
if not os.path.isdir(self.template_root_directory): |
|
72
|
|
|
return False |
|
73
|
|
|
if os.path.islink(self.template_root_directory): |
|
74
|
|
|
return False |
|
75
|
|
|
template_sources = glob.glob(os.path.join(self.template_path, "*.template")) |
|
76
|
|
|
if not os.path.isfile(self.template_yaml_path) and not template_sources: |
|
77
|
|
|
return False |
|
78
|
|
|
return True |
|
79
|
|
|
|
|
80
|
|
|
|
|
81
|
|
|
class Builder(object): |
|
82
|
|
|
""" |
|
83
|
|
|
Class for building all templated content for a given product. |
|
84
|
|
|
|
|
85
|
|
|
To generate content from templates, pass the env_yaml, path to the |
|
86
|
|
|
directory with resolved rule YAMLs, path to the directory that contains |
|
87
|
|
|
templates, path to the output directory for checks and a path to the |
|
88
|
|
|
output directory for remediations into the constructor. Then, call the |
|
89
|
|
|
method build() to perform a build. |
|
90
|
|
|
""" |
|
91
|
|
|
def __init__( |
|
92
|
|
|
self, env_yaml, resolved_rules_dir, templates_dir, |
|
93
|
|
|
remediations_dir, checks_dir): |
|
94
|
|
|
self.env_yaml = env_yaml |
|
95
|
|
|
self.resolved_rules_dir = resolved_rules_dir |
|
96
|
|
|
self.templates_dir = templates_dir |
|
97
|
|
|
self.remediations_dir = remediations_dir |
|
98
|
|
|
self.checks_dir = checks_dir |
|
99
|
|
|
self.output_dirs = dict() |
|
100
|
|
|
for lang in languages: |
|
101
|
|
|
if lang == "oval": |
|
102
|
|
|
# OVAL checks need to be put to a different directory because |
|
103
|
|
|
# they are processed differently than remediations later in the |
|
104
|
|
|
# build process |
|
105
|
|
|
output_dir = self.checks_dir |
|
106
|
|
|
else: |
|
107
|
|
|
output_dir = self.remediations_dir |
|
108
|
|
|
dir_ = os.path.join(output_dir, lang) |
|
109
|
|
|
self.output_dirs[lang] = dir_ |
|
110
|
|
|
# scan directory structure and dynamically create list of templates |
|
111
|
|
|
for item in os.listdir(self.templates_dir): |
|
112
|
|
|
itempath = os.path.join(self.templates_dir, item) |
|
113
|
|
|
maybe_template = Template(templates_dir, item) |
|
114
|
|
|
if maybe_template.looks_like_template(): |
|
115
|
|
|
maybe_template.load() |
|
116
|
|
|
templates[item] = maybe_template |
|
117
|
|
|
|
|
118
|
|
|
|
|
119
|
|
|
def build_lang( |
|
120
|
|
|
self, rule_id, template_name, template_vars, lang, local_env_yaml): |
|
121
|
|
|
""" |
|
122
|
|
|
Builds templated content for a given rule for a given language. |
|
123
|
|
|
Writes the output to the correct build directories. |
|
124
|
|
|
""" |
|
125
|
|
|
if lang not in templates[template_name].langs: |
|
126
|
|
|
return |
|
127
|
|
|
template_file_name = lang + ".template" |
|
128
|
|
|
template_file_path = os.path.join(self.templates_dir, template_name, template_file_name) |
|
129
|
|
|
ext = lang_to_ext_map[lang] |
|
130
|
|
|
output_file_name = rule_id + ext |
|
131
|
|
|
output_filepath = os.path.join( |
|
132
|
|
|
self.output_dirs[lang], output_file_name) |
|
133
|
|
|
template_parameters = templates[template_name].preprocess(template_vars, lang) |
|
134
|
|
|
jinja_dict = ssg.utils.merge_dicts(local_env_yaml, template_parameters) |
|
135
|
|
|
filled_template = ssg.jinja.process_file_with_macros( |
|
136
|
|
|
template_file_path, jinja_dict) |
|
137
|
|
|
with open(output_filepath, "w") as f: |
|
138
|
|
|
f.write(filled_template) |
|
139
|
|
|
|
|
140
|
|
|
def get_langs_to_generate(self, rule): |
|
141
|
|
|
""" |
|
142
|
|
|
For a given rule returns list of languages that should be generated |
|
143
|
|
|
from templates. This is controlled by "template_backends" in rule.yml. |
|
144
|
|
|
""" |
|
145
|
|
|
if "backends" in rule.template: |
|
146
|
|
|
backends = rule.template["backends"] |
|
147
|
|
|
for lang in backends: |
|
148
|
|
|
if lang not in languages: |
|
149
|
|
|
raise RuntimeError( |
|
150
|
|
|
"Rule {0} wants to generate unknown language '{1}" |
|
151
|
|
|
"from a template.".format(rule.id_, lang) |
|
152
|
|
|
) |
|
153
|
|
|
langs_to_generate = [] |
|
154
|
|
|
for lang in languages: |
|
155
|
|
|
backend = backends.get(lang, "on") |
|
156
|
|
|
if backend == "on": |
|
157
|
|
|
langs_to_generate.append(lang) |
|
158
|
|
|
return langs_to_generate |
|
159
|
|
|
else: |
|
160
|
|
|
return languages |
|
161
|
|
|
|
|
162
|
|
|
def build_rule(self, rule_id, rule_title, template, langs_to_generate): |
|
163
|
|
|
""" |
|
164
|
|
|
Builds templated content for a given rule for selected languages, |
|
165
|
|
|
writing the output to the correct build directories. |
|
166
|
|
|
""" |
|
167
|
|
|
try: |
|
168
|
|
|
template_name = template["name"] |
|
169
|
|
|
except KeyError: |
|
170
|
|
|
raise ValueError( |
|
171
|
|
|
"Rule {0} is missing template name under template key".format( |
|
172
|
|
|
rule_id)) |
|
173
|
|
|
if template_name not in templates.keys(): |
|
174
|
|
|
raise ValueError( |
|
175
|
|
|
"Rule {0} uses template {1} which does not exist.".format( |
|
176
|
|
|
rule_id, template_name)) |
|
177
|
|
|
try: |
|
178
|
|
|
template_vars = template["vars"] |
|
179
|
|
|
except KeyError: |
|
180
|
|
|
raise ValueError( |
|
181
|
|
|
"Rule {0} does not contain mandatory 'vars:' key under " |
|
182
|
|
|
"'template:' key.".format(rule_id)) |
|
183
|
|
|
# Add the rule ID which will be reused in OVAL templates as OVAL |
|
184
|
|
|
# definition ID so that the build system matches the generated |
|
185
|
|
|
# check with the rule. |
|
186
|
|
|
template_vars["_rule_id"] = rule_id |
|
187
|
|
|
# checks and remediations are processed with a custom YAML dict |
|
188
|
|
|
local_env_yaml = self.env_yaml.copy() |
|
189
|
|
|
local_env_yaml["rule_id"] = rule_id |
|
190
|
|
|
local_env_yaml["rule_title"] = rule_title |
|
191
|
|
|
local_env_yaml["products"] = self.env_yaml["product"] |
|
192
|
|
|
for lang in langs_to_generate: |
|
193
|
|
|
self.build_lang( |
|
194
|
|
|
rule_id, template_name, template_vars, lang, local_env_yaml) |
|
195
|
|
|
|
|
196
|
|
|
def build_extra_ovals(self): |
|
197
|
|
|
declaration_path = os.path.join(self.templates_dir, "extra_ovals.yml") |
|
198
|
|
|
declaration = ssg.yaml.open_raw(declaration_path) |
|
199
|
|
|
for oval_def_id, template in declaration.items(): |
|
200
|
|
|
langs_to_generate = ["oval"] |
|
201
|
|
|
# Since OVAL definition ID in shorthand format is always the same |
|
202
|
|
|
# as rule ID, we can use it instead of the rule ID even if no rule |
|
203
|
|
|
# with that ID exists |
|
204
|
|
|
self.build_rule( |
|
205
|
|
|
oval_def_id, oval_def_id, template, langs_to_generate) |
|
206
|
|
|
|
|
207
|
|
|
def build_all_rules(self): |
|
208
|
|
|
for rule_file in os.listdir(self.resolved_rules_dir): |
|
209
|
|
|
rule_path = os.path.join(self.resolved_rules_dir, rule_file) |
|
210
|
|
|
try: |
|
211
|
|
|
rule = ssg.build_yaml.Rule.from_yaml(rule_path, self.env_yaml) |
|
212
|
|
|
except ssg.build_yaml.DocumentationNotComplete: |
|
213
|
|
|
# Happens on non-debug build when a rule is "documentation-incomplete" |
|
214
|
|
|
continue |
|
215
|
|
|
if rule.template is None: |
|
216
|
|
|
# rule is not templated, skipping |
|
217
|
|
|
continue |
|
218
|
|
|
langs_to_generate = self.get_langs_to_generate(rule) |
|
219
|
|
|
self.build_rule( |
|
220
|
|
|
rule.id_, rule.title, rule.template, langs_to_generate) |
|
221
|
|
|
|
|
222
|
|
|
def build(self): |
|
223
|
|
|
""" |
|
224
|
|
|
Builds all templated content for all languages, writing |
|
225
|
|
|
the output to the correct build directories. |
|
226
|
|
|
""" |
|
227
|
|
|
|
|
228
|
|
|
for dir_ in self.output_dirs.values(): |
|
229
|
|
|
if not os.path.exists(dir_): |
|
230
|
|
|
os.makedirs(dir_) |
|
231
|
|
|
|
|
232
|
|
|
self.build_extra_ovals() |
|
233
|
|
|
self.build_all_rules() |
|
234
|
|
|
|