Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor templates v2 #9870

Merged
merged 13 commits into from
Nov 25, 2022
1 change: 0 additions & 1 deletion ssg/build_cpe.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ class CPEItem(XCCDFEntity):

KEYS = dict(
name=lambda: "",
title=lambda: "",
check_id=lambda: "",
bash_conditional=lambda: "",
ansible_conditional=lambda: "",
Expand Down
4 changes: 2 additions & 2 deletions ssg/build_sce.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ def checks(env_yaml, yaml_path, sce_dirs, template_builder, output):

# 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_for_rule(
rule_id, rule.title, rule.template, 'sce-bash')
raw_sce_content = template_builder.get_templatable_lang_contents(rule,
langs['sce-bash'])

ext = '.sh'
filename = rule_id + ext
Expand Down
114 changes: 11 additions & 103 deletions ssg/build_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,51 +44,11 @@
from .shims import unicode_func
import ssg.build_stig

from .entities.common import (
XCCDFEntity,
add_sub_element,
)
from .entities.common import add_sub_element, make_items_product_specific, \
XCCDFEntity, Templatable
from .entities.profile import Profile, ProfileWithInlinePolicies


def add_sub_element(parent, tag, ns, data):
"""
Creates a new child element under parent with tag tag, and sets
data as the content under the tag. In particular, data is a string
to be parsed as an XML tree, allowing sub-elements of children to be
added.

If data should not be parsed as an XML tree, either escape the contents
before passing into this function, or use ElementTree.SubElement().

Returns the newly created subelement of type tag.
"""
namespaced_data = add_xhtml_namespace(data)
# This is used because our YAML data contain XML and XHTML elements
# ET.SubElement() escapes the < > characters by &lt; and &gt;
# and therefore it does not add child elements
# we need to do a hack instead
# TODO: Remove this function after we move to Markdown everywhere in SSG
ustr = unicode_func('<{0} xmlns="{3}" xmlns:xhtml="{2}">{1}</{0}>').format(
tag, namespaced_data, xhtml_namespace, ns)

try:
element = ET.fromstring(ustr.encode("utf-8"))
except Exception:
msg = ("Error adding subelement to an element '{0}' from string: '{1}'"
.format(parent.tag, ustr))
raise RuntimeError(msg)

# Apart from HTML and XML elements the rule descriptions and similar
# also contain <xccdf:sub> elements, where we need to add the prefix
# to create a full reference.
for x in element.findall(".//{%s}sub" % XCCDF12_NS):
x.set("idref", OSCAP_VALUE + x.get("idref"))
x.set("use", "legacy")
parent.append(element)
return element


