diff --git a/build-scripts/build_sce.py b/build-scripts/build_sce.py index 11635b2f195..25a99210c64 100755 --- a/build-scripts/build_sce.py +++ b/build-scripts/build_sce.py @@ -33,6 +33,7 @@ import ssg.build_sce import ssg.environment import ssg.templates +import ssg.products from ssg.utils import mkdir_p @@ -55,9 +56,6 @@ def parse_args(): ) p.add_argument( "--output", required=True) - p.add_argument( - "scedirs", metavar="SCE_DIR", nargs="+", - help="SCE definition scripts to build for the specified product.") args = p.parse_args() return args @@ -71,7 +69,9 @@ def parse_args(): env_yaml = ssg.environment.open_environment( args.build_config_yaml, args.product_yaml) empty = "/sce/empty/placeholder" + product_yaml = ssg.products.Product(args.product_yaml) template_builder = ssg.templates.Builder( env_yaml, empty, args.templates_dir, empty, empty, empty, None) - ssg.build_sce.checks(env_yaml, args.product_yaml, args.scedirs, - template_builder, args.output) + sce_builder = ssg.build_sce.SCEBuilder( + env_yaml, product_yaml, template_builder, args.output) + sce_builder.build() diff --git a/cmake/SSGCommon.cmake b/cmake/SSGCommon.cmake index a2b713cedbc..5bf7b0548c0 100644 --- a/cmake/SSGCommon.cmake +++ b/cmake/SSGCommon.cmake @@ -332,12 +332,6 @@ endmacro() # elements as necessary. macro(ssg_build_sce PRODUCT) set(BUILD_CHECKS_DIR "${CMAKE_CURRENT_BINARY_DIR}/checks") - # Unlike build_oval_unlinked, here we're ignoring the existing checks from - # templates and other places and we're merely appending/templating the - # content from the rules directories. That's why we ignore BUILD_CHECKS_DIR - # in the combine paths below. - set(SCE_COMBINE_PATHS "${SSG_SHARED}/checks/sce" "${CMAKE_CURRENT_SOURCE_DIR}/checks/sce") - if(SSG_SCE_ENABLED) # Unlike build_oval_unlinked, we don't depend on templated content yet. # @@ -348,7 +342,7 @@ macro(ssg_build_sce PRODUCT) # the XCCDF, so we'd have a dependency circle. add_custom_command( OUTPUT "${BUILD_CHECKS_DIR}/sce/metadata.json" - COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${SSG_BUILD_SCRIPTS}/build_sce.py" --build-config-yaml "${CMAKE_BINARY_DIR}/build_config.yml" --product-yaml "${CMAKE_CURRENT_BINARY_DIR}/product.yml" --templates-dir "${SSG_SHARED}/templates" --output "${BUILD_CHECKS_DIR}/sce" ${SCE_COMBINE_PATHS} + COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${SSG_BUILD_SCRIPTS}/build_sce.py" --build-config-yaml "${CMAKE_BINARY_DIR}/build_config.yml" --product-yaml "${CMAKE_CURRENT_BINARY_DIR}/product.yml" --templates-dir "${SSG_SHARED}/templates" --output "${BUILD_CHECKS_DIR}/sce" COMMENT "[${PRODUCT}-content] generating sce/metadata.json" DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/product.yml" ) diff --git a/docs/templates/template_reference.md b/docs/templates/template_reference.md index 6ab0dcfaa8b..2ab642924cc 100644 --- a/docs/templates/template_reference.md +++ b/docs/templates/template_reference.md @@ -704,7 +704,7 @@ When the remediation is applied duplicate occurrences of `key` are removed. If **daemonname** is not specified it means the name of the daemon is the same as the name of service. -- Languages: Ansible, Bash, OVAL, Puppet, Ignition, Kubernetes, Blueprint, Kickstart +- Languages: Ansible, Bash, OVAL, Puppet, Ignition, Kubernetes, Blueprint, Kickstart, SCE #### service_enabled - Checks if a system service is enabled. Uses either systemd or SysV @@ -723,7 +723,7 @@ When the remediation is applied duplicate occurrences of `key` are removed. If **daemonname** is not specified it means the name of the daemon is the same as the name of service. -- Languages: Ansible, Bash, OVAL, Puppet, Blueprint, Kickstart +- Languages: Ansible, Bash, OVAL, Puppet, Blueprint, Kickstart, SCE #### shell_lineinfile - Checks shell variable assignments in files. Remediations will paste diff --git a/shared/templates/service_disabled/sce-bash.template b/shared/templates/service_disabled/sce-bash.template new file mode 100644 index 00000000000..84addf8e8cc --- /dev/null +++ b/shared/templates/service_disabled/sce-bash.template @@ -0,0 +1,6 @@ +#!/bin/bash +# check-import = stdout +if [[ $(systemctl is-enabled {{{ DAEMONNAME }}}.service) == "masked" ]] ; then + exit "$XCCDF_RESULT_PASS" +fi +exit "$XCCDF_RESULT_FAIL" diff --git a/shared/templates/service_disabled/template.yml b/shared/templates/service_disabled/template.yml index 799afb2961b..cdf5b9eff25 100644 --- a/shared/templates/service_disabled/template.yml +++ b/shared/templates/service_disabled/template.yml @@ -7,3 +7,4 @@ supported_languages: - puppet - blueprint - kickstart + - sce-bash diff --git a/shared/templates/service_enabled/sce-bash.template b/shared/templates/service_enabled/sce-bash.template new file mode 100644 index 00000000000..5d33a00d3a6 --- /dev/null +++ b/shared/templates/service_enabled/sce-bash.template @@ -0,0 +1,6 @@ +#!/bin/bash +# check-import = stdout +if [[ $(systemctl is-enabled {{{ DAEMONNAME }}}.service) == "enabled" ]] ; then + exit "$XCCDF_RESULT_PASS" +fi +exit "$XCCDF_RESULT_FAIL" diff --git a/shared/templates/service_enabled/template.yml b/shared/templates/service_enabled/template.yml index 53084c33753..71059ef59ef 100644 --- a/shared/templates/service_enabled/template.yml +++ b/shared/templates/service_enabled/template.yml @@ -5,3 +5,4 @@ supported_languages: - puppet - blueprint - kickstart + - sce-bash diff --git a/ssg/build_sce.py b/ssg/build_sce.py index d5ea58bbb3f..110a73f7617 100644 --- a/ssg/build_sce.py +++ b/ssg/build_sce.py @@ -15,6 +15,11 @@ from . import utils, products +def write_sce_file(sce_content, output_dir, filename): + with open(os.path.join(output_dir, filename), 'w') as output_file: + print(sce_content, file=output_file) + + def load_sce_and_metadata(file_path, local_env_yaml): """ For the given SCE audit file (file_path) under the specified environment @@ -95,140 +100,6 @@ def _check_is_loaded(already_loaded, filename): return filename in already_loaded -def checks(env_yaml, yaml_path, sce_dirs, template_builder, output): - """ - Walks the build system and builds all SCE checks (and metadata entry) - into the output directory. - """ - product = utils.required_key(env_yaml, "product") - included_checks_count = 0 - reversed_dirs = sce_dirs[::-1] - already_loaded = dict() - local_env_yaml = dict() - local_env_yaml.update(env_yaml) - - # We maintain the same search structure as build_ovals.py even though we - # don't currently have any content under shared/checks/sce. - product_yaml = products.Product(yaml_path) - product_dir = product_yaml["product_dir"] - relative_guide_dir = utils.required_key(env_yaml, "benchmark_root") - guide_dir = os.path.abspath(os.path.join(product_dir, relative_guide_dir)) - additional_content_directories = env_yaml.get("additional_content_directories", []) - add_content_dirs = [ - os.path.abspath(os.path.join(product_dir, rd)) - for rd in additional_content_directories - ] - - # First walk all rules under the product. These have higher priority than any - # out-of-tree SCE checks. - for _dir_path in find_rule_dirs_in_paths([guide_dir] + add_content_dirs): - rule_id = get_rule_dir_id(_dir_path) - - rule_path = os.path.join(_dir_path, "rule.yml") - try: - rule = Rule.from_yaml(rule_path, env_yaml) - except DocumentationNotComplete: - # Happens on non-debug builds when a rule isn't yet completed. We - # don't want to build the SCE check for this rule yet so skip it - # and move on. - continue - - local_env_yaml['rule_id'] = rule.id_ - local_env_yaml['rule_title'] = rule.title - local_env_yaml['products'] = {product} - - for _path in get_rule_dir_sces(_dir_path, product): - # To be compatible with later checks, use the rule_id (i.e., the - # value of _dir) to recreate the expected filename if this OVAL - # was in a rule directory. However, note that unlike - # build_oval.checks(...), we have to get this script's extension - # first. - _, ext = os.path.splitext(_path) - filename = "{0}{1}".format(rule_id, ext) - - sce_content, metadata = load_sce_and_metadata(_path, local_env_yaml) - metadata['filename'] = filename - - if not _check_is_applicable_for_product(metadata, product): - continue - if _check_is_loaded(already_loaded, rule_id): - continue - - with open(os.path.join(output, filename), 'w') as output_file: - print(sce_content, file=output_file) - - included_checks_count += 1 - already_loaded[rule_id] = metadata - - if rule.template: - langs = template_builder.get_resolved_langs_to_generate(rule) - if 'sce-bash' in langs: - # Here we know the specified rule has a template and this - # template actually generates (bash) SCE content. We - # prioritize bespoke SCE content over templated content, - # however, while we add this to our metadata, we do not - # bother (yet!) with generating the SCE content. This is done - # at a later time by build-scripts/build_templated_content.py. - if _check_is_loaded(already_loaded, rule_id): - continue - - # While we don't _write_ it, we still need to parse SCE - # metadata from the templated content. Render it internally. - raw_sce_content = template_builder.get_lang_contents_for_templatable( - rule, langs['sce-bash'] - ) - - ext = '.sh' - filename = rule_id + ext - - # Load metadata and infer correct file name. - sce_content, metadata = load_sce_and_metadata_parsed(raw_sce_content) - metadata['filename'] = filename - - # Skip the check if it isn't applicable for this product. - if not _check_is_applicable_for_product(metadata, product): - continue - - with open(os.path.join(output, filename), 'w') as output_file: - print(sce_content, file=output_file) - - # Finally, include it in our loaded content - included_checks_count += 1 - already_loaded[rule_id] = metadata - - # Finally take any shared SCE checks and build them as well. Note that - # there's no way for shorthand generation to include them if they do NOT - # align with a particular rule_id, so it is suggested that the former - # method be used. - for sce_dir in reversed_dirs: - if not os.path.isdir(sce_dir): - continue - - for filename in sorted(os.listdir(sce_dir)): - rule_id, _ = os.path.splitext(filename) - - sce_content, metadata = load_sce_and_metadata(filename, env_yaml) - metadata['filename'] = filename - - if not _check_is_applicable_for_product(metadata, product): - continue - if _check_is_loaded(already_loaded, rule_id): - continue - - with open(os.path.join(output, filename), 'w') as output_file: - print(sce_content, file=output_file) - - included_checks_count += 1 - already_loaded[rule_id] = metadata - - # Finally, write out our metadata to disk so that we can reference it in - # later build stages (such as during building shorthand content). - metadata_path = os.path.join(output, 'metadata.json') - json.dump(already_loaded, open(metadata_path, 'w')) - - return already_loaded - - # Retrieve the SCE checks and return a list of path to each check script. def collect_sce_checks(datastreamtree): checklists = datastreamtree.find( @@ -253,3 +124,134 @@ def collect_sce_checks(datastreamtree): checks = datastreamtree.findall(checks_xpath) # Extract the file paths of the SCE checks return [check.get('href') for check in checks] + + +class SCEBuilder(): + def __init__(self, env_yaml, product_yaml, template_builder, output_dir): + self.env_yaml = env_yaml + self.product_yaml = product_yaml + self.template_builder = template_builder + self.output_dir = output_dir + self.already_loaded = dict() + + def _build_static_sce_check(self, rule_id, path, local_env_yaml): + # To be compatible with later checks, use the rule_id (i.e., the + # value of _dir) to recreate the expected filename if this OVAL + # was in a rule directory. However, note that unlike + # build_oval.checks(...), we have to get this script's extension + # first. + _, ext = os.path.splitext(path) + filename = "{0}{1}".format(rule_id, ext) + + sce_content, metadata = load_sce_and_metadata(path, local_env_yaml) + metadata['filename'] = filename + product = utils.required_key(self.env_yaml, "product") + + if not _check_is_applicable_for_product(metadata, product): + return + if _check_is_loaded(self.already_loaded, rule_id): + return + + write_sce_file(sce_content, self.output_dir, filename) + + self.already_loaded[rule_id] = metadata + + def _get_rule_sce_lang(self, rule): + langs = self.template_builder.get_resolved_langs_to_generate(rule) + for lang in langs: + if lang.name == 'sce-bash': + return lang + return None + + def _build_templated_sce_check(self, rule): + if not rule.template: + return + sce_lang = self._get_rule_sce_lang(rule) + if sce_lang is None: + return + + # Here we know the specified rule has a template and this + # template actually generates (bash) SCE content. We + # prioritize bespoke SCE content over templated content, + # however, while we add this to our metadata, we do not + # bother (yet!) with generating the SCE content. This is done + # at a later time by build-scripts/build_templated_content.py. + if _check_is_loaded(self.already_loaded, rule.id_): + return + + # While we don't _write_ it, we still need to parse SCE + # metadata from the templated content. Render it internally. + raw_sce_content = self.template_builder.get_lang_contents_for_templatable( + rule, sce_lang + ) + + ext = '.sh' + filename = rule.id_ + ext + + # Load metadata and infer correct file name. + sce_content, metadata = load_sce_and_metadata_parsed(raw_sce_content) + metadata['filename'] = filename + + # Skip the check if it isn't applicable for this product. + product = utils.required_key(self.env_yaml, "product") + if not _check_is_applicable_for_product(metadata, product): + return + + write_sce_file(sce_content, self.output_dir, filename) + + # Finally, include it in our loaded content + self.already_loaded[rule.id_] = metadata + + def _build_rule(self, rule_dir_path): + local_env_yaml = dict() + local_env_yaml.update(self.env_yaml) + product = utils.required_key(self.env_yaml, "product") + rule_id = get_rule_dir_id(rule_dir_path) + + rule_path = os.path.join(rule_dir_path, "rule.yml") + try: + rule = Rule.from_yaml(rule_path, self.env_yaml) + except DocumentationNotComplete: + # Happens on non-debug builds when a rule isn't yet completed. We + # don't want to build the SCE check for this rule yet so skip it + # and move on. + return + + local_env_yaml['rule_id'] = rule.id_ + local_env_yaml['rule_title'] = rule.title + local_env_yaml['products'] = {product} + + for _path in get_rule_dir_sces(rule_dir_path, product): + self._build_static_sce_check(rule_id, _path, local_env_yaml) + + self._build_templated_sce_check(rule) + + def _dump_metadata(self): + # Finally, write out our metadata to disk so that we can reference it in + # later build stages (such as during building shorthand content). + metadata_path = os.path.join(self.output_dir, 'metadata.json') + json.dump(self.already_loaded, open(metadata_path, 'w')) + + def _find_content_dirs(self): + # We maintain the same search structure as build_ovals.py even though we + # don't currently have any content under shared/checks/sce. + product_dir = self.product_yaml["product_dir"] + relative_guide_dir = utils.required_key(self.env_yaml, "benchmark_root") + guide_dir = os.path.abspath(os.path.join(product_dir, relative_guide_dir)) + additional_content_directories = self.env_yaml.get( + "additional_content_directories", []) + add_content_dirs = [ + os.path.abspath(os.path.join(product_dir, rd)) + for rd in additional_content_directories + ] + return ([guide_dir] + add_content_dirs) + + def build(self): + """ + Walks the content repository and builds all SCE checks (and metadata entry) + into the output directory. + """ + content_dirs = self._find_content_dirs() + for _dir_path in find_rule_dirs_in_paths(content_dirs): + self._build_rule(_dir_path) + self._dump_metadata() diff --git a/ssg/build_yaml.py b/ssg/build_yaml.py index 7e55341838b..4622eda8c1c 100644 --- a/ssg/build_yaml.py +++ b/ssg/build_yaml.py @@ -1231,6 +1231,105 @@ def merge_control_references(self): else: self.references[ref_type] = self.control_references[ref_type] + def _get_sce_check_parent_element(self, rule_el): + if 'complex-check' in self.sce_metadata: + # Here we have an issue: XCCDF allows EITHER one or more check + # elements OR a single complex-check. While we have an explicit + # case handling the OVAL-and-SCE interaction, OCIL entries have + # (historically) been alongside OVAL content and been in an + # "OR" manner -- preferring OVAL to SCE. In order to accomplish + # this, we thus need to add _yet another parent_ when OCIL data + # is present, and add update ocil_parent accordingly. + if self.ocil or self.ocil_clause: + ocil_parent = ET.SubElement( + rule_el, "{%s}complex-check" % XCCDF12_NS) + ocil_parent.set('operator', 'OR') + + check_parent = ET.SubElement( + ocil_parent, "{%s}complex-check" % XCCDF12_NS) + check_parent.set('operator', self.sce_metadata['complex-check']) + else: + check_parent = rule_el + return check_parent + + def _add_sce_check_import_element(self, sce_check): + if isinstance(self.sce_metadata['check-import'], str): + self.sce_metadata['check-import'] = [self.sce_metadata['check-import']] + for entry in self.sce_metadata['check-import']: + check_import = ET.SubElement( + sce_check, '{%s}check-import' % XCCDF12_NS) + check_import.set('import-name', entry) + check_import.text = None + + def _add_sce_check_export_element(self, sce_check): + if isinstance(self.sce_metadata['check-export'], str): + self.sce_metadata['check-export'] = [self.sce_metadata['check-export']] + for entry in self.sce_metadata['check-export']: + export, value = entry.split('=') + check_export = ET.SubElement( + sce_check, '{%s}check-export' % XCCDF12_NS) + check_export.set('value-id', value) + check_export.set('export-name', export) + check_export.text = None + + def _add_sce_check_content_ref_element(self, sce_check): + check_ref = ET.SubElement( + sce_check, "{%s}check-content-ref" % XCCDF12_NS) + href = self.sce_metadata['relative_path'] + check_ref.set("href", href) + + def _add_sce_check_element(self, rule_el): + if not self.sce_metadata: + return + # TODO: This is pretty much another hack, just like the previous OVAL + # one. However, we avoided the external SCE content as I'm not sure it + # is generally useful (unlike say, CVE checking with external OVAL) + # + # Additionally, we build the content (check subelement) here rather + # than in xslt due to the nature of our SCE metadata. + # + # Finally, before we begin, we might have an element with both SCE + # and OVAL. We have no way of knowing (right here) whether that is + # the case (due to a variety of issues, most notably, that linking + # hasn't yet occurred). So we must rely on the content author's + # good will, by annotating SCE content with a complex-check tag + # if necessary. + parent_el = self._get_sce_check_parent_element(rule_el) + sce_check_el = ET.SubElement(parent_el, "{%s}check" % XCCDF12_NS) + sce_check_el.set("system", SCE_SYSTEM) + if 'check-import' in self.sce_metadata: + self._add_sce_check_import_element(sce_check_el) + if 'check-export' in self.sce_metadata: + self._add_sce_check_export_element(sce_check_el) + self._add_sce_check_content_ref_element(sce_check_el) + + def _add_oval_check_element(self, rule_el): + check = ET.SubElement(rule_el, '{%s}check' % XCCDF12_NS) + check.set("system", oval_namespace) + check_content_ref = ET.SubElement( + check, "{%s}check-content-ref" % XCCDF12_NS) + if self.oval_external_content: + check_content_ref.set("href", self.oval_external_content) + else: + # TODO: This is pretty much a hack, oval ID will be the same as rule ID + # and we don't want the developers to have to keep them in sync. + # Therefore let's just add an OVAL ref of that ID. + # TODO Can we not add the check element if the rule doesn't have an OVAL check? + # At the moment, the check elements of rules without OVAL are removed by + # the OVALFileLinker class. + check_content_ref.set("href", "oval-unlinked.xml") + check_content_ref.set("name", self.id_) + + def _add_ocil_check_element(self, rule_el): + patches_up_to_date = (self.id_ == "security_patches_up_to_date") + if (self.ocil or self.ocil_clause) and not patches_up_to_date: + ocil_check = ET.SubElement(rule_el, "{%s}check" % XCCDF12_NS) + ocil_check.set("system", ocil_cs) + ocil_check_ref = ET.SubElement( + ocil_check, "{%s}check-content-ref" % XCCDF12_NS) + ocil_check_ref.set("href", "ocil-unlinked.xml") + ocil_check_ref.set("name", self.id_ + "_ocil") + def to_xml_element(self, env_yaml=None): rule = ET.Element('{%s}Rule' % XCCDF12_NS) rule.set('selected', 'false') @@ -1262,94 +1361,9 @@ def to_xml_element(self, env_yaml=None): self._add_ident_elements(rule) self._add_fixes_elements(rule) - ocil_parent = rule - check_parent = rule - - if self.sce_metadata: - # TODO: This is pretty much another hack, just like the previous OVAL - # one. However, we avoided the external SCE content as I'm not sure it - # is generally useful (unlike say, CVE checking with external OVAL) - # - # Additionally, we build the content (check subelement) here rather - # than in xslt due to the nature of our SCE metadata. - # - # Finally, before we begin, we might have an element with both SCE - # and OVAL. We have no way of knowing (right here) whether that is - # the case (due to a variety of issues, most notably, that linking - # hasn't yet occurred). So we must rely on the content author's - # good will, by annotating SCE content with a complex-check tag - # if necessary. - - if 'complex-check' in self.sce_metadata: - # Here we have an issue: XCCDF allows EITHER one or more check - # elements OR a single complex-check. While we have an explicit - # case handling the OVAL-and-SCE interaction, OCIL entries have - # (historically) been alongside OVAL content and been in an - # "OR" manner -- preferring OVAL to SCE. In order to accomplish - # this, we thus need to add _yet another parent_ when OCIL data - # is present, and add update ocil_parent accordingly. - if self.ocil or self.ocil_clause: - ocil_parent = ET.SubElement( - ocil_parent, "{%s}complex-check" % XCCDF12_NS) - ocil_parent.set('operator', 'OR') - - check_parent = ET.SubElement( - ocil_parent, "{%s}complex-check" % XCCDF12_NS) - check_parent.set('operator', self.sce_metadata['complex-check']) - - # Now, add the SCE check element to the tree. - check = ET.SubElement(check_parent, "{%s}check" % XCCDF12_NS) - check.set("system", SCE_SYSTEM) - - if 'check-import' in self.sce_metadata: - if isinstance(self.sce_metadata['check-import'], str): - self.sce_metadata['check-import'] = [self.sce_metadata['check-import']] - for entry in self.sce_metadata['check-import']: - check_import = ET.SubElement( - check, '{%s}check-import' % XCCDF12_NS) - check_import.set('import-name', entry) - check_import.text = None - - if 'check-export' in self.sce_metadata: - if isinstance(self.sce_metadata['check-export'], str): - self.sce_metadata['check-export'] = [self.sce_metadata['check-export']] - for entry in self.sce_metadata['check-export']: - export, value = entry.split('=') - check_export = ET.SubElement( - check, '{%s}check-export' % XCCDF12_NS) - check_export.set('value-id', value) - check_export.set('export-name', export) - check_export.text = None - - check_ref = ET.SubElement( - check, "{%s}check-content-ref" % XCCDF12_NS) - href = self.sce_metadata['relative_path'] - check_ref.set("href", href) - - check = ET.SubElement(check_parent, '{%s}check' % XCCDF12_NS) - check.set("system", oval_namespace) - check_content_ref = ET.SubElement( - check, "{%s}check-content-ref" % XCCDF12_NS) - if self.oval_external_content: - check_content_ref.set("href", self.oval_external_content) - else: - # TODO: This is pretty much a hack, oval ID will be the same as rule ID - # and we don't want the developers to have to keep them in sync. - # Therefore let's just add an OVAL ref of that ID. - # TODO Can we not add the check element if the rule doesn't have an OVAL check? - # At the moment, the check elements of rules without OVAL are removed by - # the OVALFileLinker class. - check_content_ref.set("href", "oval-unlinked.xml") - check_content_ref.set("name", self.id_) - - patches_up_to_date = (self.id_ == "security_patches_up_to_date") - if (self.ocil or self.ocil_clause) and not patches_up_to_date: - ocil_check = ET.SubElement(check_parent, "{%s}check" % XCCDF12_NS) - ocil_check.set("system", ocil_cs) - ocil_check_ref = ET.SubElement( - ocil_check, "{%s}check-content-ref" % XCCDF12_NS) - ocil_check_ref.set("href", "ocil-unlinked.xml") - ocil_check_ref.set("name", self.id_ + "_ocil") + self._add_sce_check_element(rule) + self._add_oval_check_element(rule) + self._add_ocil_check_element(rule) return rule diff --git a/tests/unit/ssg-module/test_build_sce.py b/tests/unit/ssg-module/test_build_sce.py new file mode 100644 index 00000000000..92460060876 --- /dev/null +++ b/tests/unit/ssg-module/test_build_sce.py @@ -0,0 +1,53 @@ +import json +import os +import pytest +import tempfile + +import ssg.build_sce +import ssg.environment +import ssg.products +import ssg.templates + + +PROJECT_ROOT = os.path.join(os.path.dirname(__file__), "..", "..", "..", ) +DATADIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_build_sce_data")) +TEST_OUTPUT_DIR = tempfile.mkdtemp() + + +@pytest.fixture +def scebuilder(): + build_config_yaml_path = os.path.join( + PROJECT_ROOT, "build", "build_config.yml") + product_yaml_path = os.path.join(DATADIR, "product.yml") + env_yaml = ssg.environment.open_environment( + build_config_yaml_path, product_yaml_path) + product_yaml = ssg.products.Product(product_yaml_path) + templates_dir = os.path.join(DATADIR, "templates") + template_builder = ssg.templates.Builder( + env_yaml, '', templates_dir, + '', '', '', None) + b = ssg.build_sce.SCEBuilder( + env_yaml, product_yaml, template_builder, TEST_OUTPUT_DIR) + return b + + +def test_scebuilder(scebuilder): + scebuilder.build() + + # Verify that a static SCE check is built + assert "selinux_state.sh" in os.listdir(TEST_OUTPUT_DIR) + with open(os.path.join(TEST_OUTPUT_DIR, "selinux_state.sh")) as f: + contents = f.read() + assert "$(getenforce) == \"Enforcing\"" in contents + + # Verify that a templated SCE check for a templated rule is built + assert "package_rsyslog_installed.sh" in os.listdir(TEST_OUTPUT_DIR) + with open(os.path.join(TEST_OUTPUT_DIR, "package_rsyslog_installed.sh")) as f: + contents = f.read() + assert "rpm -q rsyslog" in contents + + # Verify metadata JSON contents + with open(os.path.join(TEST_OUTPUT_DIR, "metadata.json")) as f: + metadata = json.load(f) + assert "selinux_state" in metadata + assert "package_rsyslog_installed" in metadata diff --git a/tests/unit/ssg-module/test_build_sce_data/package_rsyslog_installed/rule.yml b/tests/unit/ssg-module/test_build_sce_data/package_rsyslog_installed/rule.yml new file mode 100644 index 00000000000..02a15afc753 --- /dev/null +++ b/tests/unit/ssg-module/test_build_sce_data/package_rsyslog_installed/rule.yml @@ -0,0 +1,16 @@ +documentation_complete: true + +title: 'Ensure rsyslog is Installed' + +description: 'Rsyslog is installed by default' + +rationale: |- + The rsyslog package provides the rsyslog daemon, which provides + system logging services. + +severity: medium + +template: + name: package_installed + vars: + pkgname: rsyslog diff --git a/tests/unit/ssg-module/test_build_sce_data/product.yml b/tests/unit/ssg-module/test_build_sce_data/product.yml new file mode 100644 index 00000000000..8d00af828aa --- /dev/null +++ b/tests/unit/ssg-module/test_build_sce_data/product.yml @@ -0,0 +1,61 @@ +product: rhel9 +full_name: Red Hat Enterprise Linux 9 +type: platform + +families: + - rhel + - rhel-like + +major_version_ordinal: 9 + +benchmark_id: RHEL-9 +benchmark_root: "." +components_root: "../../components" + +profiles_root: "./profiles" + +pkg_manager: "dnf" + +init_system: "systemd" + +# EFI and non-EFI configs are stored in same path, see https://fedoraproject.org/wiki/Changes/UnifyGrubConfig + +groups: + dedicated_ssh_keyowner: + name: ssh_keys + +sshd_distributed_config: "true" + +dconf_gdm_dir: "distro.d" + +faillock_path: "/var/log/faillock" + +# The fingerprints below are retrieved from https://access.redhat.com/security/team/key +pkg_release: "4ae0493b" +pkg_version: "fd431d51" +aux_pkg_release: "6229229e" +aux_pkg_version: "5a6340b3" + +release_key_fingerprint: "567E347AD0044ADE55BA8A5F199E2F91FD431D51" +auxiliary_key_fingerprint: "7E4624258C406535D56D6F135054E4A45A6340B3" + +cpes_root: "../../shared/applicability" +cpes: + - rhel9: + name: "cpe:/o:redhat:enterprise_linux:9" + title: "Red Hat Enterprise Linux 9" + check_id: installed_OS_is_rhel9 + +# Mapping of CPE platform to package +platform_package_overrides: + login_defs: "shadow-utils" + +reference_uris: + cis: 'https://www.cisecurity.org/benchmark/red_hat_linux/' + ccn: 'https://www.ccn-cert.cni.es/pdf/guias/series-ccn-stic/guias-de-acceso-publico-ccn-stic/6768-ccn-stic-610a22-perfilado-de-seguridad-red-hat-enterprise-linux-9-0/file.html' + +centos_pkg_release: "5ccc5b19" +centos_pkg_version: "8483c65d" +centos_major_version: "9" + +journald_conf_dir_path: /etc/systemd/journald.conf.d diff --git a/tests/unit/ssg-module/test_build_sce_data/selinux_state/rule.yml b/tests/unit/ssg-module/test_build_sce_data/selinux_state/rule.yml new file mode 100644 index 00000000000..1afdbb19f8e --- /dev/null +++ b/tests/unit/ssg-module/test_build_sce_data/selinux_state/rule.yml @@ -0,0 +1,14 @@ +documentation_complete: true + +title: 'Ensure SELinux State is Enforcing' + +description: |- + The SELinux state should be set to enforcing. + +rationale: |- + Setting the SELinux state to enforcing ensures SELinux is able to confine + potentially compromised processes to the security policy, which is designed to + prevent them from causing damage to the system or further elevating their + privileges. + +severity: high diff --git a/tests/unit/ssg-module/test_build_sce_data/selinux_state/sce/shared.sh b/tests/unit/ssg-module/test_build_sce_data/selinux_state/sce/shared.sh new file mode 100644 index 00000000000..9b02e2a677e --- /dev/null +++ b/tests/unit/ssg-module/test_build_sce_data/selinux_state/sce/shared.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# check-import = stdout +if [[ $(getenforce) == "Enforcing" ]] ; then + exit "$XCCDF_RESULT_PASS" +fi +exit "$XCCDF_RESULT_FAIL" diff --git a/tests/unit/ssg-module/test_build_sce_data/templates/package_installed/sce-bash.template b/tests/unit/ssg-module/test_build_sce_data/templates/package_installed/sce-bash.template new file mode 100644 index 00000000000..334c11453b8 --- /dev/null +++ b/tests/unit/ssg-module/test_build_sce_data/templates/package_installed/sce-bash.template @@ -0,0 +1,6 @@ +#!/bin/bash +# check-import = stdout +if [[ $(rpm -q {{{ PKGNAME }}}) ]] ; then + exit "$XCCDF_RESULT_PASS" +fi +exit "$XCCDF_RESULT_FAIL" diff --git a/tests/unit/ssg-module/test_build_sce_data/templates/package_installed/template.yml b/tests/unit/ssg-module/test_build_sce_data/templates/package_installed/template.yml new file mode 100644 index 00000000000..1e446b281db --- /dev/null +++ b/tests/unit/ssg-module/test_build_sce_data/templates/package_installed/template.yml @@ -0,0 +1,2 @@ +supported_languages: + - sce-bash