#!/usr/bin/python3
# Copyright (C) 2020 Matěj Týč <matyc@redhat.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301 USA
import argparse
import datetime
import re
import sys
import pathlib
import xml.etree.ElementTree as ET
ALL_NS = {
"xccdf-1.1": "http://checklists.nist.gov/xccdf/1.1",
"xccdf-1.2": "http://checklists.nist.gov/xccdf/1.2",
}
DEFAULT_PROFILE_SUFFIX = "_customized"
def is_valid_xccdf_id(string):
return re.match(r"^xccdf_", string) is not None
class Tailoring:
ID_NAMESPACE = "org.ssgproject.content"
def __init__(self):
self.id_suffix = "auto_tailoring_default"
self.version = 1
self.profile_name = ""
self.extends = ""
self.original_ds_filename = ""
self.profile_title = ""
self.value_changes = []
self.rules_to_select = []
self.rules_to_unselect = []
def _full_id(self, string, el_type):
if is_valid_xccdf_id(string):
return string
default_prefix = "xccdf_{namespace}_{el_type}_".format(
namespace=self.ID_NAMESPACE, el_type=el_type)
return default_prefix + string
def _full_profile_id(self, string):
return self._full_id(string, "profile")
def _full_var_id(self, string):
return self._full_id(string, "value")
def _full_rule_id(self, string):
return self._full_id(string, "rule")
def add_value_change(self, varname, value):
self.value_changes.append((varname, value))
def to_xml(self, location=None):
root = ET.Element("xccdf-1.2:Tailoring")
root.set("xmlns:xccdf-1.2", ALL_NS["xccdf-1.2"])
root.set("id", "xccdf_" + self.id_suffix)
benchmark = ET.SubElement(root, "xccdf-1.2:benchmark")
datastream_uri = pathlib.Path(self.original_ds_filename).absolute().as_uri()
benchmark.set("href", datastream_uri)
version = ET.SubElement(root, "xccdf-1.2:version")
version.set("time", datetime.datetime.now().isoformat())
version.text = str(self.version)
profile = ET.SubElement(root, "xccdf-1.2:Profile")
profile.set("id", self._full_profile_id(self.profile_name))
profile.set("extends", self._full_profile_id(self.extends))
# Title has to be there due to the schema definition.
title = ET.SubElement(profile, "xccdf-1.2:title")
if self.profile_title:
title.set("override", "true")
else:
title.set("override", "false")
title.text = self.profile_title
for rule_id in self.rules_to_select:
change = ET.SubElement(profile, "xccdf-1.2:select")
change.set("idref", self._full_rule_id(rule_id))
change.set("selected", "true")
for rule_id in self.rules_to_unselect:
change = ET.SubElement(profile, "xccdf-1.2:select")
change.set("idref", self._full_rule_id(rule_id))
change.set("selected", "false")
for varname, value in self.value_changes:
change = ET.SubElement(profile, "xccdf-1.2:set-value")
change.set("idref", self._full_var_id(varname))
change.text = str(value)
if location == "-":
location = sys.stdout.buffer
ET.ElementTree(root).write(location)
def parse_args():
parser = argparse.ArgumentParser(
description="This script produces XCCDF 1.2 tailoring files "
"to be used by SCAP scanners and SCAP datastreams.")
parser.add_argument(
"datastream", metavar="DS_FILENAME",
help="The tailored datastream filename.")
parser.add_argument(
"profile", metavar="BASE_PROFILE_ID",
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_<id-namespace>_profile' prefix will be prepended internally.")
parser.add_argument(
"--title", default="",
help="Title of the new profile.")
parser.add_argument(
"--id-namespace", default="org.ssgproject.content",
help="The reverse-DNS style string that is part of entities IDs in the corresponding datastream. If left out, the default value 'org.ssgproject.content' is used.")
parser.add_argument(
"-v", "--var-value", metavar="VAR=VALUE", action="append", default=[],
help="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.")
parser.add_argument(
"-s", "--select", metavar="RULE_ID", action="append", default=[],
help="Specify what rules to select. "
"The rule ID can be either full, or just the suffix, in which case "
"the 'xccdf_<id-namespace>_rule' prefix will be prepended internally. "
"Specify the argument multiple times if needed.")
parser.add_argument(
"-u", "--unselect", metavar="RULE_ID", action="append", default=[],
help="Specify what rules to unselect. "
"The argument works the same way as the --select argument.")
parser.add_argument(
"-p", "--new-profile-id",
help="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. "
"If left out, the new ID will be obtained "
"by appending '{suffix}' to the tailored profile ID."
.format(suffix=DEFAULT_PROFILE_SUFFIX))
parser.add_argument(
"-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)
if __name__ == "__main__":
args = parse_args()
for prefix, uri in ALL_NS.items():
ET.register_namespace(prefix, uri)
t = Tailoring()
t.ID_NAMESPACE = args.id_namespace
t.extends = args.profile
if args.new_profile_id:
t.profile_name = args.new_profile_id
else:
t.profile_name = args.profile
t.original_ds_filename = args.datastream
for change in args.var_value:
varname, value = assignment_to_tuple(change)
t.add_value_change(varname, value)
t.profile_title = args.title
t.rules_to_select = args.select
t.rules_to_unselect = args.unselect
t.to_xml(args.output)