From aff595e45298e4cab0ffe3614bb694ca49c9f283 Mon Sep 17 00:00:00 2001 From: Evgeny Kolesnikov Date: Tue, 29 Nov 2022 14:41:29 +0100 Subject: [PATCH] CPE AL: Introduce templated platforms (CPEs) Add the 'package' CPE and 'cond_package' template for it. Add platforms support to the templates.Builder. --- shared/applicability/package.yml | 9 +++++ shared/templates/cond_package/oval.template | 11 +++++++ shared/templates/cond_package/template.yml | 2 ++ ssg/build_cpe.py | 15 ++++++--- ssg/build_yaml.py | 18 +++++----- ssg/templates.py | 20 +++++++++++ .../ssg-module/data/applicability/package.yml | 6 ++++ tests/unit/ssg-module/data/package_ntp.yml | 9 +++++ tests/unit/ssg-module/test_build_yaml.py | 15 +++++---- tests/unit/ssg-module/test_templates.py | 33 +++++++++++++++++-- 10 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 shared/applicability/package.yml create mode 100644 shared/templates/cond_package/oval.template create mode 100644 shared/templates/cond_package/template.yml create mode 100644 tests/unit/ssg-module/data/package_ntp.yml diff --git a/shared/applicability/package.yml b/shared/applicability/package.yml new file mode 100644 index 00000000000..7201e50ef6c --- /dev/null +++ b/shared/applicability/package.yml @@ -0,0 +1,9 @@ +name: "cpe:/a:{arg}" +title: "Package {arg} is installed" +check_id: cond_package_{arg} +template: + name: cond_package +args: + ntp: + pkgname: ntp + title: NTP daemon and utilities diff --git a/shared/templates/cond_package/oval.template b/shared/templates/cond_package/oval.template new file mode 100644 index 00000000000..7f071d79392 --- /dev/null +++ b/shared/templates/cond_package/oval.template @@ -0,0 +1,11 @@ + + + {{{ oval_metadata("The " + pkg_system|upper + " package " + PKGNAME + " should be installed.", affected_platforms=["multi_platform_all"]) }}} + + + + +{{{ oval_test_package_installed(package=PKGNAME, test_id="cond_test_" + _RULE_ID + "_installed") }}} + diff --git a/shared/templates/cond_package/template.yml b/shared/templates/cond_package/template.yml new file mode 100644 index 00000000000..2f6f2d2c7cb --- /dev/null +++ b/shared/templates/cond_package/template.yml @@ -0,0 +1,2 @@ +supported_languages: + - oval diff --git a/ssg/build_cpe.py b/ssg/build_cpe.py index 0a694490ffe..293001f2ab7 100644 --- a/ssg/build_cpe.py +++ b/ssg/build_cpe.py @@ -13,7 +13,7 @@ from .xml import ElementTree as ET from .boolean_expression import Algebra, Symbol, Function from .boolean_expression import get_base_name_of_parametrized_platform -from .entities.common import XCCDFEntity +from .entities.common import XCCDFEntity, Templatable from .yaml import convert_string_to_bool @@ -133,7 +133,7 @@ def to_file(self, file_name, cpe_oval_file): tree.write(file_name, encoding="utf-8") -class CPEItem(XCCDFEntity): +class CPEItem(XCCDFEntity, Templatable): """ Represents the cpe-item element from the CPE standard. """ @@ -144,8 +144,10 @@ class CPEItem(XCCDFEntity): bash_conditional=lambda: "", ansible_conditional=lambda: "", is_product_cpe=lambda: False, + args=lambda: {}, ** XCCDFEntity.KEYS ) + KEYS.update(**Templatable.KEYS) MANDATORY_KEYS = [ "name", @@ -264,10 +266,15 @@ def enrich_with_cpe_info(self, cpe_products): def pass_parameters(self, product_cpes): if self.arg: - associated_cpe_item_as_dict = product_cpes.get_cpe(self.cpe_name).represent_as_dict() + cpe = product_cpes.get_cpe(self.cpe_name) + parameters = cpe.args[self.arg] + parameters.update(self.as_dict()) + associated_cpe_item_as_dict = cpe.represent_as_dict() new_associated_cpe_item_as_dict = apply_formatting_on_dict_values( - associated_cpe_item_as_dict, self.as_dict()) + associated_cpe_item_as_dict, parameters) new_associated_cpe_item_as_dict["id_"] = self.as_id() + if cpe.is_templated(): + new_associated_cpe_item_as_dict["template"]["vars"] = parameters new_associated_cpe_item = CPEItem.get_instance_from_full_dict( new_associated_cpe_item_as_dict) product_cpes.add_cpe_item(new_associated_cpe_item) diff --git a/ssg/build_yaml.py b/ssg/build_yaml.py index c87ccf8444c..1737076c6e7 100644 --- a/ssg/build_yaml.py +++ b/ssg/build_yaml.py @@ -1512,14 +1512,13 @@ class Platform(XCCDFEntity): def from_text(cls, expression, product_cpes): if not product_cpes: return None - test = product_cpes.algebra.parse( - expression, simplify=True) - id = test.as_id() - platform = cls(id) + test = product_cpes.algebra.parse(expression, simplify=True) + id_ = test.as_id() + platform = cls(id_) platform.test = test platform.test.pass_parameters(product_cpes) platform.test.enrich_with_cpe_info(product_cpes) - platform.name = id + platform.name = id_ platform.original_expression = expression platform.xml_content = platform.get_xml() platform.bash_conditional = platform.test.to_bash_conditional() @@ -1529,8 +1528,8 @@ def from_text(cls, expression, product_cpes): def get_xml(self): cpe_platform = ET.Element("{%s}platform" % Platform.ns) cpe_platform.set('id', self.name) - # in case the platform contains only single CPE name, fake the logical test - # we have to athere to CPE specification + # In case the platform contains only single CPE name, fake the logical test + # we have to adhere to CPE specification if isinstance(self.test, CPEALFactRef): cpe_test = ET.Element("{%s}logical-test" % CPEALLogicalTest.ns) cpe_test.set('operator', 'AND') @@ -1557,11 +1556,10 @@ def get_remediation_conditional(self, language): def from_yaml(cls, yaml_file, env_yaml=None, product_cpes=None): platform = super(Platform, cls).from_yaml(yaml_file, env_yaml) platform.xml_content = ET.fromstring(platform.xml_content) - # if we did receive a product_cpes, we can restore also the original test object + # If we received a product_cpes, we can restore also the original test object # it can be later used e.g. for comparison if product_cpes: - platform.test = product_cpes.algebra.parse( - platform.original_expression, simplify=True) + platform.test = product_cpes.algebra.parse(platform.original_expression, simplify=True) return platform def __eq__(self, other): diff --git a/ssg/templates.py b/ssg/templates.py index 268c30cabc9..27a221a7d18 100644 --- a/ssg/templates.py +++ b/ssg/templates.py @@ -207,6 +207,18 @@ def build_lang_for_templatable(self, templatable, lang): filled_template = self.get_lang_contents_for_templatable(templatable, lang) self.write_lang_contents_for_templatable(filled_template, lang, templatable) + def build_platform(self, platform): + """ + Builds templated content of a given Platform (all CPEs/Symbols) for all available + languages, writing the output to the correct build directories. + """ + for symbol in platform.test.get_symbols(): + platform.test.pass_parameters(self.product_cpes) + cpe = self.product_cpes.get_cpe(symbol.as_id()) + if cpe.is_templated(): + for lang in self.get_resolved_langs_to_generate(cpe): + self.build_lang_for_templatable(cpe, lang) + def build_rule(self, rule): """ Builds templated content of a given Rule for all available languages, @@ -230,6 +242,13 @@ def build_extra_ovals(self): }) self.build_lang_for_templatable(rule, LANGUAGES["oval"]) + def build_all_platforms(self): + for platform_file in sorted(os.listdir(self.platforms_dir)): + platform_path = os.path.join(self.platforms_dir, platform_file) + platform = ssg.build_yaml.Platform.from_yaml(platform_path, self.env_yaml, + self.product_cpes) + self.build_platform(platform) + def build_all_rules(self): for rule_file in sorted(os.listdir(self.resolved_rules_dir)): rule_path = os.path.join(self.resolved_rules_dir, rule_file) @@ -252,3 +271,4 @@ def build(self): self.build_extra_ovals() self.build_all_rules() + self.build_all_platforms() diff --git a/tests/unit/ssg-module/data/applicability/package.yml b/tests/unit/ssg-module/data/applicability/package.yml index 4c842bfd164..38fbc270f7b 100644 --- a/tests/unit/ssg-module/data/applicability/package.yml +++ b/tests/unit/ssg-module/data/applicability/package.yml @@ -1,3 +1,9 @@ name: "cpe:/a:{arg}" title: "Package {arg} is installed" check_id: installed_env_has_{arg}_package +template: + name: package_installed +args: + ntp: + pkgname: ntp + title: NTP diff --git a/tests/unit/ssg-module/data/package_ntp.yml b/tests/unit/ssg-module/data/package_ntp.yml new file mode 100644 index 00000000000..cfcc7f7f258 --- /dev/null +++ b/tests/unit/ssg-module/data/package_ntp.yml @@ -0,0 +1,9 @@ +name: package_ntp +original_expression: package[ntp] +xml_content: +bash_conditional: ( ( rpm --quiet -q ntp ) ) +ansible_conditional: ( ( "ntp" in ansible_facts.packages ) ) +definition_location: '' +documentation_complete: true +title: 'NTP Package' diff --git a/tests/unit/ssg-module/test_build_yaml.py b/tests/unit/ssg-module/test_build_yaml.py index d4229a93d85..b99f0d410f9 100644 --- a/tests/unit/ssg-module/test_build_yaml.py +++ b/tests/unit/ssg-module/test_build_yaml.py @@ -362,21 +362,22 @@ def test_platform_as_dict(product_cpes): assert d["bash_conditional"] == "( rpm --quiet -q chrony )" assert "xml_content" in d + def test_platform_get_invalid_conditional_language(product_cpes): platform = ssg.build_yaml.Platform.from_text("ntp or chrony", product_cpes) with pytest.raises(AttributeError): assert platform.get_remediation_conditional("foo") + def test_parametrized_platform(product_cpes): - platform = ssg.build_yaml.Platform.from_text("package[test]", product_cpes) + platform = ssg.build_yaml.Platform.from_text("package[ntp]", product_cpes) assert platform.test.cpe_name != "cpe:/a:{arg}" - assert platform.test.cpe_name == "cpe:/a:test" + assert platform.test.cpe_name == "cpe:/a:ntp" cpe_item = product_cpes.get_cpe(platform.test.cpe_name) - assert cpe_item.name == "cpe:/a:test" - assert cpe_item.title == "Package test is installed" - assert cpe_item.check_id == "installed_env_has_test_package" - - + assert cpe_item.id_ == "package_ntp" + assert cpe_item.name == "cpe:/a:ntp" + assert cpe_item.title == "Package ntp is installed" + assert cpe_item.check_id == "installed_env_has_ntp_package" def test_derive_id_from_file_name(): diff --git a/tests/unit/ssg-module/test_templates.py b/tests/unit/ssg-module/test_templates.py index 723e749df1a..6c21c6cf960 100644 --- a/tests/unit/ssg-module/test_templates.py +++ b/tests/unit/ssg-module/test_templates.py @@ -13,6 +13,7 @@ ssg_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) DATADIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) templates_dir = os.path.join(DATADIR, "templates") +platforms_dir = os.path.join(DATADIR) cpe_items_dir = os.path.join(DATADIR, "applicability") build_config_yaml = os.path.join(ssg_root, "build", "build_config.yml") @@ -33,6 +34,32 @@ def test_render_extra_ovals(): "title": oval_def_id, "template": template, }) - oval_content = builder.get_lang_contents_for_templatable(rule, - ssg.templates.LANGUAGES["oval"]) - assert "%s" % (oval_def_id,) in oval_content + oval_content = builder.get_lang_contents_for_templatable( + rule, ssg.templates.LANGUAGES["oval"]) + assert 'id="package_%s_installed"' % (rule.template['vars']['pkgname']) \ + in oval_content + + assert "%s" % (oval_def_id,) \ + in oval_content + + +def test_platform_templates(): + builder = ssg.templates.Builder( + env_yaml, '', templates_dir, + '', '', platforms_dir, cpe_items_dir) + + platform_path = os.path.join(builder.platforms_dir, "package_ntp.yml") + platform = ssg.build_yaml.Platform.from_yaml(platform_path, builder.env_yaml, + builder.product_cpes) + for symbol in platform.test.get_symbols(): + platform.test.pass_parameters(builder.product_cpes) + cpe = builder.product_cpes.get_cpe(symbol.as_id()) + if cpe.is_templated(): + oval_content = builder.get_lang_contents_for_templatable( + cpe, ssg.templates.LANGUAGES["oval"]) + + assert 'id="package_%s"' % (cpe.template['vars']['pkgname']) \ + in oval_content + + assert "Package %s is installed" % (cpe.template['vars']['pkgname'],) \ + in oval_content