Skip to content

Commit

Permalink
CPE AL: Introduce templated platforms (CPEs)
Browse files Browse the repository at this point in the history
Add the 'package' CPE and 'cond_package' template for it.
Add platforms support to the templates.Builder.
  • Loading branch information
evgenyz committed Nov 29, 2022
1 parent 55461a6 commit aff595e
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 24 deletions.
9 changes: 9 additions & 0 deletions shared/applicability/package.yml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions shared/templates/cond_package/oval.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<def-group>
<definition class="inventory" id="cond_{{{ _RULE_ID }}}"
version="1">
{{{ oval_metadata("The " + pkg_system|upper + " package " + PKGNAME + " should be installed.", affected_platforms=["multi_platform_all"]) }}}
<criteria>
<criterion comment="Conditional package {{{ PKGNAME }}} is installed"
test_ref="cond_test_{{{ _RULE_ID }}}_installed" />
</criteria>
</definition>
{{{ oval_test_package_installed(package=PKGNAME, test_id="cond_test_" + _RULE_ID + "_installed") }}}
</def-group>
2 changes: 2 additions & 0 deletions shared/templates/cond_package/template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
supported_languages:
- oval
15 changes: 11 additions & 4 deletions ssg/build_cpe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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.
"""
Expand All @@ -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",
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 8 additions & 10 deletions ssg/build_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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')
Expand All @@ -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):
Expand Down
20 changes: 20 additions & 0 deletions ssg/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -252,3 +271,4 @@ def build(self):

self.build_extra_ovals()
self.build_all_rules()
self.build_all_platforms()
6 changes: 6 additions & 0 deletions tests/unit/ssg-module/data/applicability/package.yml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions tests/unit/ssg-module/data/package_ntp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: package_ntp
original_expression: package[ntp]
xml_content: <ns0:platform xmlns:ns0="http://cpe.mitre.org/language/2.0" id="package_ntp"><ns0:logical-test
operator="AND" negate="false"><ns0:fact-ref name="cpe:/a:ntp" /></ns0:logical-test></ns0:platform>
bash_conditional: ( ( rpm --quiet -q ntp ) )
ansible_conditional: ( ( "ntp" in ansible_facts.packages ) )
definition_location: ''
documentation_complete: true
title: 'NTP Package'
15 changes: 8 additions & 7 deletions tests/unit/ssg-module/test_build_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
33 changes: 30 additions & 3 deletions tests/unit/ssg-module/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 "<title>%s</title>" % (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 "<title>%s</title>" % (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 "<title>Package %s is installed</title>" % (cpe.template['vars']['pkgname'],) \
in oval_content

0 comments on commit aff595e

Please sign in to comment.