Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4112838

Browse files
committedFeb 26, 2024
Introduce JSON tailoring import option for autotailor
The --json-tailoring option will provide support for importing https://github.com/ComplianceAsCode/schemas/tree/main/tailoring.
1 parent 7b45a7e commit 4112838

File tree

7 files changed

+228
-44
lines changed

7 files changed

+228
-44
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ build/
1717
*.a
1818
*.la
1919
.cproject
20+
.idea
2021
.project
2122
.settings/language.settings.xml
2223

‎docs/manual/manual.adoc

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
= OpenSCAP User Manual
22
:imagesdir: ./images
33
:workbench_url: https://www.open-scap.org/tools/scap-workbench/
4+
:json_tailoring_url: https://github.com/ComplianceAsCode/schemas/tree/main/tailoring
45
:sce_web: https://www.open-scap.org/features/other-standards/sce/
56
:openscap_web: https://open-scap.org/
67
:oscap_git: https://github.com/OpenSCAP/openscap
@@ -868,6 +869,12 @@ $ autotailor --unselect service_usbguard_enabled --output /tmp/tailoring.xml \
868869
--new-profile-id custom /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml ospp
869870
----
870871

872+
The `autotailor` tool can also consume {json_tailoring_url}[JSON tailoring] files and convert them into XCCDF Tailoring.
873+
874+
----
875+
$ autotailor --json-tailoring custom.json /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml
876+
----
877+
871878
For more details about other options of the `autotailor` program please read the `autotailor(8)` man page or run `autotailor --help`.
872879

873880

‎tests/utils/autotailor_integration_test.sh

+24-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ set -e -o pipefail
77
autotailor="$top_srcdir/utils/autotailor"
88
tailoring="$(mktemp)"
99
ds="$srcdir/data_stream.xml"
10+
json_tailoring="$srcdir/custom.json"
1011
stdout="$(mktemp)"
1112
original_profile="P1"
1213
result="$(mktemp)"
@@ -93,11 +94,33 @@ assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www
9394
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3"]/result[text()="pass"]'
9495
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R4"]/result[text()="notselected"]'
9596

96-
# refine value v1 to 30
97+
# set value v1 to thirty
9798
python3 $autotailor --id-namespace "com.example.www" --var-value V1=thirty $ds $original_profile > $tailoring
9899
$OSCAP xccdf eval --profile P1_customized --progress --tailoring-file $tailoring --results $result $ds
99100
assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V1" and text()="thirty"]'
100101
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R1"]/result[text()="pass"]'
101102
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R2"]/result[text()="pass"]'
102103
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3"]/result[text()="notselected"]'
103104
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R4"]/result[text()="notselected"]'
105+
106+
# refine value v1 to 'thirty' (30) and v2 to 'other' (Other Value)
107+
python3 $autotailor --id-namespace "com.example.www" --var-select V1=thirty --var-select V2=other $ds $original_profile > $tailoring
108+
$OSCAP xccdf eval --profile P1_customized --progress --tailoring-file $tailoring --results $result $ds
109+
assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V1" and text()="30"]'
110+
assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V2" and text()="Other Value"]'
111+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R1"]/result[text()="pass"]'
112+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R2"]/result[text()="pass"]'
113+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3"]/result[text()="notselected"]'
114+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R4"]/result[text()="notselected"]'
115+
116+
# use JSON tailoring
117+
python3 $autotailor $ds --id-namespace "com.example.www" --json-tailoring $json_tailoring > $tailoring
118+
$OSCAP xccdf eval --profile JSON_P1 --progress --tailoring-file $tailoring --results $result $ds
119+
assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V1" and text()="New Value"]'
120+
assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V2" and text()="Some Value"]'
121+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R1"]/result[text()="notselected"]'
122+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R2"]/result[text()="pass"]'
123+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3"]/result[text()="notchecked"]'
124+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3" and @role="unchecked"]'
125+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3" and @severity="unknown"]'
126+
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R4"]/result[text()="notselected"]'

