diff --git a/components/chrony.yml b/components/chrony.yml index 857576f7ea2..a83d1e7042d 100644 --- a/components/chrony.yml +++ b/components/chrony.yml @@ -2,6 +2,7 @@ name: chrony packages: - chrony rules: +- chronyd_configure_pool_and_server - chronyd_run_as_chrony_user - chronyd_server_directive - chronyd_specify_remote_server diff --git a/components/firewalld.yml b/components/firewalld.yml index 7e781067001..4ef2a3d76a3 100644 --- a/components/firewalld.yml +++ b/components/firewalld.yml @@ -12,6 +12,7 @@ rules: - firewalld-backend - firewalld_loopback_traffic_restricted - firewalld_loopback_traffic_trusted +- network_implement_access_control - package_firewalld_installed - package_firewalld_removed - service_firewalld_disabled diff --git a/docs/manual/developer/03_creating_content.md b/docs/manual/developer/03_creating_content.md index 8128c8c35ea..91750881fbd 100644 --- a/docs/manual/developer/03_creating_content.md +++ b/docs/manual/developer/03_creating_content.md @@ -1254,3 +1254,5 @@ YAML file keys: - `changelog` (list) - records substantial changes in the given component that affected rules and remediations (optional) Each rule in the benchmark in the `/linux_os/guide` directory must be a member of at least 1 component. + +Products specify a path to the directory with component files by the `components_root` key in the `product.yml`. diff --git a/products/example/product.yml b/products/example/product.yml index 3ae0b024f8a..86c5ec427ce 100644 --- a/products/example/product.yml +++ b/products/example/product.yml @@ -4,6 +4,7 @@ type: platform benchmark_id: EXAMPLE benchmark_root: "../../linux_os/guide" +components_root: "../../components" profiles_root: "./profiles" diff --git a/products/fedora/product.yml b/products/fedora/product.yml index 3508fd3d268..6d172116634 100644 --- a/products/fedora/product.yml +++ b/products/fedora/product.yml @@ -4,6 +4,7 @@ type: platform benchmark_id: FEDORA benchmark_root: "../../linux_os/guide" +components_root: "../../components" profiles_root: "./profiles" diff --git a/products/rhel7/product.yml b/products/rhel7/product.yml index 83c9f17e43a..f92e5cde19a 100644 --- a/products/rhel7/product.yml +++ b/products/rhel7/product.yml @@ -4,6 +4,7 @@ type: platform benchmark_id: RHEL-7 benchmark_root: "../../linux_os/guide" +components_root: "../../components" profiles_root: "./profiles" diff --git a/products/rhel8/product.yml b/products/rhel8/product.yml index 447f68d3550..5cec8b5f4f2 100644 --- a/products/rhel8/product.yml +++ b/products/rhel8/product.yml @@ -4,6 +4,7 @@ type: platform benchmark_id: RHEL-8 benchmark_root: "../../linux_os/guide" +components_root: "../../components" profiles_root: "./profiles" diff --git a/products/rhel9/product.yml b/products/rhel9/product.yml index 050c3dc7f5e..ec3c0ba745b 100644 --- a/products/rhel9/product.yml +++ b/products/rhel9/product.yml @@ -4,6 +4,7 @@ type: platform benchmark_id: RHEL-9 benchmark_root: "../../linux_os/guide" +components_root: "../../components" profiles_root: "./profiles" diff --git a/ssg/build_yaml.py b/ssg/build_yaml.py index 3e4bb539549..a0a2b9b78ee 100644 --- a/ssg/build_yaml.py +++ b/ssg/build_yaml.py @@ -2,6 +2,7 @@ from __future__ import print_function from copy import deepcopy +import collections import datetime import json import os @@ -12,6 +13,7 @@ import ssg.build_remediations +import ssg.components from .build_cpe import CPEALLogicalTest, CPEALCheckFactRef, ProductCPEs from .constants import (XCCDF12_NS, OSCAP_BENCHMARK, @@ -1331,6 +1333,18 @@ def __init__( self.stig_references = None if stig_reference_path: self.stig_references = ssg.build_stig.map_versions_to_rule_ids(stig_reference_path) + self.rule_to_components = self._load_components() + + def _load_components(self): + if "components_root" not in self.env_yaml: + return None + product_dir = self.env_yaml["product_dir"] + components_root = self.env_yaml["components_root"] + components_dir = os.path.join(product_dir, components_root) + components = ssg.components.load(components_dir) + rule_to_components = ssg.components.get_rule_to_components_mapping( + components) + return rule_to_components def _process_values(self): for value_yaml in self.value_files: @@ -1338,6 +1352,23 @@ def _process_values(self): self.all_values[value.id_] = value self.loaded_group.add_value(value) + def __process_rule(self, rule): + if self.rule_to_components is not None and rule.id_ not in self.rule_to_components: + raise ValueError( + "The rule '%s' isn't mapped to any component! Insert the " + "rule ID at least once to the rule-component mapping." % + (rule.id_)) + prodtypes = parse_prodtype(rule.prodtype) + if "all" not in prodtypes and self.product not in prodtypes: + return False + self.all_rules[rule.id_] = rule + self.loaded_group.add_rule( + rule, env_yaml=self.env_yaml, product_cpes=self.product_cpes) + rule.normalize(self.env_yaml["product"]) + if self.stig_references: + rule.add_stig_references(self.stig_references) + return True + def _process_rules(self): for rule_yaml in self.rule_files: try: @@ -1346,16 +1377,8 @@ def _process_rules(self): except DocumentationNotComplete: # Happens on non-debug build when a rule is "documentation-incomplete" continue - prodtypes = parse_prodtype(rule.prodtype) - if "all" not in prodtypes and self.product not in prodtypes: + if not self.__process_rule(rule): continue - self.all_rules[rule.id_] = rule - self.loaded_group.add_rule( - rule, env_yaml=self.env_yaml, product_cpes=self.product_cpes) - - rule.normalize(self.env_yaml["product"]) - if self.stig_references: - rule.add_stig_references(self.stig_references) def _get_new_loader(self): loader = BuildLoader( @@ -1364,6 +1387,8 @@ def _get_new_loader(self): loader.sce_metadata = self.sce_metadata # Do it this way so we only have to parse the STIG references once. loader.stig_references = self.stig_references + # Do it this way so we only have to parse the component metadata once. + loader.rule_to_components = self.rule_to_components return loader def export_group_to_file(self, filename): diff --git a/ssg/components.py b/ssg/components.py new file mode 100644 index 00000000000..96ca2051dcd --- /dev/null +++ b/ssg/components.py @@ -0,0 +1,65 @@ +from __future__ import print_function + +from collections import defaultdict +import os + +import ssg.yaml + + +def load(components_dir): + components = {} + for component_filename in os.listdir(components_dir): + components_filepath = os.path.join(components_dir, component_filename) + component = Component(components_filepath) + components[component.name] = component + return components + + +def rule_components_mapping(components): + rules_to_components = defaultdict(list) + for component in components.values(): + for rule_id in component.rules: + rules_to_components[rule_id].append(component) + return rules_to_components + + +def package_component_mapping(components): + packages_to_components = {} + for component in components.values(): + for package in component.packages: + packages_to_components[package] = component.name + return packages_to_components + + +def template_component_mapping(components): + template_to_component = {} + for component in components.values(): + for template in component.templates: + template_to_component[template] = component.name + return template_to_component + + +def group_components_mapping(components): + group_to_component = defaultdict(list) + for component in components.values(): + for group in component.groups: + group_to_component[group].append(component.name) + return group_to_component + + +class Component: + def __init__(self, filepath): + yaml_data = ssg.yaml.open_raw(filepath) + self.name = yaml_data["name"] + self.rules = yaml_data["rules"] + self.packages = yaml_data["packages"] + self.templates = yaml_data.get("templates", []) + self.groups = yaml_data.get("groups", []) + + +def get_rule_to_components_mapping(components): + rule_to_components = defaultdict(list) + for component in components.values(): + for rule_id in component.rules: + rule_to_components[rule_id].append(component.name) + return rule_to_components diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 218bc73c1fc..3d1db4d7f6a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -360,3 +360,10 @@ if (PYTHON_VERSION_MAJOR GREATER 2) ) set_tests_properties("install-vm" PROPERTIES LABELS quick) endif() + +if (SSG_PRODUCT_RHEL9) +add_test( + NAME "components" + COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_components.py" --build-dir "${CMAKE_BINARY_DIR}" --source-dir "${CMAKE_SOURCE_DIR}" --product "rhel9" +) +endif() diff --git a/tests/test_components.py b/tests/test_components.py new file mode 100644 index 00000000000..aff34d3cdc1 --- /dev/null +++ b/tests/test_components.py @@ -0,0 +1,234 @@ +import argparse +import collections +import os +import re + +import ssg.build_yaml +import ssg.components +import ssg.environment +import ssg.rules +import ssg.yaml + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Test components data consistency") + parser.add_argument("--source-dir", help="Path to the root directory") + parser.add_argument("--build-dir", help="Path to the build directory") + parser.add_argument("--product", help="Product ID") + return parser.parse_args() + + +def get_component_by_template( + rule, package_to_component, template_to_component): + template = rule.template + component = None + reason = None + if not template: + return (component, reason) + template_name = template["name"] + template_vars = template["vars"] + if template_name in template_to_component: + component = template_to_component[template_name] + reason = ( + "all rules using template '%s' should be assigned to component " + "'%s'" % (template_name, component)) + elif template_name in ["package_installed", "package_removed"]: + package = template_vars["pkgname"] + component = package_to_component.get(package, package) + reason = ( + "rule uses template '%s' with 'pkgname' parameter set to '%s' " + "which is a package that already belongs to component '%s'" % + (template_name, package, component)) + elif template_name in ["service_enabled", "service_disabled"]: + if "packagename" in template_vars: + package = template_vars["packagename"] + else: + package = template_vars["servicename"] + component = package_to_component.get(package, package) + reason = ( + "rule uses template '%s' checking service '%s' provided by " + "package '%s' which is a package that already belongs to " + "component '%s'" % ( + template_name, template_vars["servicename"], + package, component)) + return (component, reason) + + +def test_nonexistent_rules(rules_in_benchmark, rules_with_component): + nonexistent_rules = rules_with_component - rules_in_benchmark + if nonexistent_rules: + print("The following rules aren't part of the benchmark:") + for rule_id in nonexistent_rules: + print("- %s" % (rule_id)) + return False + return True + + +def test_unmapped_rules(rules_in_benchmark, rules_with_component): + unmapped_rules = rules_in_benchmark - rules_with_component + if unmapped_rules: + print("The following rules aren't part of any component:") + for x in unmapped_rules: + print("- " + x) + return False + return True + + +def find_all_rules(base_dir): + for rule_dir in ssg.rules.find_rule_dirs(base_dir): + rule_id = ssg.rules.get_rule_dir_id(rule_dir) + yield rule_id + + +def iterate_over_resolved_rules(built_rules_dir): + for file_name in os.listdir(built_rules_dir): + file_path = os.path.join(built_rules_dir, file_name) + try: + rule = ssg.build_yaml.Rule.from_yaml(file_path) + except ssg.yaml.DocumentationNotComplete: + pass + yield rule + + +def test_templates( + rule, package_to_component, rule_components, template_to_component): + result = True + candidate, reason = get_component_by_template( + rule, package_to_component, template_to_component) + if candidate and candidate not in rule_components: + result = False + print( + "Rule '%s' should be assigned to component '%s', because %s." % + (rule.id_, candidate, reason)) + return result + + +def test_package_platform(rule, package_to_component, rule_components): + match = re.match(r"package\[([\w\-_]+)\]", rule.platform) + if not match: + return True + result = True + for package in match.groups(): + component = package_to_component.get(package, package) + if component not in rule_components: + print( + "Rule '%s' should be assigned to component '%s', " + "because it uses the package['%s'] platform." % + (rule.id_, component, package)) + result = False + return result + + +def test_platform(rule, package_to_component, rule_components): + platform = rule.platform + if platform is None: + return True + result = True + if "package" in platform: + result = test_package_platform( + rule, package_to_component, rule_components) + elif platform == "grub2" and "grub2" not in rule_components: + print( + "Rule '%s' should be assigned to component 'grub2', " + "because it uses the 'grub2' platform." % + (rule.id_)) + result = False + elif platform == "sssd-ldap" and "sssd" not in rule_components: + print( + "Rule '%s' should be assigned to component 'sssd', " + "because it uses the 'sssd-ldap' platform." % + (rule.id_)) + result = False + return result + + +def test_group(rule, rule_components, rule_groups, group_to_components): + result = True + for g in rule_groups: + components = group_to_components.get(g, []) + for c in components: + if c not in rule_components: + print( + "Rule '%s' should be in component '%s' because it's a " + "member of '%s' group." % (rule.id_, c, g)) + result = False + return result + + +def get_rule_to_groups(groups_dir): + rule_to_groups = collections.defaultdict(list) + for file_name in os.listdir(groups_dir): + group_file_path = os.path.join(groups_dir, file_name) + group = ssg.build_yaml.Group.from_yaml(group_file_path) + for rule in group.rules: + rule_to_groups[rule].append(group.id_) + return rule_to_groups + + +def test_benchmark_rules(components, source_dir): + result = True + rule_to_components = ssg.components.rule_components_mapping(components) + rules_with_component = set(rule_to_components.keys()) + linux_os_guide_dir = os.path.join(source_dir, "linux_os", "guide") + rules_in_benchmark = set(find_all_rules(linux_os_guide_dir)) + if not test_nonexistent_rules(rules_in_benchmark, rules_with_component): + result = False + if not test_unmapped_rules(rules_in_benchmark, rules_with_component): + result = False + return result + + +def test_rule(rule, mappings): + ( + rule_to_components, package_to_component, template_to_component, + rule_to_groups, group_to_components) = mappings + result = True + rule_components = [c.name for c in rule_to_components[rule.id_]] + rule_groups = rule_to_groups[rule.id_] + if not test_templates( + rule, package_to_component, rule_components, + template_to_component): + result = False + if not test_platform(rule, package_to_component, rule_components): + result = False + if not test_group(rule, rule_components, rule_groups, group_to_components): + result = False + return result + + +def test_resolved_rules(components, build_dir, product): + result = True + rule_to_components = ssg.components.rule_components_mapping(components) + package_to_component = ssg.components.package_component_mapping( + components) + template_to_component = ssg.components.template_component_mapping( + components) + group_to_components = ssg.components.group_components_mapping(components) + product_dir = os.path.join(build_dir, product) + groups_dir = os.path.join(product_dir, "groups") + rule_to_groups = get_rule_to_groups(groups_dir) + rules_dir = os.path.join(product_dir, "rules") + mappings = ( + rule_to_components, package_to_component, template_to_component, + rule_to_groups, group_to_components) + for rule in iterate_over_resolved_rules(rules_dir): + if not test_rule(rule, mappings): + result = False + return result + + +def main(): + args = parse_args() + components_dir = os.path.join(args.source_dir, "components") + components = ssg.components.load(components_dir) + result = 0 + if not test_benchmark_rules(components, args.source_dir): + result = 1 + if not test_resolved_rules(components, args.build_dir, args.product): + result = 1 + exit(result) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/ssg-module/data/components_dir/fapolicyd.yml b/tests/unit/ssg-module/data/components_dir/fapolicyd.yml new file mode 100644 index 00000000000..a7a0dbd2e03 --- /dev/null +++ b/tests/unit/ssg-module/data/components_dir/fapolicyd.yml @@ -0,0 +1,12 @@ +name: fapolicyd +groups: +- fapolicy +- integrity +packages: +- fapolicyd-server +rules: +- fapolicy_default_deny +- fapolicyd_prevent_home_folder_access +- service_fapolicyd_enabled +templates: +- file_policy_blocked diff --git a/tests/unit/ssg-module/test_components.py b/tests/unit/ssg-module/test_components.py new file mode 100644 index 00000000000..1f14f0b82a6 --- /dev/null +++ b/tests/unit/ssg-module/test_components.py @@ -0,0 +1,99 @@ +import os +import pytest + +import ssg.components + +ssg_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) +components_dir = os.path.join(data_dir, "components_dir") +component_file = os.path.join(components_dir, "fapolicyd.yml") + + +def test_load(): + cs = ssg.components.load(components_dir) + assert isinstance(cs, dict) + assert len(cs) == 1 + assert "fapolicyd" in cs + assert isinstance(cs["fapolicyd"], ssg.components.Component) + assert cs["fapolicyd"].name == "fapolicyd" + + +def test_rule_components_mapping(): + cs = ssg.components.load(components_dir) + rule_to_components = ssg.components.rule_components_mapping(cs) + assert isinstance(rule_to_components, dict) + assert len(rule_to_components) == 3 + assert "fapolicy_default_deny" in rule_to_components + assert isinstance( + rule_to_components["fapolicy_default_deny"], list) + assert len(rule_to_components["fapolicy_default_deny"]) == 1 + assert isinstance( + rule_to_components["fapolicy_default_deny"][0], + ssg.components.Component) + assert rule_to_components["fapolicy_default_deny"][0].name == "fapolicyd" + assert "fapolicyd_prevent_home_folder_access" in rule_to_components + assert isinstance( + rule_to_components["fapolicyd_prevent_home_folder_access"], list) + assert len(rule_to_components["fapolicyd_prevent_home_folder_access"]) == 1 + assert isinstance( + rule_to_components["fapolicyd_prevent_home_folder_access"][0], + ssg.components.Component) + assert \ + rule_to_components["fapolicyd_prevent_home_folder_access"][0].name == \ + "fapolicyd" + assert "service_fapolicyd_enabled" in rule_to_components + assert isinstance( + rule_to_components["service_fapolicyd_enabled"], list) + assert len(rule_to_components["service_fapolicyd_enabled"]) == 1 + assert isinstance( + rule_to_components["service_fapolicyd_enabled"][0], + ssg.components.Component) + assert rule_to_components["service_fapolicyd_enabled"][0].name == \ + "fapolicyd" + + +def test_package_component_mapping(): + cs = ssg.components.load(components_dir) + package_to_component = ssg.components.package_component_mapping(cs) + assert isinstance(package_to_component, dict) + assert len(package_to_component.keys()) == 1 + assert "fapolicyd-server" in package_to_component + assert package_to_component["fapolicyd-server"] == "fapolicyd" + + +def test_template_component_mapping(): + cs = ssg.components.load(components_dir) + template_to_component = ssg.components.template_component_mapping(cs) + assert isinstance(template_to_component, dict) + assert len(template_to_component.keys()) == 1 + assert "file_policy_blocked" in template_to_component + assert template_to_component["file_policy_blocked"] == "fapolicyd" + + +def test_group_components_mapping(): + cs = ssg.components.load(components_dir) + group_to_component = ssg.components.group_components_mapping(cs) + assert isinstance(group_to_component, dict) + assert len(group_to_component.keys()) == 2 + assert "fapolicy" in group_to_component + assert len(group_to_component["fapolicy"]) == 1 + assert group_to_component["fapolicy"][0] == "fapolicyd" + assert "integrity" in group_to_component + assert len(group_to_component["integrity"]) == 1 + assert group_to_component["integrity"][0] == "fapolicyd" + + +def test_component_parse(): + c = ssg.components.Component(component_file) + assert c.name == "fapolicyd" + assert len(c.groups) == 2 + assert "fapolicy" in c.groups + assert "integrity" in c.groups + assert len(c.packages) == 1 + assert "fapolicyd-server" in c.packages + assert len(c.rules) == 3 + assert "fapolicy_default_deny" in c.rules + assert "fapolicyd_prevent_home_folder_access" in c.rules + assert "service_fapolicyd_enabled" in c.rules + assert len(c.templates) == 1 + assert "file_policy_blocked" in c.templates