def reorder_according_to_ordering(unordered, ordering, regex=None):
ordered = []
if regex is None:
Expand Down Expand Up @@ -187,7 +147,6 @@ class Value(XCCDFEntity):
"""Represents XCCDF Value
"""
KEYS = dict(
title=lambda: "",
description=lambda: "",
type=lambda: "",
operator=lambda: "equals",
Expand Down Expand Up @@ -260,7 +219,6 @@ class Benchmark(XCCDFEntity):
"""Represents XCCDF Benchmark
"""
KEYS = dict(
title=lambda: "",
evgenyz marked this conversation as resolved.
Show resolved Hide resolved
status=lambda: "",
description=lambda: "",
notice_id=lambda: "",
Expand Down Expand Up @@ -480,7 +438,6 @@ class Group(XCCDFEntity):

KEYS = dict(
prodtype=lambda: "all",
title=lambda: "",
description=lambda: "",
warnings=lambda: list(),
requires=lambda: list(),
Expand Down Expand Up @@ -692,12 +649,11 @@ def filterfunc(rule):
return filterfunc


class Rule(XCCDFEntity):
class Rule(XCCDFEntity, Templatable):
"""Represents XCCDF Rule
"""
KEYS = dict(
prodtype=lambda: "all",
title=lambda: "",
description=lambda: "",
rationale=lambda: "",
severity=lambda: "",
Expand All @@ -718,12 +674,12 @@ class Rule(XCCDFEntity):
platforms=lambda: set(),
sce_metadata=lambda: dict(),
inherited_platforms=lambda: set(),
template=lambda: None,
cpe_platform_names=lambda: set(),
inherited_cpe_platform_names=lambda: set(),
bash_conditional=lambda: None,
fixes=lambda: dict(),
** XCCDFEntity.KEYS
** XCCDFEntity.KEYS,
** Templatable.KEYS
)

MANDATORY_KEYS = {
Expand All @@ -737,7 +693,6 @@ class Rule(XCCDFEntity):
ID_LABEL = "rule_id"

PRODUCT_REFERENCES = ("stigid", "cis",)
GLOBAL_REFERENCES = ("srg", "vmmsrg", "disa", "cis-csc",)

def __init__(self, id_):
super(Rule, self).__init__(id_)
Expand Down Expand Up @@ -895,21 +850,11 @@ def load_policy_specific_content(self, rule_filename, env_yaml):
env_yaml, policy_specific_content_files)
self.policy_specific_content = policy_specific_content

def make_template_product_specific(self, product):
product_suffix = "@{0}".format(product)

if not self.template:
return

not_specific_vars = self.template.get("vars", dict())
specific_vars = self._make_items_product_specific(
not_specific_vars, product_suffix, True)
self.template["vars"] = specific_vars

not_specific_backends = self.template.get("backends", dict())
specific_backends = self._make_items_product_specific(
not_specific_backends, product_suffix, True)
self.template["backends"] = specific_backends
def get_template_context(self, env_yaml):
ctx = super(Rule, self).get_template_context(env_yaml)
if self.identifiers:
ctx["cce_identifiers"] = self.identifiers
return ctx

def make_refs_and_identifiers_product_specific(self, product):
product_suffix = "@{0}".format(product)
Expand All @@ -933,7 +878,7 @@ def make_refs_and_identifiers_product_specific(self, product):
)
for name, (dic, allow_overwrites) in to_set.items():
try:
new_items = self._make_items_product_specific(
new_items = make_items_product_specific(
dic, product_suffix, allow_overwrites)
except ValueError as exc:
msg = (
Expand All @@ -950,43 +895,6 @@ def make_refs_and_identifiers_product_specific(self, product):

self._verify_stigid_format(product)

def _make_items_product_specific(self, items_dict, product_suffix, allow_overwrites=False):
new_items = dict()
for full_label, value in items_dict.items():
if "@" not in full_label and full_label not in new_items:
new_items[full_label] = value
continue

label = full_label.split("@")[0]

# this test should occur before matching product_suffix with the product qualifier
# present in the reference, so it catches problems even for products that are not
# being built at the moment
if label in Rule.GLOBAL_REFERENCES:
msg = (
"You cannot use product-qualified for the '{item_u}' reference. "
"Please remove the product-qualifier and merge values with the "
"existing reference if there is any. Original line: {item_q}: {value_q}"
.format(item_u=label, item_q=full_label, value_q=value)
)
raise ValueError(msg)

if not full_label.endswith(product_suffix):
continue

if label in items_dict and not allow_overwrites and value != items_dict[label]:
msg = (
"There is a product-qualified '{item_q}' item, "
"but also an unqualified '{item_u}' item "
"and those two differ in value - "
"'{value_q}' vs '{value_u}' respectively."
.format(item_q=full_label, item_u=label,
value_q=value, value_u=items_dict[label])
)
raise ValueError(msg)
new_items[label] = value
return new_items

def validate_identifiers(self, yaml_file):
if self.identifiers is None:
raise ValueError("Empty identifier section in file %s" % yaml_file)
Expand Down
2 changes: 2 additions & 0 deletions ssg/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@
'eks': 'Amazon Elastic Kubernetes Service',
}

# References that can not be used with product-qualifiers
GLOBAL_REFERENCES = ("srg", "vmmsrg", "disa", "cis-csc",)

# Application constants
DEFAULT_GID_MIN = 1000
Expand Down
122 changes: 122 additions & 0 deletions ssg/entities/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,48 @@
XCCDF_REFINABLE_PROPERTIES,
XCCDF12_NS,
OSCAP_VALUE,
GLOBAL_REFERENCES
)


def make_items_product_specific(items_dict, product_suffix, allow_overwrites=False):
new_items = dict()
for full_label, value in items_dict.items():
if "@" not in full_label and full_label not in new_items:
new_items[full_label] = value
continue

label = full_label.split("@")[0]

# This test should occur before matching product_suffix with the product qualifier
# present in the reference, so it catches problems even for products that are not
# being built at the moment
if label in GLOBAL_REFERENCES:
msg = (
"You cannot use product-qualified for the '{item_u}' reference. "
"Please remove the product-qualifier and merge values with the "
"existing reference if there is any. Original line: {item_q}: {value_q}"
.format(item_u=label, item_q=full_label, value_q=value)
)
raise ValueError(msg)

if not full_label.endswith(product_suffix):
continue

if label in items_dict and not allow_overwrites and value != items_dict[label]:
msg = (
"There is a product-qualified '{item_q}' item, "
"but also an unqualified '{item_u}' item "
"and those two differ in value - "
"'{value_q}' vs '{value_u}' respectively."
.format(item_q=full_label, item_u=label,
value_q=value, value_u=items_dict[label])
)
raise ValueError(msg)
new_items[label] = value
return new_items


def add_sub_element(parent, tag, ns, data):
"""
Creates a new child element under parent with tag tag, and sets
Expand Down Expand Up @@ -81,6 +120,7 @@ class XCCDFEntity(object):
"""
KEYS = dict(
id_=lambda: "",
title=lambda: "",
definition_location=lambda: "",
)

Expand Down Expand Up @@ -309,3 +349,85 @@ def update_with(self, rhs):
updated_refinements = self._subtract_refinements(extended_refinements)
updated_refinements.update(self.refine_rules)
self.refine_rules = updated_refinements


class Templatable(object):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to add some doc text here to explain what "Templatable" is.

Copy link
Member Author

@evgenyz evgenyz Nov 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On it.


KEYS = dict(
template=lambda: None,
)

def __init__(self):
pass

def is_templated(self):
return isinstance(self.template, dict)

def get_template_context(self, env_yaml):
# TODO: The first two variables, 'rule_id' and 'rule_title' are expected by some
# templates and macros even if they are not rendered in a rule context.
# Better name for these variables are 'entity_id' and 'entity_title'.
return {
"rule_id": self.id_,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add entity_id as well, so the transition period can start.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather just rename it in a separate self-contained PR, without any backward-compatibility. We have enough legacy lingering around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK.

"rule_title": self.title,
"products": env_yaml["product"],
}

def get_template_name(self):
"""
Given a template dictionary from a Rule instance, determine the name
of the template (from templates) this rule uses.
"""
try:
template_name = self.template["name"]
except KeyError:
raise ValueError(
"Templatable {0} is missing template name under template key".format(self))
return template_name

def get_template_vars(self, env_yaml):
if "vars" not in self.template:
raise ValueError(
"Templatable {0} does not contain mandatory 'vars:' key under "
"'template:' key.".format(self))
template_vars = self.template["vars"]

# Add the rule ID which will be used in template preprocessors (template.py)
# as a unique sub-element for a variety of composite IDs.
# TODO: The name _rule_id is a legacy from the era when rule was the only
# context for a template. Preprocessors implicitly depend on this name.
# A better name is '_entity_id' (as in XCCDF Entity).
template_vars["_rule_id"] = self.id_
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a good comment above it but I don't understand why a _rule_id is add here and at the same time a rule_id is add in get_template_context. Both items are set to self.id_ . I have an impression that finally both of these variables will be passed to the template so it seems to be duplicate. Any thoughts?

Copy link
Member Author

@evgenyz evgenyz Nov 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason all template.py operate with _rule_id and template.oval,ansible,etc along with marcos operate with rule_id.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be no deliberate decision regarding the initial underscore. The variable has been introduced long ago (see https://github.com/ComplianceAsCode/content/blame/453a1d53dc0e2b129fc3704545264dd076e17889/ssg/templates.py#L359) and the leading underscore was probably supplied as a preemptive measure to prevent overwrites. I would drop the assignment here, and also resort to the underscore-less usage elsewhere. We use short IDs everywhere, there is nothing to fear.

Copy link
Member Author

@evgenyz evgenyz Nov 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dictionary element is used in:

  • shared/templates/audit_rules_privileged_commands/template.py
  • shared/templates/yamlfile_value/template.py
  • shared/templates/file_groupowner/template.py
  • shared/templates/file_owner/template.py
  • shared/templates/file_permissions/template.py
  • shared/templates/sysctl/template.py
  • shared/templates/sebool/template.py
  • ssg/utils.py

The utils.py parts are also being called from different template.pys.

What is more important templates.Teamplate.preprocess is executed without extra environment (where rule_id and rule_title reside). On top of that _rule_id is transformed into _RULE_ID as the result of this process. It's a bit messy. So, we can't just drop it.

My suggestion is to leave it for another self-contained round of refactoring.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion is to leave it for another self-contained round of refactoring.

I agree, please don't do this in this PR, we can get to that later, I have created a ticket to track this effort: #9882


return make_items_product_specific(template_vars, env_yaml["product"])

def get_template_backend_langs(self):
"""
Returns list of languages that should be generated from a template
configuration, controlled by backends.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
configuration, controlled by backends.
configuration, controlled by 'backends' key of the template.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"""
from ..templates import LANGUAGES
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the import here and not at the top?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point there was a circular dependency problem. I'll check if it is solved now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is still there. Unfortunately in order to solve it I would have to extract Builder from templates.py. It totally makes sense, but it will mess this PR. How about solving it in the next one?

The problem is that Builder wants build_yaml.Rule, Rule wants common, common wants templates. Also I'm open to alternative layout suggestions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch that. Fixed.

if "backends" in self.template:
backends = self.template["backends"]
for lang in backends:
if lang not in LANGUAGES:
raise RuntimeError("Templatable {0} wants to generate unknown language '{1}"
.format(self, lang))
return [lang for name, lang in LANGUAGES.items() if backends.get(name, "on") == "on"]
return LANGUAGES.values()

def make_template_product_specific(self, product):
if not self.is_templated():
return

product_suffix = "@{0}".format(product)

not_specific_vars = self.template.get("vars", dict())
specific_vars = make_items_product_specific(
not_specific_vars, product_suffix, True)
self.template["vars"] = specific_vars

not_specific_backends = self.template.get("backends", dict())
specific_backends = make_items_product_specific(
not_specific_backends, product_suffix, True)
self.template["backends"] = specific_backends
1 change: 0 additions & 1 deletion ssg/entities/profile_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class Profile(XCCDFEntity, SelectionHandler):
"""Represents XCCDF profile
"""
KEYS = dict(
title=lambda: "",
description=lambda: "",
extends=lambda: "",
metadata=lambda: None,
Expand Down
Loading