‎tests/utils/custom.json

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"profiles": [
3+
{
4+
"id": "JSON_P1",
5+
"title": "JSON Tailored Profile P1",
6+
"base_profile_id": "P1",
7+
"rules": {
8+
"R1": {
9+
"evaluate": false
10+
},
11+
"R3": {
12+
"evaluate": true,
13+
"role": "unchecked",
14+
"severity": "unknown"
15+
}
16+
},
17+
"variables": {
18+
"V1": {
19+
"value": "New Value"
20+
},
21+
"V2": {
22+
"option_id": "some"
23+
}
24+
}
25+
}
26+
]
27+
}

‎tests/utils/data_stream.xml

+9-1
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,20 @@
6565
<select idref="xccdf_com.example.www_rule_R2" selected="true"/>
6666
</Profile>
6767
<Value id="xccdf_com.example.www_value_V1" operator="equals" type="number">
68-
<title>value</title>
68+
<title>value 1</title>
6969
<description xml:lang="en">cccc</description>
7070
<question xml:lang="en">ssss</question>
7171
<value>5</value>
7272
<value selector="thirty">30</value>
7373
</Value>
74+
<Value id="xccdf_com.example.www_value_V2" operator="equals" type="string">
75+
<title>value 2</title>
76+
<description xml:lang="en">22222</description>
77+
<question xml:lang="en">Q2</question>
78+
<value>Default</value>
79+
<value selector="some">Some Value</value>
80+
<value selector="other">Other Value</value>
81+
</Value>
7482
<Rule selected="false" id="xccdf_com.example.www_rule_R1">
7583
<title>Rule R1</title>
7684
<description>Description</description>

‎utils/autotailor

+141-39
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ DEFAULT_PROFILE_SUFFIX = "_customized"
3232
DEFAULT_REVERSE_DNS = "org.ssgproject.content"
3333
ROLES = ["full", "unscored", "unchecked"]
3434
SEVERITIES = ["unknown", "info", "low", "medium", "high"]
35+
ATTRIBUTES = ["role", "severity"]
3536

3637

3738
def quote(string):
3839
return "\"" + string + "\""
3940

4041

42+
def assignment_to_tuple(assignment):
43+
varname, value = assignment.split("=", 1)
44+
return varname, value
45+
46+
4147
def is_valid_xccdf_id(string):
4248
return re.match(
4349
r"^xccdf_[a-zA-Z0-9.-]+_(benchmark|profile|rule|group|value|"
@@ -59,6 +65,7 @@ class Tailoring:
5965
self.rules_to_select = []
6066
self.rules_to_unselect = []
6167
self._rule_refinements = collections.defaultdict(dict)
68+
self._value_refinements = collections.defaultdict(dict)
6269

6370
@property
6471
def profile_id(self):
@@ -75,7 +82,7 @@ class Tailoring:
7582
return self._rule_refinements[rule_id][attribute]
7683

7784
@staticmethod
78-
def _find_enumeration(attribute):
85+
def _find_rule_enumeration(attribute):
7986
if attribute == "role":
8087
enumeration = ROLES
8188
elif attribute == "severity":
@@ -90,46 +97,81 @@ class Tailoring:
9097
if not is_valid_xccdf_id(rule_id):
9198
msg = f"Rule id '{rule_id}' is invalid!"
9299
raise ValueError(msg)
93-
enumeration = Tailoring._find_enumeration(attribute)
100+
enumeration = Tailoring._find_rule_enumeration(attribute)
94101
if value in enumeration:
95102
return
96103
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}.")
100117
raise ValueError(msg)
101118

102119
def _prevent_duplicate_rule_refinement(self, attribute, rule_id, value):
103120
refinements = self._rule_refinements[rule_id]
104121
if attribute not in refinements:
105122
return
106123
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}'.")
110135
raise ValueError(msg)
111136

