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/components/iptables.yml b/components/iptables.yml index 782caa5cfca..b3f19683e97 100644 --- a/components/iptables.yml +++ b/components/iptables.yml @@ -11,6 +11,7 @@ rules: - package_iptables-persistent_installed - package_iptables-persistent_removed - package_iptables-services_installed +- package_iptables-services_removed - package_iptables_installed - service_ip6tables_enabled - service_iptables_enabled diff --git a/docs/manual/developer/03_creating_content.md b/docs/manual/developer/03_creating_content.md index a91b76997bf..2b99457da60 100644 --- a/docs/manual/developer/03_creating_content.md +++ b/docs/manual/developer/03_creating_content.md @@ -439,6 +439,8 @@ type: platform benchmark_root: "../../linux_os/guide" +components_root: "../../components" + profiles_root: "./profiles" pkg_manager: "yum" @@ -1254,3 +1256,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 00783b7d5a5..d8ab686b350 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..2a25d616be1 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, @@ -657,6 +659,7 @@ class Rule(XCCDFEntity, Templatable): rationale=lambda: "", severity=lambda: "", references=lambda: dict(), + components=lambda: list(), identifiers=lambda: dict(), ocil_clause=lambda: None, ocil=lambda: None, @@ -1331,6 +1334,19 @@ 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.abspath( + os.path.join(product_dir, components_root)) + components = ssg.components.load(components_dir) + rule_to_components = ssg.components.rule_component_mapping( + components) + return rule_to_components def _process_values(self): for value_yaml in self.value_files: @@ -1338,6 +1354,25 @@ 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) + if self.rule_to_components is not None: + rule.components = self.rule_to_components[rule.id_] + return True + def _process_rules(self): for rule_yaml in self.rule_files: try: @@ -1346,16 +1381,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 +1391,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..04e418b23f6 --- /dev/null +++ b/ssg/components.py @@ -0,0 +1,49 @@ +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 _reverse_mapping(components, attribute): + mapping = defaultdict(list) + for component in components.values(): + for item in getattr(component, attribute): + mapping[item].append(component.name) + return mapping + + +def package_component_mapping(components): + return _reverse_mapping(components, "packages") + + +def template_component_mapping(components): + return _reverse_mapping(components, "templates") + + +def group_component_mapping(components): + return _reverse_mapping(components, "groups") + + +def rule_component_mapping(components): + return _reverse_mapping(components, "rules") + + +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", []) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e04f3e2078a..89b3909b749 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -361,8 +361,15 @@ if(PYTHON_VERSION_MAJOR GREATER 2) set_tests_properties("install-vm" PROPERTIES LABELS quick) endif() +if(SSG_PRODUCT_RHEL9) add_test( NAME "automatus-sanity" COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/tests/automatus.py" "--help" ) set_tests_properties("automatus-sanity" PROPERTIES LABELS quick) + +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/data/product_stability/fedora.yml b/tests/data/product_stability/fedora.yml index e65481498b6..70a18dec9fc 100644 --- a/tests/data/product_stability/fedora.yml +++ b/tests/data/product_stability/fedora.yml @@ -6,6 +6,7 @@ basic_properties_derived: true benchmark_id: FEDORA benchmark_root: ../../linux_os/guide chrony_conf_path: /etc/chrony.conf +components_root: ../../components cpes: - fedora_40: check_id: installed_OS_is_fedora diff --git a/tests/data/product_stability/rhel7.yml b/tests/data/product_stability/rhel7.yml index f705070fab3..413af7229a2 100644 --- a/tests/data/product_stability/rhel7.yml +++ b/tests/data/product_stability/rhel7.yml @@ -12,6 +12,7 @@ centos_major_version: '7' centos_pkg_release: 53a7ff4b centos_pkg_version: f4a80eb5 chrony_conf_path: /etc/chrony.conf +components_root: ../../components cpes: - rhel7: check_id: installed_OS_is_rhel7 diff --git a/tests/data/product_stability/rhel8.yml b/tests/data/product_stability/rhel8.yml index cfd8522b5c7..87fc3cc61f8 100644 --- a/tests/data/product_stability/rhel8.yml +++ b/tests/data/product_stability/rhel8.yml @@ -12,6 +12,7 @@ centos_major_version: '8' centos_pkg_release: 5ccc5b19 centos_pkg_version: 8483c65d chrony_conf_path: /etc/chrony.conf +components_root: ../../components cpes: - rhel8: check_id: installed_OS_is_rhel8 diff --git a/tests/data/product_stability/rhel9.yml b/tests/data/product_stability/rhel9.yml index 94b35d203d0..09f3c300e2e 100644 --- a/tests/data/product_stability/rhel9.yml +++ b/tests/data/product_stability/rhel9.yml @@ -12,6 +12,7 @@ centos_major_version: '9' centos_pkg_release: 5ccc5b19 centos_pkg_version: 8483c65d chrony_conf_path: /etc/chrony.conf +components_root: ../../components cpes: - rhel9: check_id: installed_OS_is_rhel9 diff --git a/tests/test_components.py b/tests/test_components.py new file mode 100644 index 00000000000..0db778a6238 --- /dev/null +++ b/tests/test_components.py @@ -0,0 +1,270 @@ +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", required=True) + parser.add_argument( + "--build-dir", help="Path to the build directory", required=True) + parser.add_argument( + "--product", help="Product ID", required=True) + return parser.parse_args() + + +def test_template_name( + template, package_to_component, template_to_component): + template_name = template["name"] + if template_name in template_to_component: + component = template_to_component[template_name][0] + reason = ( + "all rules using template '%s' must be assigned to component " + "'%s'" % (template_name, component)) + return (component, reason) + return None + + +def test_template_package( + template, package_to_component, template_to_component): + template_name = template["name"] + template_vars = template["vars"] + if template_name in ["package_installed", "package_removed"]: + package = template_vars["pkgname"] + component = package_to_component.get(package, [package])[0] + 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)) + return (component, reason) + return None + + +def test_template_service( + template, package_to_component, template_to_component): + template_name = template["name"] + template_vars = template["vars"] + if 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])[0] + 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) + return None + + +template_test_plugins = [ + test_template_name, + test_template_package, + test_template_service, +] + + +def get_component_by_template( + rule, package_to_component, template_to_component): + template = rule.template + if not template: + return None + for plugin_function in template_test_plugins: + plugin_result = plugin_function( + template, package_to_component, template_to_component) + if plugin_result is not None: + return plugin_result + return None + + +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 + sub_outcome = get_component_by_template( + rule, package_to_component, template_to_component) + if sub_outcome is None: + return result + candidate, reason = sub_outcome + if candidate and candidate not in rule_components: + result = False + print( + "Rule '%s' must 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])[0] + if component not in rule_components: + print( + "Rule '%s' must 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) + component_exclusive_platforms = { + "grub2": "grub2", + "sssd-ldap": "sssd" + } + for e_platform, e_component in component_exclusive_platforms.items(): + if platform == e_platform and e_component not in rule_components: + print( + "Rule '%s' must be assigned to component '%s', " + "because it uses the '%s' platform." % + (rule.id_, e_component, e_platform)) + 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' must 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_component_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 = 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_component_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_component_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..ce140eba4df --- /dev/null +++ b/tests/unit/ssg-module/test_components.py @@ -0,0 +1,65 @@ +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_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_component_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