@@ -32,12 +32,18 @@ DEFAULT_PROFILE_SUFFIX = "_customized"
32
32
DEFAULT_REVERSE_DNS = "org.ssgproject.content"
33
33
ROLES = ["full" , "unscored" , "unchecked" ]
34
34
SEVERITIES = ["unknown" , "info" , "low" , "medium" , "high" ]
35
+ ATTRIBUTES = ["role" , "severity" ]
35
36
36
37
37
38
def quote (string ):
38
39
return "\" " + string + "\" "
39
40
40
41
42
+ def assignment_to_tuple (assignment ):
43
+ varname , value = assignment .split ("=" , 1 )
44
+ return varname , value
45
+
46
+
41
47
def is_valid_xccdf_id (string ):
42
48
return re .match (
43
49
r"^xccdf_[a-zA-Z0-9.-]+_(benchmark|profile|rule|group|value|"
@@ -59,6 +65,7 @@ class Tailoring:
59
65
self .rules_to_select = []
60
66
self .rules_to_unselect = []
61
67
self ._rule_refinements = collections .defaultdict (dict )
68
+ self ._value_refinements = collections .defaultdict (dict )
62
69
63
70
@property
64
71
def profile_id (self ):
@@ -75,7 +82,7 @@ class Tailoring:
75
82
return self ._rule_refinements [rule_id ][attribute ]
76
83
77
84
@staticmethod
78
- def _find_enumeration (attribute ):
85
+ def _find_rule_enumeration (attribute ):
79
86
if attribute == "role" :
80
87
enumeration = ROLES
81
88
elif attribute == "severity" :
@@ -90,46 +97,81 @@ class Tailoring:
90
97
if not is_valid_xccdf_id (rule_id ):
91
98
msg = f"Rule id '{ rule_id } ' is invalid!"
92
99
raise ValueError (msg )
93
- enumeration = Tailoring ._find_enumeration (attribute )
100
+ enumeration = Tailoring ._find_rule_enumeration (attribute )
94
101
if value in enumeration :
95
102
return
96
103
allowed = ", " .join (map (quote , enumeration ))
97
- msg = (
98
- f"Can't refine { attribute } of rule '{ rule_id } ' to '{ value } '. "
99
- f"Allowed { attribute } values are: { allowed } ." )
104
+ msg = (f"Can't refine { attribute } of rule '{ rule_id } ' to '{ value } '. "
105
+ f"Allowed { attribute } values are: { allowed } ." )
106
+ raise ValueError (msg )
107
+
108
+ @staticmethod
109
+ def _validate_value_refinement_params (value_id , attribute , value ):
110
+ if not is_valid_xccdf_id (value_id ):
111
+ msg = f"Value id '{ value_id } ' is invalid!"
112
+ raise ValueError (msg )
113
+ if attribute == 'selector' :
114
+ return
115
+ msg = (f"Can't refine { attribute } of value '{ value_id } ' to '{ value } '. "
116
+ f"Unsupported refine-rule attribute { attribute } ." )
100
117
raise ValueError (msg )
101
118
102
119
def _prevent_duplicate_rule_refinement (self , attribute , rule_id , value ):
103
120
refinements = self ._rule_refinements [rule_id ]
104
121
if attribute not in refinements :
105
122
return
106
123
current = refinements [attribute ]
107
- msg = (
108
- f"Can't refine { attribute } of rule '{ rule_id } ' to '{ value } '. "
109
- f"This rule { attribute } is already refined to '{ current } '." )
124
+ msg = (f"Can't refine { attribute } of rule '{ rule_id } ' to '{ value } '. "
125
+ f"This rule { attribute } is already refined to '{ current } '." )
126
+ raise ValueError (msg )
127
+
128
+ def _prevent_duplicate_value_refinement (self , attribute , value_id , value ):
129
+ refinements = self ._value_refinements [value_id ]
130
+ if attribute not in refinements :
131
+ return
132
+ current = refinements [attribute ]
133
+ msg = (f"Can't refine { attribute } of value '{ value_id } ' to '{ value } '. "
134
+ f"This value { attribute } is already refined to '{ current } '." )
110
135
raise ValueError (msg )
111
136
112
137
def refine_rule (self , rule_id , attribute , value ):
113
138
Tailoring ._validate_rule_refinement_params (rule_id , attribute , value )
114
139
self ._prevent_duplicate_rule_refinement (attribute , rule_id , value )
115
140
self ._rule_refinements [rule_id ][attribute ] = value
116
141
117
- def change_attributes (self , assignements , attribute ):
118
- for change in assignements :
142
+ def refine_value (self , value_id , attribute , value ):
143
+ Tailoring ._validate_value_refinement_params (value_id , attribute , value )
144
+ self ._prevent_duplicate_value_refinement (attribute , value_id , value )
145
+ self ._value_refinements [value_id ][attribute ] = value
146
+
147
+ def change_rule_attribute (self , rule_id , attribute , value ):
148
+ full_rule_id = self ._full_rule_id (rule_id )
149
+ self .refine_rule (full_rule_id , attribute , value )
150
+
151
+ def change_value_attribute (self , var_id , attribute , value ):
152
+ full_value_id = self ._full_var_id (var_id )
153
+ self .refine_value (full_value_id , attribute , value )
154
+
155
+ def change_rules_attributes (self , assignments , attribute ):
156
+ for change in assignments :
119
157
rule_id , value = assignment_to_tuple (change )
120
- full_rule_id = self ._full_rule_id (rule_id )
121
- self .refine_rule (full_rule_id , attribute , value )
158
+ self .change_rule_attribute (rule_id , attribute , value )
122
159
123
- def change_roles (self , assignements ):
124
- self .change_attributes ( assignements , "role" )
160
+ def change_roles (self , assignments ):
161
+ self .change_rules_attributes ( assignments , "role" )
125
162
126
- def change_severities (self , assignements ):
127
- self .change_attributes ( assignements , "severity" )
163
+ def change_severities (self , assignments ):
164
+ self .change_rules_attributes ( assignments , "severity" )
128
165
129
- def change_values (self , assignements ):
130
- for change in assignements :
166
+ def change_values (self , assignments ):
167
+ for change in assignments :
131
168
varname , value = assignment_to_tuple (change )
132
- t .add_value_change (varname , value )
169
+ self .add_value_change (varname , value )
170
+
171
+ def change_selectors (self , assignments ):
172
+ for change in assignments :
173
+ varname , selector = assignment_to_tuple (change )
174
+ self .change_value_attribute (varname , "selector" , selector )
133
175
134
176
def _full_id (self , string , el_type ):
135
177
if is_valid_xccdf_id (string ):
@@ -159,7 +201,7 @@ class Tailoring:
159
201
change .set ("idref" , self ._full_rule_id (rule_id ))
160
202
change .set ("selected" , "false" )
161
203
162
- def _add_value_selections (self , container_element ):
204
+ def _add_value_overrides (self , container_element ):
163
205
for varname , value in self .value_changes :
164
206
change = ET .SubElement (container_element , "{%s}set-value" % NS )
165
207
change .set ("idref" , self ._full_var_id (varname ))
@@ -172,6 +214,13 @@ class Tailoring:
172
214
for attr , val in refinements .items ():
173
215
ref_rule_el .set (attr , val )
174
216
217
+ def value_refinements_to_xml (self , profile_el ):
218
+ for value_id , refinements in self ._value_refinements .items ():
219
+ ref_value_el = ET .SubElement (profile_el , "{%s}refine-value" % NS )
220
+ ref_value_el .set ("idref" , value_id )
221
+ for attr , val in refinements .items ():
222
+ ref_value_el .set (attr , val )
223
+
175
224
def to_xml (self , location = None ):
176
225
root = ET .Element ("{%s}Tailoring" % NS )
177
226
root .set ("id" , self .id )
@@ -198,27 +247,66 @@ class Tailoring:
198
247
title .text = self .profile_title
199
248
200
249
self ._add_rule_select_operations (profile )
201
- self ._add_value_selections (profile )
250
+ self ._add_value_overrides (profile )
202
251
self .rule_refinements_to_xml (profile )
252
+ self .value_refinements_to_xml (profile )
203
253
204
254
root_str = ET .tostring (root )
205
255
pretty_xml = xml .dom .minidom .parseString (root_str ).toprettyxml ()
206
256
with open (location , "w" ) if location != "-" else sys .stdout as f :
207
257
f .write (pretty_xml )
208
258
259
+ def import_json_tailoring (self , json_tailoring ):
260
+ import json
261
+ with open (json_tailoring , "r" ) as jf :
262
+ all_tailorings = json .load (jf )
263
+
264
+ if 'profiles' in all_tailorings and all_tailorings ['profiles' ]:
265
+ # We currently support tailoring of one profile only
266
+ tailoring = all_tailorings ['profiles' ][0 ]
267
+ else :
268
+ raise ValueError ("JSON Tailoring does not define any profiles." )
269
+
270
+ self .extends = tailoring ["base_profile_id" ]
271
+
272
+ self .profile_id = tailoring .get ("id" , self .profile_id )
273
+ self .profile_title = tailoring .get ("title" , self .profile_title )
209
274
210
- def parse_args ():
275
+ if "rules" in tailoring :
276
+ for rule_id , props in tailoring ["rules" ].items ():
277
+ if "evaluate" in props :
278
+ if props ["evaluate" ]:
279
+ self .rules_to_select .append (rule_id )
280
+ else :
281
+ self .rules_to_unselect .append (rule_id )
282
+ for attr in ATTRIBUTES :
283
+ if attr in props :
284
+ self .change_rule_attribute (rule_id , attr , props [attr ])
285
+
286
+ if "variables" in tailoring :
287
+ for variable_id , props in tailoring ["variables" ].items ():
288
+ if "value" in props :
289
+ self .add_value_change (variable_id , props ["value" ])
290
+ if "option_id" in props :
291
+ self .change_value_attribute (variable_id , "selector" , props ["option_id" ])
292
+
293
+
294
+ def get_parser ():
211
295
parser = argparse .ArgumentParser (
212
296
description = "This script produces XCCDF 1.2 tailoring files "
213
297
"to be used by SCAP scanners and SCAP data streams." )
214
298
parser .add_argument (
215
299
"datastream" , metavar = "DS_FILENAME" ,
216
300
help = "The tailored data stream filename." )
217
301
parser .add_argument (
218
- "profile" , metavar = "BASE_PROFILE_ID" ,
302
+ "profile" , metavar = "BASE_PROFILE_ID" , nargs = '?' , default = "" ,
219
303
help = "Specify ID of the base profile. ID of the profile can be "
220
304
"either its full ID, or the suffix, in which case the "
221
305
"'xccdf_<id-namespace>_profile' prefix will be prepended internally." )
306
+ parser .add_argument (
307
+ "--json-tailoring" , metavar = "JSON_TAILORING_FILENAME" , default = "" ,
308
+ help = "JSON Tailoring (https://github.com/ComplianceAsCode/schemas/blob/main/tailoring/schema.json) "
309
+ "filename." )
222
310
parser .add_argument (
223
311
"--title" , default = "" ,
224
312
help = "Title of the new profile." )
@@ -234,6 +322,13 @@ def parse_args():
234
322
"or the suffix, in which case the 'xccdf_<id-namespace>_value' prefix "
235
323
"will be prepended internally. Specify the argument multiple times "
236
324
"if needed." )
325
+ parser .add_argument (
326
+ "-V" , "--var-select" , metavar = "VAR=SELECTOR" , action = "append" , default = [],
327
+ help = "Specify refinement of the XCCDF value in form "
328
+ "<varname>=<selector>. Name of the variable can be either its full name, "
329
+ "or the suffix, in which case the 'xccdf_<id-namespace>_value' prefix "
330
+ "will be prepended internally. Specify the argument multiple times "
331
+ "if needed." )
237
332
parser .add_argument (
238
333
"-r" , "--rule-role" , metavar = "RULE=ROLE" , action = "append" , default = [],
239
334
help = "Specify refinement of the XCCDF rule role in form "
@@ -273,30 +368,37 @@ def parse_args():
273
368
"-o" , "--output" , default = "-" ,
274
369
help = "Where to save the tailoring file. If not supplied, write to "
275
370
"standard output." )
276
- args = parser .parse_args ()
277
- return args
278
-
279
-
280
- def assignment_to_tuple (assignment ):
281
- varname , value = assignment .split ("=" , 1 )
282
- return (varname , value )
371
+ return parser
283
372
284
373
285
374
if __name__ == "__main__" :
286
- args = parse_args ()
375
+ parser = get_parser ()
376
+ args = parser .parse_args ()
377
+
378
+ if not args .profile and not args .json_tailoring :
379
+ parser .error ("one of the following arguments has to be provided: "
380
+ "BASE_PROFILE_ID or --json-tailoring JSON_TAILORING_FILENAME" )
287
381
288
382
t = Tailoring ()
289
- t .reverse_dns = args .id_namespace
290
- t .extends = args .profile
291
- t .profile_id = args .new_profile_id
292
383
t .original_ds_filename = args .datastream
384
+ t .reverse_dns = args .id_namespace
385
+
386
+ if args .json_tailoring :
387
+ t .import_json_tailoring (args .json_tailoring )
388
+
389
+ if args .profile :
390
+ t .extends = args .profile
391
+ if args .new_profile_id :
392
+ t .profile_id = args .new_profile_id
393
+ if args .title :
394
+ t .profile_title = args .title
395
+
396
+ t .rules_to_select .extend (args .select )
397
+ t .rules_to_unselect .extend (args .unselect )
398
+
293
399
t .change_values (args .var_value )
400
+ t .change_selectors (args .var_select )
294
401
t .change_roles (args .rule_role )
295
402
t .change_severities (args .rule_severity )
296
403
297
- t .profile_title = args .title
298
-
299
- t .rules_to_select = args .select
300
- t .rules_to_unselect = args .unselect
301
-
302
404
t .to_xml (args .output )
0 commit comments