112137
def refine_rule(self, rule_id, attribute, value):
113138
Tailoring._validate_rule_refinement_params(rule_id, attribute, value)
114139
self._prevent_duplicate_rule_refinement(attribute, rule_id, value)
115140
self._rule_refinements[rule_id][attribute] = value
116141

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:
119157
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)
122159

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")
125162

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")
128165

129-
def change_values(self, assignements):
130-
for change in assignements:
166+
def change_values(self, assignments):
167+
for change in assignments:
131168
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)
133175

134176
def _full_id(self, string, el_type):
135177
if is_valid_xccdf_id(string):
@@ -159,7 +201,7 @@ class Tailoring:
159201
change.set("idref", self._full_rule_id(rule_id))
160202
change.set("selected", "false")
161203

162-
def _add_value_selections(self, container_element):
204+
def _add_value_overrides(self, container_element):
163205
for varname, value in self.value_changes:
164206
change = ET.SubElement(container_element, "{%s}set-value" % NS)
165207
change.set("idref", self._full_var_id(varname))
@@ -172,6 +214,13 @@ class Tailoring:
172214
for attr, val in refinements.items():
173215
ref_rule_el.set(attr, val)
174216

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+
175224
def to_xml(self, location=None):
176225
root = ET.Element("{%s}Tailoring" % NS)
177226
root.set("id", self.id)
@@ -198,27 +247,66 @@ class Tailoring:
198247
title.text = self.profile_title
199248

200249
self._add_rule_select_operations(profile)
201-
self._add_value_selections(profile)
250+
self._add_value_overrides(profile)
202251
self.rule_refinements_to_xml(profile)
252+
self.value_refinements_to_xml(profile)
203253

