diff --git a/.gitignore b/.gitignore
index b1690d5a9a..4a399446ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ build/
*.a
*.la
.cproject
+.idea
.project
.settings/language.settings.xml
diff --git a/docs/manual/manual.adoc b/docs/manual/manual.adoc
index a30ec8f3e5..1a78406dd2 100644
--- a/docs/manual/manual.adoc
+++ b/docs/manual/manual.adoc
@@ -1,6 +1,7 @@
= OpenSCAP User Manual
:imagesdir: ./images
:workbench_url: https://www.open-scap.org/tools/scap-workbench/
+:json_tailoring_url: https://github.com/ComplianceAsCode/schemas/tree/main/tailoring
:sce_web: https://www.open-scap.org/features/other-standards/sce/
:openscap_web: https://open-scap.org/
:oscap_git: https://github.com/OpenSCAP/openscap
@@ -868,6 +869,12 @@ $ autotailor --unselect service_usbguard_enabled --output /tmp/tailoring.xml \
--new-profile-id custom /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml ospp
----
+The `autotailor` tool can also consume {json_tailoring_url}[JSON tailoring] files and convert them into XCCDF Tailoring.
+
+----
+$ autotailor --json-tailoring custom.json /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml
+----
+
For more details about other options of the `autotailor` program please read the `autotailor(8)` man page or run `autotailor --help`.
diff --git a/tests/utils/autotailor_integration_test.sh b/tests/utils/autotailor_integration_test.sh
index 92ef0a2288..815b9b9bbc 100755
--- a/tests/utils/autotailor_integration_test.sh
+++ b/tests/utils/autotailor_integration_test.sh
@@ -7,6 +7,7 @@ set -e -o pipefail
autotailor="$top_srcdir/utils/autotailor"
tailoring="$(mktemp)"
ds="$srcdir/data_stream.xml"
+json_tailoring="$srcdir/custom.json"
stdout="$(mktemp)"
original_profile="P1"
result="$(mktemp)"
@@ -93,7 +94,7 @@ assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3"]/result[text()="pass"]'
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R4"]/result[text()="notselected"]'
-# refine value v1 to 30
+# set value v1 to thirty
python3 $autotailor --id-namespace "com.example.www" --var-value V1=thirty $ds $original_profile > $tailoring
$OSCAP xccdf eval --profile P1_customized --progress --tailoring-file $tailoring --results $result $ds
assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V1" and text()="thirty"]'
@@ -101,3 +102,25 @@ assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R2"]/result[text()="pass"]'
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3"]/result[text()="notselected"]'
assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R4"]/result[text()="notselected"]'
+
+# refine value v1 to 'thirty' (30) and v2 to 'other' (Other Value)
+python3 $autotailor --id-namespace "com.example.www" --var-select V1=thirty --var-select V2=other $ds $original_profile > $tailoring
+$OSCAP xccdf eval --profile P1_customized --progress --tailoring-file $tailoring --results $result $ds
+assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V1" and text()="30"]'
+assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V2" and text()="Other Value"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R1"]/result[text()="pass"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R2"]/result[text()="pass"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3"]/result[text()="notselected"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R4"]/result[text()="notselected"]'
+
+# use JSON tailoring
+python3 $autotailor $ds --id-namespace "com.example.www" --json-tailoring $json_tailoring > $tailoring
+$OSCAP xccdf eval --profile JSON_P1 --progress --tailoring-file $tailoring --results $result $ds
+assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V1" and text()="New Value"]'
+assert_exists 1 '/Benchmark/TestResult/set-value[@idref="xccdf_com.example.www_value_V2" and text()="Some Value"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R1"]/result[text()="notselected"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R2"]/result[text()="pass"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3"]/result[text()="notchecked"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3" and @role="unchecked"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R3" and @severity="unknown"]'
+assert_exists 1 '/Benchmark/TestResult/rule-result[@idref="xccdf_com.example.www_rule_R4"]/result[text()="notselected"]'
diff --git a/tests/utils/custom.json b/tests/utils/custom.json
new file mode 100644
index 0000000000..7fc06f8f1a
--- /dev/null
+++ b/tests/utils/custom.json
@@ -0,0 +1,23 @@
+{
+ "id": "JSON_P1",
+ "title": "JSON Tailored Profile P1",
+ "base_profile_id": "P1",
+ "rules": {
+ "R1": {
+ "selected": false
+ },
+ "R3": {
+ "selected": true,
+ "role": "unchecked",
+ "severity": "unknown"
+ }
+ },
+ "variables": {
+ "V1": {
+ "value": "New Value"
+ },
+ "V2": {
+ "select": "some"
+ }
+ }
+}
diff --git a/tests/utils/data_stream.xml b/tests/utils/data_stream.xml
index 7f748d839f..5ebf4dc2b0 100644
--- a/tests/utils/data_stream.xml
+++ b/tests/utils/data_stream.xml
@@ -65,12 +65,20 @@
- value
+ value 1ccccssss530
+
+ value 2
+ 22222
+ Q2
+ Default
+ Some Value
+ Other Value
+ Rule R1Description
diff --git a/utils/autotailor b/utils/autotailor
index df91a6bf3f..a510e7e9c3 100755
--- a/utils/autotailor
+++ b/utils/autotailor
@@ -32,12 +32,18 @@ DEFAULT_PROFILE_SUFFIX = "_customized"
DEFAULT_REVERSE_DNS = "org.ssgproject.content"
ROLES = ["full", "unscored", "unchecked"]
SEVERITIES = ["unknown", "info", "low", "medium", "high"]
+ATTRIBUTES = ["role", "severity"]
def quote(string):
return "\"" + string + "\""
+def assignment_to_tuple(assignment):
+ varname, value = assignment.split("=", 1)
+ return varname, value
+
+
def is_valid_xccdf_id(string):
return re.match(
r"^xccdf_[a-zA-Z0-9.-]+_(benchmark|profile|rule|group|value|"
@@ -59,6 +65,7 @@ class Tailoring:
self.rules_to_select = []
self.rules_to_unselect = []
self._rule_refinements = collections.defaultdict(dict)
+ self._value_refinements = collections.defaultdict(dict)
@property
def profile_id(self):
@@ -75,7 +82,7 @@ class Tailoring:
return self._rule_refinements[rule_id][attribute]
@staticmethod
- def _find_enumeration(attribute):
+ def _find_rule_enumeration(attribute):
if attribute == "role":
enumeration = ROLES
elif attribute == "severity":
@@ -90,13 +97,23 @@ class Tailoring:
if not is_valid_xccdf_id(rule_id):
msg = f"Rule id '{rule_id}' is invalid!"
raise ValueError(msg)
- enumeration = Tailoring._find_enumeration(attribute)
+ enumeration = Tailoring._find_rule_enumeration(attribute)
if value in enumeration:
return
allowed = ", ".join(map(quote, enumeration))
- msg = (
- f"Can't refine {attribute} of rule '{rule_id}' to '{value}'. "
- f"Allowed {attribute} values are: {allowed}.")
+ msg = (f"Can't refine {attribute} of rule '{rule_id}' to '{value}'. "
+ f"Allowed {attribute} values are: {allowed}.")
+ raise ValueError(msg)
+
+ @staticmethod
+ def _validate_value_refinement_params(value_id, attribute, value):
+ if not is_valid_xccdf_id(value_id):
+ msg = f"Value id '{value_id}' is invalid!"
+ raise ValueError(msg)
+ if attribute == 'selector':
+ return
+ msg = (f"Can't refine {attribute} of value '{value_id}' to '{value}'. "
+ f"Unsupported refine-rule attribute {attribute}.")
raise ValueError(msg)
def _prevent_duplicate_rule_refinement(self, attribute, rule_id, value):
@@ -104,9 +121,17 @@ class Tailoring:
if attribute not in refinements:
return
current = refinements[attribute]
- msg = (
- f"Can't refine {attribute} of rule '{rule_id}' to '{value}'. "
- f"This rule {attribute} is already refined to '{current}'.")
+ msg = (f"Can't refine {attribute} of rule '{rule_id}' to '{value}'. "
+ f"This rule {attribute} is already refined to '{current}'.")
+ raise ValueError(msg)
+
+ def _prevent_duplicate_value_refinement(self, attribute, value_id, value):
+ refinements = self._value_refinements[value_id]
+ if attribute not in refinements:
+ return
+ current = refinements[attribute]
+ msg = (f"Can't refine {attribute} of value '{value_id}' to '{value}'. "
+ f"This value {attribute} is already refined to '{current}'.")
raise ValueError(msg)
def refine_rule(self, rule_id, attribute, value):
@@ -114,22 +139,39 @@ class Tailoring:
self._prevent_duplicate_rule_refinement(attribute, rule_id, value)
self._rule_refinements[rule_id][attribute] = value
- def change_attributes(self, assignements, attribute):
- for change in assignements:
+ def refine_value(self, value_id, attribute, value):
+ Tailoring._validate_value_refinement_params(value_id, attribute, value)
+ self._prevent_duplicate_value_refinement(attribute, value_id, value)
+ self._value_refinements[value_id][attribute] = value
+
+ def change_rule_attribute(self, rule_id, attribute, value):
+ full_rule_id = self._full_rule_id(rule_id)
+ self.refine_rule(full_rule_id, attribute, value)
+
+ def change_value_attribute(self, var_id, attribute, value):
+ full_value_id = self._full_var_id(var_id)
+ self.refine_value(full_value_id, attribute, value)
+
+ def change_rules_attributes(self, assignments, attribute):
+ for change in assignments:
rule_id, value = assignment_to_tuple(change)
- full_rule_id = self._full_rule_id(rule_id)
- self.refine_rule(full_rule_id, attribute, value)
+ self.change_rule_attribute(rule_id, attribute, value)
- def change_roles(self, assignements):
- self.change_attributes(assignements, "role")
+ def change_roles(self, assignments):
+ self.change_rules_attributes(assignments, "role")
- def change_severities(self, assignements):
- self.change_attributes(assignements, "severity")
+ def change_severities(self, assignments):
+ self.change_rules_attributes(assignments, "severity")
- def change_values(self, assignements):
- for change in assignements:
+ def change_values(self, assignments):
+ for change in assignments:
varname, value = assignment_to_tuple(change)
- t.add_value_change(varname, value)
+ self.add_value_change(varname, value)
+
+ def change_selectors(self, assignments):
+ for change in assignments:
+ varname, selector = assignment_to_tuple(change)
+ self.change_value_attribute(varname, "selector", selector)
def _full_id(self, string, el_type):
if is_valid_xccdf_id(string):
@@ -159,7 +201,7 @@ class Tailoring:
change.set("idref", self._full_rule_id(rule_id))
change.set("selected", "false")
- def _add_value_selections(self, container_element):
+ def _add_value_overrides(self, container_element):
for varname, value in self.value_changes:
change = ET.SubElement(container_element, "{%s}set-value" % NS)
change.set("idref", self._full_var_id(varname))
@@ -172,6 +214,13 @@ class Tailoring:
for attr, val in refinements.items():
ref_rule_el.set(attr, val)
+ def value_refinements_to_xml(self, profile_el):
+ for value_id, refinements in self._value_refinements.items():
+ ref_value_el = ET.SubElement(profile_el, "{%s}refine-value" % NS)
+ ref_value_el.set("idref", value_id)
+ for attr, val in refinements.items():
+ ref_value_el.set(attr, val)
+
def to_xml(self, location=None):
root = ET.Element("{%s}Tailoring" % NS)
root.set("id", self.id)
@@ -198,16 +247,45 @@ class Tailoring:
title.text = self.profile_title
self._add_rule_select_operations(profile)
- self._add_value_selections(profile)
+ self._add_value_overrides(profile)
self.rule_refinements_to_xml(profile)
+ self.value_refinements_to_xml(profile)
root_str = ET.tostring(root)
pretty_xml = xml.dom.minidom.parseString(root_str).toprettyxml()
with open(location, "w") if location != "-" else sys.stdout as f:
f.write(pretty_xml)
+ def import_json_tailoring(self, json_tailoring):
+ import json
+ with open(json_tailoring, "r") as jf:
+ tailoring = json.load(jf)
+
+ self.extends = tailoring["base_profile_id"]
+
+ self.profile_id = tailoring.get("id", self.profile_id)
+ self.profile_title = tailoring.get("title", self.profile_title)
+
+ if "rules" in tailoring:
+ for rule_id, props in tailoring["rules"].items():
+ if "selected" in props:
+ if props["selected"]:
+ self.rules_to_select.append(rule_id)
+ else:
+ self.rules_to_unselect.append(rule_id)
+ for attr in ATTRIBUTES:
+ if attr in props:
+ self.change_rule_attribute(rule_id, attr, props[attr])
-def parse_args():
+ if "variables" in tailoring:
+ for variable_id, props in tailoring["variables"].items():
+ if "value" in props:
+ self.add_value_change(variable_id, props["value"])
+ if "select" in props:
+ self.change_value_attribute(variable_id, "selector", props["select"])
+
+
+def get_parser():
parser = argparse.ArgumentParser(
description="This script produces XCCDF 1.2 tailoring files "
"to be used by SCAP scanners and SCAP data streams.")
@@ -215,10 +293,14 @@ def parse_args():
"datastream", metavar="DS_FILENAME",
help="The tailored data stream filename.")
parser.add_argument(
- "profile", metavar="BASE_PROFILE_ID",
+ "profile", metavar="BASE_PROFILE_ID", nargs='?', default="",
help="Specify ID of the base profile. ID of the profile can be "
"either its full ID, or the suffix, in which case the "
"'xccdf__profile' prefix will be prepended internally.")
+ parser.add_argument(
+ "--json-tailoring", metavar="JSON_TAILORING_FILENAME", default="",
+ help="JSON Tailoring (https://github.com/ComplianceAsCode/schemas/blob/main/tailoring/schema.json) "
+ "filename.")
parser.add_argument(
"--title", default="",
help="Title of the new profile.")
@@ -234,6 +316,13 @@ def parse_args():
"or the suffix, in which case the 'xccdf__value' prefix "
"will be prepended internally. Specify the argument multiple times "
"if needed.")
+ parser.add_argument(
+ "-V", "--var-select", metavar="VAR=SELECTOR", action="append", default=[],
+ help="Specify refinement of the XCCDF value in form "
+ "=. Name of the variable can be either its full name, "
+ "or the suffix, in which case the 'xccdf__value' prefix "
+ "will be prepended internally. Specify the argument multiple times "
+ "if needed.")
parser.add_argument(
"-r", "--rule-role", metavar="RULE=ROLE", action="append", default=[],
help="Specify refinement of the XCCDF rule role in form "
@@ -273,30 +362,37 @@ def parse_args():
"-o", "--output", default="-",
help="Where to save the tailoring file. If not supplied, write to "
"standard output.")
- args = parser.parse_args()
- return args
-
-
-def assignment_to_tuple(assignment):
- varname, value = assignment.split("=", 1)
- return (varname, value)
+ return parser
if __name__ == "__main__":
- args = parse_args()
+ parser = get_parser()
+ args = parser.parse_args()
+
+ if not args.profile and not args.json_tailoring:
+ parser.error("one of the following arguments has to be provided: "
+ "BASE_PROFILE_ID or --json-tailoring JSON_TAILORING_FILENAME")
t = Tailoring()
- t.reverse_dns = args.id_namespace
- t.extends = args.profile
- t.profile_id = args.new_profile_id
t.original_ds_filename = args.datastream
+ t.reverse_dns = args.id_namespace
+
+ if args.json_tailoring:
+ t.import_json_tailoring(args.json_tailoring)
+
+ if args.profile:
+ t.extends = args.profile
+ if args.new_profile_id:
+ t.profile_id = args.new_profile_id
+ if args.title:
+ t.profile_title = args.title
+
+ t.rules_to_select.extend(args.select)
+ t.rules_to_unselect.extend(args.unselect)
+
t.change_values(args.var_value)
+ t.change_selectors(args.var_select)
t.change_roles(args.rule_role)
t.change_severities(args.rule_severity)
- t.profile_title = args.title
-
- t.rules_to_select = args.select
- t.rules_to_unselect = args.unselect
-
t.to_xml(args.output)
diff --git a/utils/autotailor.8 b/utils/autotailor.8
index 5679f071d9..4e13ad0e9c 100644
--- a/utils/autotailor.8
+++ b/utils/autotailor.8
@@ -1,8 +1,8 @@
-.TH autotailor "8" "October 2023" "Red Hat, Inc." "System Administration Utilities"
+.TH autotailor "8" "January 2024" "Red Hat, Inc." "System Administration Utilities"
.SH NAME
autotailor \- CLI tool for tailoring of SCAP data streams.
.SH DESCRIPTION
-autotailor produces tailoring files that SCAP-compliant scanners can use to complement SCAP data streams.
+The autotailor tool produces tailoring files that SCAP-compliant scanners can use to complement SCAP data streams.
A tailoring file adds a new profile, which is supposed to extend a profile that is already present in the data stream.
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
The tool doesn't prevent you from extending non-existent profiles, selecting non-existent rules, and so on.
.SH SYNOPSIS
-autotailor [OPTION...] DATASTREAM_FILE BASE_PROFILE_ID
+autotailor [OPTION...] DATASTREAM_FILE [BASE_PROFILE_ID]
.SH OPTIONS
.TP
@@ -31,6 +31,11 @@ The reverse-DNS style string that is part of entities IDs in the corresponding d
Specify modification of the XCCDF value in form =. Name of the variable can be either its full name, or the suffix, in which case the 'xccdf__value' prefix will be prepended internally. Specify the argument multiple times if needed.
.RE
.TP
+\fB-v VAR=SELECTOR, --var-value VAR=SELECTOR\fR
+.RS
+Specify refinement of the XCCDF value in form =. Name of the variable can be either its full name, or the suffix, in which case the 'xccdf__value' prefix will be prepended internally. Specify the argument multiple times if needed.
+.RE
+.TP
\fB-r RULE=ROLE, --rule-role RULE=ROLE\fR
.RS
Specify refinement of the XCCDF rule role in form =. Name of the rule can be either its full name, or the suffix, in which case the 'xccdf__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
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__profile_' prefix will be prepended internally.
If left out, the new ID will be obtained by appending '_customized' to the tailored profile ID.
.RE
+.TP
+\fB--json-tailoring JSON_TAILORING_FILE\fR
+.RS
+Import tailoring from a JSON file (https://github.com/ComplianceAsCode/schemas/tree/main/tailoring). This option makes BASE_PROFILE_ID positional argument optional.
+However, data passed in the command line options takes precedence over JSON contents, including the BASE_PROFILE_ID argument.
+.RE
.SH USAGE
.SS Modify a variable value
@@ -74,6 +85,9 @@ The tailoring tailoring_file defines a new profile, xccdf_org.ssgproject.content
.SS Perform more modifications
$ 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
+.SS Import JSON tailoring
+$ autotailor ssg-rhel8-ds.xml --json-tailoring tailoring.json
+
.SH REPORTING BUGS
.nf
Please report bugs using https://github.com/OpenSCAP/openscap/issues
@@ -81,4 +95,6 @@ Please report bugs using https://github.com/OpenSCAP/openscap/issues
.SH AUTHORS
.nf
Matěj Týč
+Jan Černý
+Evgenii Kolesnikov
.fi