204254
root_str = ET.tostring(root)
205255
pretty_xml = xml.dom.minidom.parseString(root_str).toprettyxml()
206256
with open(location, "w") if location != "-" else sys.stdout as f:
207257
f.write(pretty_xml)
208258

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

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():
211295
parser = argparse.ArgumentParser(
212296
description="This script produces XCCDF 1.2 tailoring files "
213297
"to be used by SCAP scanners and SCAP data streams.")
214298
parser.add_argument(
215299
"datastream", metavar="DS_FILENAME",
216300
help="The tailored data stream filename.")
217301
parser.add_argument(
218-
"profile", metavar="BASE_PROFILE_ID",
302+
"profile", metavar="BASE_PROFILE_ID", nargs='?', default="",
219303
help="Specify ID of the base profile. ID of the profile can be "
220304
"either its full ID, or the suffix, in which case the "
221305
"'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.")
222310
parser.add_argument(
223311
"--title", default="",
224312
help="Title of the new profile.")
@@ -234,6 +322,13 @@ def parse_args():
234322
"or the suffix, in which case the 'xccdf_<id-namespace>_value' prefix "
235323
"will be prepended internally. Specify the argument multiple times "
236324
"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.")
237332
parser.add_argument(
238333
"-r", "--rule-role", metavar="RULE=ROLE", action="append", default=[],
239334
help="Specify refinement of the XCCDF rule role in form "
@@ -273,30 +368,37 @@ def parse_args():
273368
"-o", "--output", default="-",
274369
help="Where to save the tailoring file. If not supplied, write to "
275370
"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
283372

284373

285374
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")
287381

288382
t = Tailoring()
289-
t.reverse_dns = args.id_namespace
290-
t.extends = args.profile
291-
t.profile_id = args.new_profile_id
292383
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+
293399
t.change_values(args.var_value)
400+
t.change_selectors(args.var_select)
294401
t.change_roles(args.rule_role)
295402
t.change_severities(args.rule_severity)
296403

297-
t.profile_title = args.title
298-
299-
t.rules_to_select = args.select
300-
t.rules_to_unselect = args.unselect
301-
302404
t.to_xml(args.output)

‎utils/autotailor.8

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
.TH autotailor "8" "October 2023" "Red Hat, Inc." "System Administration Utilities"
1+
.TH autotailor "8" "January 2024" "Red Hat, Inc." "System Administration Utilities"
22
.SH NAME
33
autotailor \- CLI tool for tailoring of SCAP data streams.
44
.SH DESCRIPTION
5-
autotailor produces tailoring files that SCAP-compliant scanners can use to complement SCAP data streams.
5+
The autotailor tool produces tailoring files that SCAP-compliant scanners can use to complement SCAP data streams.
66
A tailoring file adds a new profile, which is supposed to extend a profile that is already present in the data stream.
77

88
Tailoring can add, remove or refine rules, and it also can redefine contents of XCCDF variables.
@@ -12,7 +12,7 @@ Note however, that the referenced data stream is not opened, and the validity of
1212
The tool doesn't prevent you from extending non-existent profiles, selecting non-existent rules, and so on.
1313

1414
.SH SYNOPSIS
15-
autotailor [OPTION...] DATASTREAM_FILE BASE_PROFILE_ID
15+
autotailor [OPTION...] DATASTREAM_FILE [BASE_PROFILE_ID]
1616

1717
.SH OPTIONS
1818
.TP
@@ -31,6 +31,11 @@ The reverse-DNS style string that is part of entities IDs in the corresponding d
3131
Specify modification of the XCCDF value in form <varname>=<value>. Name of the variable can be either its full name, or the suffix, in which case the 'xccdf_<id-namespace>_value' prefix will be prepended internally. Specify the argument multiple times if needed.
3232
.RE
3333
.TP
34+
\fB-v VAR=SELECTOR, --var-value VAR=SELECTOR\fR
35+
.RS
36+
Specify refinement of the XCCDF value in form <varname>=<selector>. Name of the variable can be either its full name, or the suffix, in which case the 'xccdf_<id-namespace>_value' prefix will be prepended internally. Specify the argument multiple times if needed.
37+
.RE
38+
.TP
3439
\fB-r RULE=ROLE, --rule-role RULE=ROLE\fR
3540
.RS
3641
Specify refinement of the XCCDF rule role in form <rule_id>=<role>. Name of the rule can be either its full name, or the suffix, in which case the 'xccdf_<id-namespace>_rule_' prefix will be prepended internally.
@@ -57,6 +62,12 @@ Specify the rule to unselect. The argument works the same way as the --select ar
5762
Specify the ID of the tailored profile. The ID of the new profile can be either its full ID, or the suffix, in which case the 'xccdf_<id-namespace>_profile_' prefix will be prepended internally.
5863
If left out, the new ID will be obtained by appending '_customized' to the tailored profile ID.
5964
.RE
65+
.TP
66+
\fB--json-tailoring JSON_TAILORING_FILE\fR
67+
.RS
68+
Import tailoring from a JSON file (https://github.com/ComplianceAsCode/schemas/tree/main/tailoring). This option makes BASE_PROFILE_ID positional argument optional.
69+
However, data passed in the command line options takes precedence over JSON contents, including the BASE_PROFILE_ID argument.
70+
.RE
6071

6172
.SH USAGE
6273
.SS Modify a variable value
@@ -74,11 +85,16 @@ The tailoring tailoring_file defines a new profile, xccdf_org.ssgproject.content
7485
.SS Perform more modifications
7586
$ autotailor --var-value var_screensaver_lock_delay=120 --select gconf_gnome_screensaver_idle_delay --var-value inactivity_timeout_value=600 ssg-rhel8-ds.xml pci_dss
7687

88+
.SS Import JSON tailoring
89+
$ autotailor ssg-rhel8-ds.xml --json-tailoring tailoring.json
90+
7791
.SH REPORTING BUGS
7892
.nf
7993
Please report bugs using https://github.com/OpenSCAP/openscap/issues
8094

8195
.SH AUTHORS
8296
.nf
8397
Matěj Týč <matyc@redhat.com>
98+
Jan Černý <jcerny@redhat.com>
99+
Evgenii Kolesnikov <ekolesni@redhat.com>
84100
.fi

0 commit comments

Comments
 (0)
Please sign in to comment.