1838 lines
83 KiB
Diff
1838 lines
83 KiB
Diff
From 1036c6b55b95a27be57b065a4b9acfecc83639b3 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Tue, 6 Jul 2021 16:02:03 -0400
|
|
Subject: [PATCH 01/19] Add tests for package_installed template
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
.../package_installed/tests/package-installed-removed.fail.sh | 4 ++++
|
|
.../package_installed/tests/package-installed.pass.sh | 3 +++
|
|
.../templates/package_installed/tests/package-removed.fail.sh | 3 +++
|
|
3 files changed, 10 insertions(+)
|
|
create mode 100644 shared/templates/package_installed/tests/package-installed-removed.fail.sh
|
|
create mode 100644 shared/templates/package_installed/tests/package-installed.pass.sh
|
|
create mode 100644 shared/templates/package_installed/tests/package-removed.fail.sh
|
|
|
|
diff --git a/shared/templates/package_installed/tests/package-installed-removed.fail.sh b/shared/templates/package_installed/tests/package-installed-removed.fail.sh
|
|
new file mode 100644
|
|
index 00000000000..1ce59225303
|
|
--- /dev/null
|
|
+++ b/shared/templates/package_installed/tests/package-installed-removed.fail.sh
|
|
@@ -0,0 +1,4 @@
|
|
+#!/bin/bash
|
|
+
|
|
+{{{ bash_package_install(PKGNAME) }}}
|
|
+{{{ bash_package_remove(PKGNAME) }}}
|
|
diff --git a/shared/templates/package_installed/tests/package-installed.pass.sh b/shared/templates/package_installed/tests/package-installed.pass.sh
|
|
new file mode 100644
|
|
index 00000000000..2a0506c57fd
|
|
--- /dev/null
|
|
+++ b/shared/templates/package_installed/tests/package-installed.pass.sh
|
|
@@ -0,0 +1,3 @@
|
|
+#!/bin/bash
|
|
+
|
|
+{{{ bash_package_install(PKGNAME) }}}
|
|
diff --git a/shared/templates/package_installed/tests/package-removed.fail.sh b/shared/templates/package_installed/tests/package-removed.fail.sh
|
|
new file mode 100644
|
|
index 00000000000..c2838396f56
|
|
--- /dev/null
|
|
+++ b/shared/templates/package_installed/tests/package-removed.fail.sh
|
|
@@ -0,0 +1,3 @@
|
|
+#!/bin/bash
|
|
+
|
|
+{{{ bash_package_remove(PKGNAME) }}}
|
|
|
|
From 3008339e7779b144a2b7fa72854d1185e33438f5 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Tue, 6 Jul 2021 16:16:56 -0400
|
|
Subject: [PATCH 02/19] Refactor ssg/templates.py generation (SCE)
|
|
|
|
This refactors SSG's template generation code to better align between
|
|
where we are now upstream and future changes related to templated SCE
|
|
support.
|
|
|
|
At this point in time, we wish to make some changes to allow building
|
|
templated (both in the Jinja and shared/templates senses) test cases,
|
|
and many of the SCE-enablement changes are relevant, I chose to pull
|
|
them in almost entirely. The original commit message is preserved below:
|
|
|
|
Refactor template generation for SCEs
|
|
|
|
ssg/templates.py holds the core of template generation logic. We
|
|
will need a template_builder for SCE (so we can load SCE metadata
|
|
from templated content) but we do not wish to write this SCE content
|
|
out immediately; instead, we wish to wait until
|
|
build-scripts/build_templated_content.py is run.
|
|
|
|
We refactor to make the resolved languages for a rule (the
|
|
intersection between theoretical languages allowed by the rule and
|
|
the actual languages allowed by the template) accessible to callers,
|
|
and, also allow reading just a single templated language artifact
|
|
into memory (instead of from disk).
|
|
|
|
Notably, this last change is most useful to us; we don't want to put the
|
|
file in a template/build-system specified location; we wish to control
|
|
its location exactly here.
|
|
|
|
Note that no changes to ssg/templates.py for the test suite change were
|
|
done here; this was purely a change to sync future changes for SCE
|
|
content.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
ssg/templates.py | 112 +++++++++++++++++++++++++++++++++++++----------
|
|
1 file changed, 89 insertions(+), 23 deletions(-)
|
|
|
|
diff --git a/ssg/templates.py b/ssg/templates.py
|
|
index 6783d0f2d5e..47a8a5eb852 100644
|
|
--- a/ssg/templates.py
|
|
+++ b/ssg/templates.py
|
|
@@ -107,14 +107,17 @@ def __init__(
|
|
self.checks_dir = checks_dir
|
|
self.output_dirs = dict()
|
|
for lang in languages:
|
|
+ lang_dir = lang
|
|
if lang == "oval":
|
|
# OVAL checks need to be put to a different directory because
|
|
- # they are processed differently than remediations later in the
|
|
- # build process
|
|
+ # they are processed differently than remediations later in
|
|
+ # the build process
|
|
output_dir = self.checks_dir
|
|
+ if lang.startswith("sce-"):
|
|
+ lang_dir = "sce"
|
|
else:
|
|
output_dir = self.remediations_dir
|
|
- dir_ = os.path.join(output_dir, lang)
|
|
+ dir_ = os.path.join(output_dir, lang_dir)
|
|
self.output_dirs[lang] = dir_
|
|
# scan directory structure and dynamically create list of templates
|
|
for item in sorted(os.listdir(self.templates_dir)):
|
|
@@ -124,25 +127,41 @@ def __init__(
|
|
maybe_template.load()
|
|
templates[item] = maybe_template
|
|
|
|
+ def build_lang_file(
|
|
+ self, rule_id, template_name, template_vars, lang, local_env_yaml):
|
|
+ """
|
|
+ Builds and returns templated content for a given rule for a given
|
|
+ language; does not write the output to disk.
|
|
+ """
|
|
+ if lang not in templates[template_name].langs:
|
|
+ return None
|
|
+
|
|
+ template_file_name = lang + ".template"
|
|
+ template_file_path = os.path.join(self.templates_dir, template_name, template_file_name)
|
|
+ template_parameters = templates[template_name].preprocess(template_vars, lang)
|
|
+ jinja_dict = ssg.utils.merge_dicts(local_env_yaml, template_parameters)
|
|
+ filled_template = ssg.jinja.process_file_with_macros(
|
|
+ template_file_path, jinja_dict)
|
|
+
|
|
+ return filled_template
|
|
|
|
def build_lang(
|
|
- self, rule_id, template_name, template_vars, lang, local_env_yaml):
|
|
+ self, rule_id, template_name, template_vars, lang, local_env_yaml, platforms=None):
|
|
"""
|
|
Builds templated content for a given rule for a given language.
|
|
Writes the output to the correct build directories.
|
|
"""
|
|
if lang not in templates[template_name].langs:
|
|
return
|
|
- template_file_name = lang + ".template"
|
|
- template_file_path = os.path.join(self.templates_dir, template_name, template_file_name)
|
|
+
|
|
+ filled_template = self.build_lang_file(rule_id, template_name,
|
|
+ template_vars, lang, local_env_yaml)
|
|
+
|
|
ext = lang_to_ext_map[lang]
|
|
output_file_name = rule_id + ext
|
|
output_filepath = os.path.join(
|
|
self.output_dirs[lang], output_file_name)
|
|
- template_parameters = templates[template_name].preprocess(template_vars, lang)
|
|
- jinja_dict = ssg.utils.merge_dicts(local_env_yaml, template_parameters)
|
|
- filled_template = ssg.jinja.process_file_with_macros(
|
|
- template_file_path, jinja_dict)
|
|
+
|
|
with open(output_filepath, "w") as f:
|
|
f.write(filled_template)
|
|
|
|
@@ -168,6 +187,37 @@ def get_langs_to_generate(self, rule):
|
|
else:
|
|
return languages
|
|
|
|
+ def get_template_name(self, template):
|
|
+ """
|
|
+ Given a template dictionary from a Rule instance, determine the name
|
|
+ of the template (from templates) this rule uses.
|
|
+ """
|
|
+ try:
|
|
+ template_name = template["name"]
|
|
+ except KeyError:
|
|
+ raise ValueError(
|
|
+ "Rule {0} is missing template name under template key".format(
|
|
+ rule_id))
|
|
+ if template_name not in templates.keys():
|
|
+ raise ValueError(
|
|
+ "Rule {0} uses template {1} which does not exist.".format(
|
|
+ rule_id, template_name))
|
|
+ return template_name
|
|
+
|
|
+ def get_resolved_langs_to_generate(self, rule):
|
|
+ """
|
|
+ Given a specific Rule instance, determine which languages are
|
|
+ generated by the combination of the rule's template_backends AND
|
|
+ the rule's template keys.
|
|
+ """
|
|
+ if rule.template is None:
|
|
+ return None
|
|
+
|
|
+ rule_langs = set(self.get_langs_to_generate(rule))
|
|
+ template_name = self.get_template_name(rule.template)
|
|
+ template_langs = set(templates[template_name].langs)
|
|
+ return rule_langs.intersection(template_langs)
|
|
+
|
|
def process_product_vars(self, all_variables):
|
|
"""
|
|
Given a dictionary with the format key[@<product>]=value, filter out
|
|
@@ -183,21 +233,12 @@ def process_product_vars(self, all_variables):
|
|
|
|
return processed
|
|
|
|
- def build_rule(self, rule_id, rule_title, template, langs_to_generate):
|
|
+ def build_rule(self, rule_id, rule_title, template, langs_to_generate, platforms=None):
|
|
"""
|
|
Builds templated content for a given rule for selected languages,
|
|
writing the output to the correct build directories.
|
|
"""
|
|
- try:
|
|
- template_name = template["name"]
|
|
- except KeyError:
|
|
- raise ValueError(
|
|
- "Rule {0} is missing template name under template key".format(
|
|
- rule_id))
|
|
- if template_name not in templates.keys():
|
|
- raise ValueError(
|
|
- "Rule {0} uses template {1} which does not exist.".format(
|
|
- rule_id, template_name))
|
|
+ template_name = self.get_template_name(template)
|
|
try:
|
|
template_vars = self.process_product_vars(template["vars"])
|
|
except KeyError:
|
|
@@ -216,11 +257,36 @@ def build_rule(self, rule_id, rule_title, template, langs_to_generate):
|
|
for lang in langs_to_generate:
|
|
try:
|
|
self.build_lang(
|
|
- rule_id, template_name, template_vars, lang, local_env_yaml)
|
|
+ rule_id, template_name, template_vars, lang, local_env_yaml, platforms)
|
|
except Exception as e:
|
|
print("Error building templated {0} content for rule {1}".format(lang, rule_id), file=sys.stderr)
|
|
raise e
|
|
|
|
+ def get_lang_for_rule(self, rule_id, rule_title, template, language):
|
|
+ """
|
|
+ For the specified rule, build and return only the specified language
|
|
+ content.
|
|
+ """
|
|
+ template_name = self.get_template_name(template)
|
|
+ try:
|
|
+ template_vars = self.process_product_vars(template["vars"])
|
|
+ except KeyError:
|
|
+ raise ValueError(
|
|
+ "Rule {0} does not contain mandatory 'vars:' key under "
|
|
+ "'template:' key.".format(rule_id))
|
|
+ # Add the rule ID which will be reused in OVAL templates as OVAL
|
|
+ # definition ID so that the build system matches the generated
|
|
+ # check with the rule.
|
|
+ template_vars["_rule_id"] = rule_id
|
|
+ # checks and remediations are processed with a custom YAML dict
|
|
+ local_env_yaml = self.env_yaml.copy()
|
|
+ local_env_yaml["rule_id"] = rule_id
|
|
+ local_env_yaml["rule_title"] = rule_title
|
|
+ local_env_yaml["products"] = self.env_yaml["product"]
|
|
+
|
|
+ return self.build_lang_file(rule_id, template_name, template_vars,
|
|
+ language, local_env_yaml)
|
|
+
|
|
def build_extra_ovals(self):
|
|
declaration_path = os.path.join(self.templates_dir, "extra_ovals.yml")
|
|
declaration = ssg.yaml.open_raw(declaration_path)
|
|
@@ -245,7 +311,7 @@ def build_all_rules(self):
|
|
continue
|
|
langs_to_generate = self.get_langs_to_generate(rule)
|
|
self.build_rule(
|
|
- rule.id_, rule.title, rule.template, langs_to_generate)
|
|
+ rule.id_, rule.title, rule.template, langs_to_generate, platforms=rule.platforms)
|
|
|
|
def build(self):
|
|
"""
|
|
|
|
From 8da04c6a85d40432a19904b16251885e12b75999 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Tue, 6 Jul 2021 16:50:06 -0400
|
|
Subject: [PATCH 03/19] Remove incorrect tests for package installation
|
|
|
|
These tests assume that the package manager is yum (and forcibly
|
|
installs it) in some cases. This doesn't work on Debian-like systems;
|
|
prefer the templated version instead, which uses the
|
|
bash_package_install Jinja macro instead (and uses the
|
|
bash_package_remove macro for the negative case)
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
.../ntp/package_chrony_installed/tests/installed.pass.sh | 4 ----
|
|
.../ntp/package_chrony_installed/tests/removed.fail.sh | 3 ---
|
|
.../package_audit_installed/tests/installed.pass.sh | 4 ----
|
|
.../package_audit_installed/tests/removed.fail.sh | 3 ---
|
|
.../package_rsyslog_installed/tests/installed.pass.sh | 3 ---
|
|
.../package_rsyslog_installed/tests/notinstalled.fail.sh | 3 ---
|
|
.../package_firewalld_installed/tests/installed.pass.sh | 4 ----
|
|
.../package_firewalld_installed/tests/removed.fail.sh | 3 ---
|
|
.../package_libselinux_installed/tests/installed.pass.sh | 4 ----
|
|
.../aide/package_aide_installed/tests/installed.pass.sh | 3 ---
|
|
.../aide/package_aide_installed/tests/notinstalled.fail.sh | 3 ---
|
|
.../sudo/package_sudo_installed/tests/installed.pass.sh | 4 ----
|
|
.../sudo/package_sudo_installed/tests/removed.fail.sh | 3 ---
|
|
13 files changed, 53 deletions(-)
|
|
delete mode 100644 linux_os/guide/services/ntp/package_chrony_installed/tests/installed.pass.sh
|
|
delete mode 100644 linux_os/guide/services/ntp/package_chrony_installed/tests/removed.fail.sh
|
|
delete mode 100644 linux_os/guide/system/auditing/package_audit_installed/tests/installed.pass.sh
|
|
delete mode 100644 linux_os/guide/system/auditing/package_audit_installed/tests/removed.fail.sh
|
|
delete mode 100644 linux_os/guide/system/logging/package_rsyslog_installed/tests/installed.pass.sh
|
|
delete mode 100644 linux_os/guide/system/logging/package_rsyslog_installed/tests/notinstalled.fail.sh
|
|
delete mode 100644 linux_os/guide/system/network/network-firewalld/firewalld_activation/package_firewalld_installed/tests/installed.pass.sh
|
|
delete mode 100644 linux_os/guide/system/network/network-firewalld/firewalld_activation/package_firewalld_installed/tests/removed.fail.sh
|
|
delete mode 100644 linux_os/guide/system/selinux/package_libselinux_installed/tests/installed.pass.sh
|
|
delete mode 100644 linux_os/guide/system/software/integrity/software-integrity/aide/package_aide_installed/tests/installed.pass.sh
|
|
delete mode 100644 linux_os/guide/system/software/integrity/software-integrity/aide/package_aide_installed/tests/notinstalled.fail.sh
|
|
delete mode 100644 linux_os/guide/system/software/sudo/package_sudo_installed/tests/installed.pass.sh
|
|
delete mode 100644 linux_os/guide/system/software/sudo/package_sudo_installed/tests/removed.fail.sh
|
|
|
|
diff --git a/linux_os/guide/services/ntp/package_chrony_installed/tests/installed.pass.sh b/linux_os/guide/services/ntp/package_chrony_installed/tests/installed.pass.sh
|
|
deleted file mode 100644
|
|
index c692df2b282..00000000000
|
|
--- a/linux_os/guide/services/ntp/package_chrony_installed/tests/installed.pass.sh
|
|
+++ /dev/null
|
|
@@ -1,4 +0,0 @@
|
|
-#!/bin/bash
|
|
-# package = yum
|
|
-
|
|
-yum install -y chrony
|
|
diff --git a/linux_os/guide/services/ntp/package_chrony_installed/tests/removed.fail.sh b/linux_os/guide/services/ntp/package_chrony_installed/tests/removed.fail.sh
|
|
deleted file mode 100644
|
|
index eba551f99d4..00000000000
|
|
--- a/linux_os/guide/services/ntp/package_chrony_installed/tests/removed.fail.sh
|
|
+++ /dev/null
|
|
@@ -1,3 +0,0 @@
|
|
-#!/bin/bash
|
|
-
|
|
-yum remove -y chrony
|
|
diff --git a/linux_os/guide/system/auditing/package_audit_installed/tests/installed.pass.sh b/linux_os/guide/system/auditing/package_audit_installed/tests/installed.pass.sh
|
|
deleted file mode 100644
|
|
index 1c19ce31a87..00000000000
|
|
--- a/linux_os/guide/system/auditing/package_audit_installed/tests/installed.pass.sh
|
|
+++ /dev/null
|
|
@@ -1,4 +0,0 @@
|
|
-#!/bin/bash
|
|
-# package = yum
|
|
-
|
|
-yum install -y audit
|
|
diff --git a/linux_os/guide/system/auditing/package_audit_installed/tests/removed.fail.sh b/linux_os/guide/system/auditing/package_audit_installed/tests/removed.fail.sh
|
|
deleted file mode 100644
|
|
index 45dd05a9638..00000000000
|
|
--- a/linux_os/guide/system/auditing/package_audit_installed/tests/removed.fail.sh
|
|
+++ /dev/null
|
|
@@ -1,3 +0,0 @@
|
|
-#!/bin/bash
|
|
-
|
|
-yum remove -y audit
|
|
diff --git a/linux_os/guide/system/logging/package_rsyslog_installed/tests/installed.pass.sh b/linux_os/guide/system/logging/package_rsyslog_installed/tests/installed.pass.sh
|
|
deleted file mode 100644
|
|
index 6e4107fa3bd..00000000000
|
|
--- a/linux_os/guide/system/logging/package_rsyslog_installed/tests/installed.pass.sh
|
|
+++ /dev/null
|
|
@@ -1,3 +0,0 @@
|
|
-#!/bin/bash
|
|
-# packages = rsyslog
|
|
-
|
|
diff --git a/linux_os/guide/system/logging/package_rsyslog_installed/tests/notinstalled.fail.sh b/linux_os/guide/system/logging/package_rsyslog_installed/tests/notinstalled.fail.sh
|
|
deleted file mode 100644
|
|
index f64bf1b340a..00000000000
|
|
--- a/linux_os/guide/system/logging/package_rsyslog_installed/tests/notinstalled.fail.sh
|
|
+++ /dev/null
|
|
@@ -1,3 +0,0 @@
|
|
-#!/bin/bash
|
|
-
|
|
-yum remove -y rsyslog
|
|
diff --git a/linux_os/guide/system/network/network-firewalld/firewalld_activation/package_firewalld_installed/tests/installed.pass.sh b/linux_os/guide/system/network/network-firewalld/firewalld_activation/package_firewalld_installed/tests/installed.pass.sh
|
|
deleted file mode 100644
|
|
index 7a4748863bf..00000000000
|
|
--- a/linux_os/guide/system/network/network-firewalld/firewalld_activation/package_firewalld_installed/tests/installed.pass.sh
|
|
+++ /dev/null
|
|
@@ -1,4 +0,0 @@
|
|
-#!/bin/bash
|
|
-# package = yum
|
|
-
|
|
-yum install -y firewalld
|
|
diff --git a/linux_os/guide/system/network/network-firewalld/firewalld_activation/package_firewalld_installed/tests/removed.fail.sh b/linux_os/guide/system/network/network-firewalld/firewalld_activation/package_firewalld_installed/tests/removed.fail.sh
|
|
deleted file mode 100644
|
|
index a9107df7de8..00000000000
|
|
--- a/linux_os/guide/system/network/network-firewalld/firewalld_activation/package_firewalld_installed/tests/removed.fail.sh
|
|
+++ /dev/null
|
|
@@ -1,3 +0,0 @@
|
|
-#!/bin/bash
|
|
-
|
|
-yum remove -y firewalld
|
|
diff --git a/linux_os/guide/system/selinux/package_libselinux_installed/tests/installed.pass.sh b/linux_os/guide/system/selinux/package_libselinux_installed/tests/installed.pass.sh
|
|
deleted file mode 100644
|
|
index 5d30cf77841..00000000000
|
|
--- a/linux_os/guide/system/selinux/package_libselinux_installed/tests/installed.pass.sh
|
|
+++ /dev/null
|
|
@@ -1,4 +0,0 @@
|
|
-#!/bin/bash
|
|
-# package = yum
|
|
-
|
|
-yum install -y libselinux
|
|
diff --git a/linux_os/guide/system/software/integrity/software-integrity/aide/package_aide_installed/tests/installed.pass.sh b/linux_os/guide/system/software/integrity/software-integrity/aide/package_aide_installed/tests/installed.pass.sh
|
|
deleted file mode 100644
|
|
index fa8b85b..0000000
|
|
--- a/linux_os/guide/system/software/integrity/software-integrity/aide/package_aide_installed/tests/installed.pass.sh
|
|
+++ /dev/null
|
|
@@ -1,3 +0,0 @@
|
|
-#!/bin/bash
|
|
-# packages = aide
|
|
-
|
|
diff --git a/linux_os/guide/system/software/integrity/software-integrity/aide/package_aide_installed/tests/notinstalled.fail.sh b/linux_os/guide/system/software/integrity/software-integrity/aide/package_aide_installed/tests/notinstalled.fail.sh
|
|
deleted file mode 100644
|
|
index 75978b6..0000000
|
|
--- a/linux_os/guide/system/software/integrity/software-integrity/aide/package_aide_installed/tests/notinstalled.fail.sh
|
|
+++ /dev/null
|
|
@@ -1,3 +0,0 @@
|
|
-#!/bin/bash
|
|
-
|
|
-yum remove -y aide
|
|
diff --git a/linux_os/guide/system/software/sudo/package_sudo_installed/tests/installed.pass.sh b/linux_os/guide/system/software/sudo/package_sudo_installed/tests/installed.pass.sh
|
|
deleted file mode 100644
|
|
index dafc6998a9a..00000000000
|
|
--- a/linux_os/guide/system/software/sudo/package_sudo_installed/tests/installed.pass.sh
|
|
+++ /dev/null
|
|
@@ -1,4 +0,0 @@
|
|
-#!/bin/bash
|
|
-# package = yum
|
|
-
|
|
-yum install -y sudo
|
|
diff --git a/linux_os/guide/system/software/sudo/package_sudo_installed/tests/removed.fail.sh b/linux_os/guide/system/software/sudo/package_sudo_installed/tests/removed.fail.sh
|
|
deleted file mode 100644
|
|
index d22b562c15e..00000000000
|
|
--- a/linux_os/guide/system/software/sudo/package_sudo_installed/tests/removed.fail.sh
|
|
+++ /dev/null
|
|
@@ -1,3 +0,0 @@
|
|
-#!/bin/bash
|
|
-
|
|
-rpm -e --nodeps sudo
|
|
|
|
From 6c49dcb79aaf5b239f09ecfdac07b443736b5b5e Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Tue, 6 Jul 2021 16:51:05 -0400
|
|
Subject: [PATCH 04/19] Support templating test content
|
|
|
|
We introduced a new method to the template builder in ssg/templates.py
|
|
for finding (and building) test content under shared/templates/...
|
|
This uses Jinja macros and has the full context of the template present,
|
|
so for instance, package_installed tests can use the correct package
|
|
name on the target platform.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
ssg/templates.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++
|
|
1 file changed, 46 insertions(+)
|
|
|
|
diff --git a/ssg/templates.py b/ssg/templates.py
|
|
index 47a8a5eb852..0a39c3ba094 100644
|
|
--- a/ssg/templates.py
|
|
+++ b/ssg/templates.py
|
|
@@ -145,6 +145,52 @@ def build_lang_file(
|
|
|
|
return filled_template
|
|
|
|
+ def get_all_tests(
|
|
+ self, rule_id, rule_template, local_env_yaml, platforms=None):
|
|
+ """
|
|
+ Builds a dictionary of a test case path -> test case value mapping.
|
|
+
|
|
+ Here, we want to know what the relative path on disk (under the tests/
|
|
+ subdirectory) is (such as "installed.pass.sh"), along with the actual
|
|
+ contents of the test case.
|
|
+
|
|
+ Presumably, we'll find the test case we want (all of them when
|
|
+ building a test case tarball) and write them to disk in the
|
|
+ appropriate location.
|
|
+ """
|
|
+ template_name = rule_template['name']
|
|
+ template_vars = rule_template['vars']
|
|
+
|
|
+ base_dir = os.path.abspath(os.path.join(self.templates_dir, template_name, "tests"))
|
|
+ results = dict()
|
|
+
|
|
+ # If no test cases exist, return an empty dictionary.
|
|
+ if not os.path.exists(base_dir):
|
|
+ return results
|
|
+
|
|
+ # Walk files; note that we don't need to do anything about directories
|
|
+ # as only files are recorded in the mapping; directories can be
|
|
+ # inferred from the path.
|
|
+ for dirpath, _, filenames in os.walk(base_dir):
|
|
+ if not filenames:
|
|
+ continue
|
|
+
|
|
+ for filename in filenames:
|
|
+ # Relative path to the file becomes our results key.
|
|
+ absolute_path = os.path.abspath(os.path.join(dirpath, filename))
|
|
+ relative_path = os.path.relpath(absolute_path, base_dir)
|
|
+
|
|
+ # Load template parameters and apply it to the test case.
|
|
+ template_parameters = templates[template_name].preprocess(template_vars, "tests")
|
|
+ jinja_dict = ssg.utils.merge_dicts(local_env_yaml, template_parameters)
|
|
+ filled_template = ssg.jinja.process_file_with_macros(
|
|
+ absolute_path, jinja_dict)
|
|
+
|
|
+ # Save the results under the relative path.
|
|
+ results[relative_path] = filled_template
|
|
+
|
|
+ return results
|
|
+
|
|
def build_lang(
|
|
self, rule_id, template_name, template_vars, lang, local_env_yaml, platforms=None):
|
|
"""
|
|
|
|
From 4bd5061ffbb524b09975982fbd6dae26f35770c3 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Tue, 6 Jul 2021 16:53:44 -0400
|
|
Subject: [PATCH 05/19] Add templated tests into templated directory
|
|
|
|
This extends the existing templated directory generation to also include
|
|
tests under shared/templates/.../tests. The complicated portion is in
|
|
subdirectory handling: we need to ensure we create all necessary nested
|
|
subdirectories that are implicitly implied by the relative paths. This
|
|
is a shortcoming in the get_all_tests's return format.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 43 ++++++++++++++++++++++++++++++++++
|
|
1 file changed, 43 insertions(+)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 3dbeaf304a4..2bd4b2bd1c7 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -21,6 +21,9 @@
|
|
from ssg.rule_yaml import parse_prodtype
|
|
from ssg_test_suite.log import LogHelper
|
|
|
|
+import ssg.templates
|
|
+
|
|
+
|
|
Scenario_run = namedtuple(
|
|
"Scenario_run",
|
|
("rule_id", "script"))
|
|
@@ -39,6 +42,8 @@
|
|
|
|
_SHARED_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../shared'))
|
|
|
|
+_SHARED_TEMPLATES = os.path.abspath(os.path.join(SSG_ROOT, 'shared/templates'))
|
|
+
|
|
REMOTE_USER = "root"
|
|
REMOTE_USER_HOME_DIRECTORY = "/root"
|
|
REMOTE_TEST_SCENARIOS_DIRECTORY = os.path.join(REMOTE_USER_HOME_DIRECTORY, "ssgts")
|
|
@@ -278,6 +283,11 @@ def template_tests(product=None):
|
|
yaml_path = product_yaml_path(SSG_ROOT, product)
|
|
product_yaml = load_product_yaml(yaml_path)
|
|
|
|
+ # Initialize a mock template_builder.
|
|
+ empty = "/ssgts/empty/placeholder"
|
|
+ template_builder = ssg.templates.Builder(product_yaml, empty,
|
|
+ _SHARED_TEMPLATES, empty, empty)
|
|
+
|
|
# Below we could run into a DocumentationNotComplete error. However,
|
|
# because the test suite isn't executed in the context of a particular
|
|
# build (though, ideally it would be linked), we may not know exactly
|
|
@@ -299,8 +309,11 @@ def template_tests(product=None):
|
|
|
|
# Load rule content in our environment. We use this to satisfy
|
|
# some implied properties that might be used in the test suite.
|
|
+ # Make sure we normalize to a specific product as well so that
|
|
+ # when we load templated content it is correct.
|
|
rule_path = get_rule_dir_yaml(dirpath)
|
|
rule = RuleYAML.from_yaml(rule_path, product_yaml)
|
|
+ rule.normalize(product)
|
|
|
|
# Note that most places would check prodtype, but we don't care
|
|
# about that here: if the rule is available to the product, we
|
|
@@ -320,6 +333,36 @@ def template_tests(product=None):
|
|
dest_path = os.path.join(tmpdir, rule.id_)
|
|
os.mkdir(dest_path)
|
|
|
|
+ # The priority order is rule-specific tests over templated tests.
|
|
+ # That is, for any test under rule_id/tests with a name matching a
|
|
+ # test under shared/templates/<template_name>/tests/, the former
|
|
+ # will preferred. This means we need to process templates first,
|
|
+ # so they'll be overwritten later if necessary.
|
|
+ if rule.template:
|
|
+ templated_tests = template_builder.get_all_tests(
|
|
+ rule.id_, rule.template, local_env_yaml)
|
|
+
|
|
+ for relative_path in templated_tests:
|
|
+ output_path = os.path.join(dest_path, relative_path)
|
|
+
|
|
+ # If there's a separator in the file name, it means we
|
|
+ # have nested directories to deal with.
|
|
+ if os.path.sep in relative_path:
|
|
+ parts = os.path.split(relative_path)[:-1]
|
|
+ for subdir_index in range(len(parts)):
|
|
+ # We need to expand all directories in correct
|
|
+ # order, preserving any previous directories (as
|
|
+ # they're nested). Use the star operator to splat
|
|
+ # array parts into arguments to os.path.join(...).
|
|
+ new_directory = os.path.join(dest_path, *parts[:subdir_index])
|
|
+ os.mkdir(new_directory)
|
|
+
|
|
+ # Write out the test content to the desired location on
|
|
+ # disk.
|
|
+ with open(output_path, 'w') as output_fp:
|
|
+ test_content = templated_tests[relative_path]
|
|
+ print(test_content, file=output_fp)
|
|
+
|
|
# Walk the test directory, writing all tests into the output
|
|
# directory, recursively.
|
|
tests_dir_path = os.path.join(dirpath, "tests")
|
|
|
|
From 013b247c1be897bd12ae4cf2bc2443279b1a52c9 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Wed, 7 Jul 2021 07:00:37 -0400
|
|
Subject: [PATCH 06/19] Teach iterate_over_rules about templated tests
|
|
|
|
There are two parts to rule-based test execution:
|
|
|
|
1. Building the tarball of tests and shipping it over to the remote
|
|
machine.
|
|
2. Iterating over these tests to see which scenarios are actually
|
|
applicable and any other metadata actions we need to take.
|
|
|
|
In particular, item two is controlled by iterate_over_rules. Here we
|
|
need to:
|
|
|
|
1. Teach it about the templating system.
|
|
2. Switch it from returning a list of test cases to a dictionary
|
|
mapping test case -> contents. This allows us to contain the
|
|
templating system to common.py only and not leak it into other parts
|
|
of the test suite.
|
|
3. Do a little bit of refactoring to contain shared code between
|
|
iterate_over_rules and template_tests.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 146 ++++++++++++++++++++++++---------
|
|
1 file changed, 108 insertions(+), 38 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 2bd4b2bd1c7..b357f816027 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -264,6 +264,62 @@ def _rel_abs_path(current_path, base_path):
|
|
return os.path.relpath(current_path, base_path)
|
|
|
|
|
|
+def get_product_context(product=None):
|
|
+ """
|
|
+ Returns a product YAML context if any product is specified. Hard-coded to
|
|
+ assume a debug build.
|
|
+ """
|
|
+ # Load product's YAML file if present. This will allow us to parse
|
|
+ # tests in the context of the product we're executing under.
|
|
+ product_yaml = dict()
|
|
+ if product:
|
|
+ yaml_path = product_yaml_path(SSG_ROOT, product)
|
|
+ product_yaml = load_product_yaml(yaml_path)
|
|
+
|
|
+ # We could run into a DocumentationNotComplete error when loading a
|
|
+ # rule's YAML contents. However, because the test suite isn't executed
|
|
+ # in the context of a particular build (though, ideally it would be
|
|
+ # linked), we may not know exactly whether the top-level rule/profile
|
|
+ # we're testing is actually completed. Thus, forcibly set the required
|
|
+ # property to bypass this error.
|
|
+ product_yaml['cmake_build_type'] = 'Debug'
|
|
+
|
|
+ return product_yaml
|
|
+
|
|
+
|
|
+def load_rule_and_env(rule_dir_path, env_yaml, product=None):
|
|
+ """
|
|
+ Loads a rule and returns the combination of the RuleYAML class and
|
|
+ the corresponding local environment for that rule.
|
|
+ """
|
|
+
|
|
+ # First build the path to the rule.yml file
|
|
+ rule_path = get_rule_dir_yaml(rule_dir_path)
|
|
+
|
|
+ # Load rule content in our environment. We use this to satisfy
|
|
+ # some implied properties that might be used in the test suite.
|
|
+ # Make sure we normalize to a specific product as well so that
|
|
+ # when we load templated content it is correct.
|
|
+ rule = RuleYAML.from_yaml(rule_path, env_yaml)
|
|
+ rule.normalize(product)
|
|
+
|
|
+ # Note that most places would check prodtype, but we don't care
|
|
+ # about that here: if the rule is available to the product, we
|
|
+ # load and parse it anyways as we have no knowledge of the
|
|
+ # top-level profile or rule passed into the test suite.
|
|
+ prodtypes = parse_prodtype(rule.prodtype)
|
|
+
|
|
+ # Our local copy of env_yaml needs some properties from rule.yml
|
|
+ # for completeness.
|
|
+ local_env_yaml = dict()
|
|
+ local_env_yaml.update(env_yaml)
|
|
+ local_env_yaml['rule_id'] = rule.id_
|
|
+ local_env_yaml['rule_title'] = rule.title
|
|
+ local_env_yaml['products'] = prodtypes
|
|
+
|
|
+ return rule, local_env_yaml
|
|
+
|
|
+
|
|
def template_tests(product=None):
|
|
"""
|
|
Create a temporary directory with test cases parsed via jinja using
|
|
@@ -276,26 +332,14 @@ def template_tests(product=None):
|
|
# it on success. Wrap in a try/except block and reraise the original
|
|
# exception after removing the temporary directory.
|
|
try:
|
|
- # Load product's YAML file if present. This will allow us to parse
|
|
- # tests in the context of the product we're executing under.
|
|
- product_yaml = dict()
|
|
- if product:
|
|
- yaml_path = product_yaml_path(SSG_ROOT, product)
|
|
- product_yaml = load_product_yaml(yaml_path)
|
|
+ # Load the product context we're executing under, if any.
|
|
+ product_yaml = get_product_context(product)
|
|
|
|
# Initialize a mock template_builder.
|
|
empty = "/ssgts/empty/placeholder"
|
|
template_builder = ssg.templates.Builder(product_yaml, empty,
|
|
_SHARED_TEMPLATES, empty, empty)
|
|
|
|
- # Below we could run into a DocumentationNotComplete error. However,
|
|
- # because the test suite isn't executed in the context of a particular
|
|
- # build (though, ideally it would be linked), we may not know exactly
|
|
- # whether the top-level rule/profile we're testing is actually
|
|
- # completed. Thus, forcibly set the required property to bypass this
|
|
- # error.
|
|
- product_yaml['cmake_build_type'] = 'Debug'
|
|
-
|
|
# Note that we're not exactly copying 1-for-1 the contents of the
|
|
# directory structure into the temporary one. Instead we want a
|
|
# flattened mapping with all rules in a single top-level directory
|
|
@@ -307,27 +351,8 @@ def template_tests(product=None):
|
|
if "tests" not in dirnames or not is_rule_dir(dirpath):
|
|
continue
|
|
|
|
- # Load rule content in our environment. We use this to satisfy
|
|
- # some implied properties that might be used in the test suite.
|
|
- # Make sure we normalize to a specific product as well so that
|
|
- # when we load templated content it is correct.
|
|
- rule_path = get_rule_dir_yaml(dirpath)
|
|
- rule = RuleYAML.from_yaml(rule_path, product_yaml)
|
|
- rule.normalize(product)
|
|
-
|
|
- # Note that most places would check prodtype, but we don't care
|
|
- # about that here: if the rule is available to the product, we
|
|
- # load and parse it anyways as we have no knowledge of the
|
|
- # top-level profile or rule passed into the test suite.
|
|
- prodtypes = parse_prodtype(rule.prodtype)
|
|
-
|
|
- # Our local copy of env_yaml needs some properties from rule.yml
|
|
- # for completeness.
|
|
- local_env_yaml = dict()
|
|
- local_env_yaml.update(product_yaml)
|
|
- local_env_yaml['rule_id'] = rule.id_
|
|
- local_env_yaml['rule_title'] = rule.title
|
|
- local_env_yaml['products'] = prodtypes
|
|
+ # Load the rule and its environment
|
|
+ rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
|
|
# Create the destination directory.
|
|
dest_path = os.path.join(tmpdir, rule.id_)
|
|
@@ -473,21 +498,66 @@ def iterate_over_rules(product=None):
|
|
id -- full rule id as it is present in datastream
|
|
short_id -- short rule ID, the same as basename of the directory
|
|
containing the test scenarios in Bash
|
|
- files -- list of executable .sh files in the "tests" directory
|
|
+ files -- list of executable .sh files in the uploaded tarball
|
|
"""
|
|
+
|
|
+ # Here we need to perform some magic to handle parsing the rule (from a
|
|
+ # product perspective) and loading any templated tests. In particular,
|
|
+ # identifying which tests to potentially run involves invoking the
|
|
+ # templating engine.
|
|
+ #
|
|
+ # Begin by loading context about our execution environment, if any.
|
|
+ product_yaml = get_product_context(product)
|
|
+
|
|
+ # Initialize a mock template_builder.
|
|
+ empty = "/ssgts/empty/placeholder"
|
|
+ template_builder = ssg.templates.Builder(product_yaml, empty,
|
|
+ _SHARED_TEMPLATES, empty, empty)
|
|
+
|
|
for dirpath, dirnames, filenames in walk_through_benchmark_dirs(product):
|
|
if "rule.yml" in filenames and "tests" in dirnames:
|
|
short_rule_id = os.path.basename(dirpath)
|
|
+
|
|
+ # Load the rule itself to check for a template.
|
|
+ rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
+
|
|
+ # All tests is a mapping from path (in the tarball) to contents
|
|
+ # of the test case. This is necessary because later code (which
|
|
+ # attempts to parse headers from the test case) don't have easy
|
|
+ # access to templated content. By reading it and returning it
|
|
+ # here, we can save later code from having to understand the
|
|
+ # templating system.
|
|
+ all_tests = dict()
|
|
+
|
|
+ # Start
|
|
+ if rule.template:
|
|
+ templated_tests = template_builder.get_all_tests(
|
|
+ rule.id_, rule.template, local_env_yaml)
|
|
+ all_tests.update(templated_tests)
|
|
+
|
|
+ # Add additional tests from the local rule directory. Note that,
|
|
+ # like the behavior in template_tests, this will overwrite any
|
|
+ # templated tests with the same file name.
|
|
tests_dir = os.path.join(dirpath, "tests")
|
|
tests_dir_files = os.listdir(tests_dir)
|
|
+ for test_case in tests_dir_files:
|
|
+ test_path = os.path.join(tests_dir, test_case)
|
|
+ if os.path.isdir(test_path):
|
|
+ continue
|
|
+
|
|
+ with open(test_path) as fp:
|
|
+ all_tests[test_case] = fp.read()
|
|
+
|
|
# Filter out everything except the shell test scenarios.
|
|
# Other files in rule directories are editor swap files
|
|
# or other content than a test case.
|
|
- scripts = filter(lambda x: x.endswith(".sh"), tests_dir_files)
|
|
+ allowed_scripts = filter(lambda x: x.endswith(".sh"), all_tests)
|
|
+ content_mapping = {x: all_tests[x] for x in allowed_scripts}
|
|
+
|
|
full_rule_id = OSCAP_RULE + short_rule_id
|
|
result = Rule(
|
|
directory=tests_dir, id=full_rule_id, short_id=short_rule_id,
|
|
- files=scripts)
|
|
+ files=content_mapping)
|
|
yield result
|
|
|
|
|
|
|
|
From 84014297a6881610e682ec7d54eca658b6ff127a Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Wed, 7 Jul 2021 07:05:13 -0400
|
|
Subject: [PATCH 07/19] Update rule execution to support templated tests
|
|
|
|
We needed to make a few small changes to rule-based test execution in
|
|
order for it to correctly understand templated tests:
|
|
|
|
1. Product selection (from the CLI) needs to be passed down into the
|
|
iterate_over_rules helper function.
|
|
2. Scenario loading needs to have knowledge of the returned test data.
|
|
3. Parsing of parameters needs to occur from the returned test data
|
|
rather than attempting to re-read it ourselves.
|
|
|
|
My assumption is that this will suffice for combined, since that method
|
|
of operation uses this method internally.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/rule.py | 30 +++++++++++++++---------------
|
|
1 file changed, 15 insertions(+), 15 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/rule.py b/tests/ssg_test_suite/rule.py
|
|
index 9f390749077..5ad7eb8ed27 100644
|
|
--- a/tests/ssg_test_suite/rule.py
|
|
+++ b/tests/ssg_test_suite/rule.py
|
|
@@ -24,7 +24,7 @@
|
|
|
|
|
|
Scenario = collections.namedtuple(
|
|
- "Scenario", ["script", "context", "script_params"])
|
|
+ "Scenario", ["script", "context", "script_params", "contents"])
|
|
|
|
|
|
def get_viable_profiles(selected_profiles, datastream, benchmark, script=None):
|
|
@@ -250,7 +250,7 @@ def _prepare_environment(self, scenarios_by_rule):
|
|
|
|
def _get_rules_to_test(self, target):
|
|
rules_to_test = []
|
|
- for rule in common.iterate_over_rules():
|
|
+ for rule in common.iterate_over_rules(self.test_env.product):
|
|
if not self._rule_should_be_tested(rule, target):
|
|
continue
|
|
if not xml_operations.find_rule_in_benchmark(
|
|
@@ -300,7 +300,7 @@ def _modify_parameters(self, script, params):
|
|
.format(OSCAP_PROFILE_ALL_ID, script))
|
|
return params
|
|
|
|
- def _parse_parameters(self, script):
|
|
+ def _parse_parameters(self, script_content):
|
|
"""Parse parameters from script header"""
|
|
params = {'profiles': [],
|
|
'templates': [],
|
|
@@ -309,16 +309,15 @@ def _parse_parameters(self, script):
|
|
'remediation': ['all'],
|
|
'variables': [],
|
|
}
|
|
- with open(script, 'r') as script_file:
|
|
- script_content = script_file.read()
|
|
- for parameter in params:
|
|
- found = re.search(r'^# {0} = (.*)$'.format(parameter),
|
|
- script_content,
|
|
- re.MULTILINE)
|
|
- if found is None:
|
|
- continue
|
|
- splitted = found.group(1).split(',')
|
|
- params[parameter] = [value.strip() for value in splitted]
|
|
+
|
|
+ for parameter in params:
|
|
+ found = re.search(r'^# {0} = (.*)$'.format(parameter),
|
|
+ script_content, re.MULTILINE)
|
|
+ if found is None:
|
|
+ continue
|
|
+ splitted = found.group(1).split(',')
|
|
+ params[parameter] = [value.strip() for value in splitted]
|
|
+
|
|
return params
|
|
|
|
def _get_scenarios(self, rule_dir, scripts, scenarios_regex, benchmark_cpes):
|
|
@@ -331,6 +330,7 @@ def _get_scenarios(self, rule_dir, scripts, scenarios_regex, benchmark_cpes):
|
|
|
|
scenarios = []
|
|
for script in scripts:
|
|
+ script_contents = scripts[script]
|
|
if scenarios_regex is not None:
|
|
if scenarios_pattern.match(script) is None:
|
|
logging.debug("Skipping script %s - it did not match "
|
|
@@ -338,10 +338,10 @@ def _get_scenarios(self, rule_dir, scripts, scenarios_regex, benchmark_cpes):
|
|
continue
|
|
script_context = _get_script_context(script)
|
|
if script_context is not None:
|
|
- script_params = self._parse_parameters(os.path.join(rule_dir, script))
|
|
+ script_params = self._parse_parameters(script_contents)
|
|
script_params = self._modify_parameters(script, script_params)
|
|
if common.matches_platform(script_params["platform"], benchmark_cpes):
|
|
- scenarios += [Scenario(script, script_context, script_params)]
|
|
+ scenarios += [Scenario(script, script_context, script_params, script_contents)]
|
|
else:
|
|
logging.warning("Script %s is not applicable on given platform" % script)
|
|
|
|
|
|
From c763af671037cb9bfd5bfd5bd24f5efe9e4fc4c1 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Wed, 7 Jul 2021 07:22:47 -0400
|
|
Subject: [PATCH 08/19] Make sure we read all test contents with Jinja
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 3 +--
|
|
1 file changed, 1 insertion(+), 2 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index b357f816027..865c8a8b988 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -545,8 +545,7 @@ def iterate_over_rules(product=None):
|
|
if os.path.isdir(test_path):
|
|
continue
|
|
|
|
- with open(test_path) as fp:
|
|
- all_tests[test_case] = fp.read()
|
|
+ all_tests[test_case] = process_file(test_path, local_env_yaml)
|
|
|
|
# Filter out everything except the shell test scenarios.
|
|
# Other files in rule directories are editor swap files
|
|
|
|
From 1234addaa4f776e3c814192c0aaf8f2137a74481 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Thu, 8 Jul 2021 07:44:16 -0400
|
|
Subject: [PATCH 09/19] Fix pep8 issues
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
ssg/templates.py | 5 +++--
|
|
tests/ssg_test_suite/common.py | 5 +++--
|
|
2 files changed, 6 insertions(+), 4 deletions(-)
|
|
|
|
diff --git a/ssg/templates.py b/ssg/templates.py
|
|
index 0a39c3ba094..818b1f79f68 100644
|
|
--- a/ssg/templates.py
|
|
+++ b/ssg/templates.py
|
|
@@ -201,7 +201,8 @@ def build_lang(
|
|
return
|
|
|
|
filled_template = self.build_lang_file(rule_id, template_name,
|
|
- template_vars, lang, local_env_yaml)
|
|
+ template_vars, lang,
|
|
+ local_env_yaml)
|
|
|
|
ext = lang_to_ext_map[lang]
|
|
output_file_name = rule_id + ext
|
|
@@ -331,7 +332,7 @@ def get_lang_for_rule(self, rule_id, rule_title, template, language):
|
|
local_env_yaml["products"] = self.env_yaml["product"]
|
|
|
|
return self.build_lang_file(rule_id, template_name, template_vars,
|
|
- language, local_env_yaml)
|
|
+ language, local_env_yaml)
|
|
|
|
def build_extra_ovals(self):
|
|
declaration_path = os.path.join(self.templates_dir, "extra_ovals.yml")
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 865c8a8b988..977e9f52c24 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -338,7 +338,8 @@ def template_tests(product=None):
|
|
# Initialize a mock template_builder.
|
|
empty = "/ssgts/empty/placeholder"
|
|
template_builder = ssg.templates.Builder(product_yaml, empty,
|
|
- _SHARED_TEMPLATES, empty, empty)
|
|
+ _SHARED_TEMPLATES, empty,
|
|
+ empty)
|
|
|
|
# Note that we're not exactly copying 1-for-1 the contents of the
|
|
# directory structure into the temporary one. Instead we want a
|
|
@@ -512,7 +513,7 @@ def iterate_over_rules(product=None):
|
|
# Initialize a mock template_builder.
|
|
empty = "/ssgts/empty/placeholder"
|
|
template_builder = ssg.templates.Builder(product_yaml, empty,
|
|
- _SHARED_TEMPLATES, empty, empty)
|
|
+ _SHARED_TEMPLATES, empty, empty)
|
|
|
|
for dirpath, dirnames, filenames in walk_through_benchmark_dirs(product):
|
|
if "rule.yml" in filenames and "tests" in dirnames:
|
|
|
|
From 81fcdf3e18f83da44b697ef7f47759a8eadd9854 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Wed, 14 Jul 2021 08:06:57 -0400
|
|
Subject: [PATCH 10/19] Split template_tests into template_rule_tests
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 146 +++++++++++++++++----------------
|
|
1 file changed, 77 insertions(+), 69 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 977e9f52c24..4e77bd888fe 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -320,6 +320,82 @@ def load_rule_and_env(rule_dir_path, env_yaml, product=None):
|
|
return rule, local_env_yaml
|
|
|
|
|
|
+def template_rule_tests(product, product_yaml, template_builder, tmpdir, dirpath):
|
|
+ """
|
|
+ For a given rule directory, templates all contained tests into the output
|
|
+ (tmpdir) directory.
|
|
+ """
|
|
+
|
|
+ # Load the rule and its environment
|
|
+ rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
+
|
|
+ # Create the destination directory.
|
|
+ dest_path = os.path.join(tmpdir, rule.id_)
|
|
+ os.mkdir(dest_path)
|
|
+
|
|
+ # The priority order is rule-specific tests over templated tests.
|
|
+ # That is, for any test under rule_id/tests with a name matching a
|
|
+ # test under shared/templates/<template_name>/tests/, the former
|
|
+ # will preferred. This means we need to process templates first,
|
|
+ # so they'll be overwritten later if necessary.
|
|
+ if rule.template:
|
|
+ templated_tests = template_builder.get_all_tests(rule.id_, rule.template,
|
|
+ local_env_yaml)
|
|
+
|
|
+ for relative_path in templated_tests:
|
|
+ output_path = os.path.join(dest_path, relative_path)
|
|
+
|
|
+ # If there's a separator in the file name, it means we
|
|
+ # have nested directories to deal with.
|
|
+ if os.path.sep in relative_path:
|
|
+ parts = os.path.split(relative_path)[:-1]
|
|
+ for subdir_index in range(len(parts)):
|
|
+ # We need to expand all directories in correct
|
|
+ # order, preserving any previous directories (as
|
|
+ # they're nested). Use the star operator to splat
|
|
+ # array parts into arguments to os.path.join(...).
|
|
+ new_directory = os.path.join(dest_path, *parts[:subdir_index])
|
|
+ os.mkdir(new_directory)
|
|
+
|
|
+ # Write out the test content to the desired location on
|
|
+ # disk.
|
|
+ with open(output_path, 'w') as output_fp:
|
|
+ test_content = templated_tests[relative_path]
|
|
+ print(test_content, file=output_fp)
|
|
+
|
|
+ # Walk the test directory, writing all tests into the output
|
|
+ # directory, recursively.
|
|
+ tests_dir_path = os.path.join(dirpath, "tests")
|
|
+ tests_dir_path = os.path.abspath(tests_dir_path)
|
|
+ for dirpath, dirnames, filenames in os.walk(tests_dir_path):
|
|
+ for dirname in dirnames:
|
|
+ # We want to recreate the correct path under the temporary
|
|
+ # directory. Resolve it to a relative path from the tests/
|
|
+ # directory.
|
|
+ dir_path = _rel_abs_path(os.path.join(dirpath, dirname), tests_dir_path)
|
|
+ assert '../' not in dir_path
|
|
+ tmp_dir_path = os.path.join(dest_path, dir_path)
|
|
+ os.mkdir(tmp_dir_path)
|
|
+
|
|
+ for filename in filenames:
|
|
+ # We want to recreate the correct path under the temporary
|
|
+ # directory. Resolve it to a relative path from the tests/
|
|
+ # directory. Assumption: directories should be created
|
|
+ # prior to recursing into them, so we don't need to handle
|
|
+ # if a file's parent directory doesn't yet exist under the
|
|
+ # destination.
|
|
+ src_test_path = os.path.join(dirpath, filename)
|
|
+ rel_test_path = _rel_abs_path(src_test_path, tests_dir_path)
|
|
+ dest_test_path = os.path.join(dest_path, rel_test_path)
|
|
+
|
|
+ # Rather than performing an OS-level copy, we need to
|
|
+ # first parse the test with jinja and then write it back
|
|
+ # out to the destination.
|
|
+ parsed_test = process_file(src_test_path, local_env_yaml)
|
|
+ with open(dest_test_path, 'w') as output_fp:
|
|
+ print(parsed_test, file=output_fp)
|
|
+
|
|
+
|
|
def template_tests(product=None):
|
|
"""
|
|
Create a temporary directory with test cases parsed via jinja using
|
|
@@ -352,75 +428,7 @@ def template_tests(product=None):
|
|
if "tests" not in dirnames or not is_rule_dir(dirpath):
|
|
continue
|
|
|
|
- # Load the rule and its environment
|
|
- rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
-
|
|
- # Create the destination directory.
|
|
- dest_path = os.path.join(tmpdir, rule.id_)
|
|
- os.mkdir(dest_path)
|
|
-
|
|
- # The priority order is rule-specific tests over templated tests.
|
|
- # That is, for any test under rule_id/tests with a name matching a
|
|
- # test under shared/templates/<template_name>/tests/, the former
|
|
- # will preferred. This means we need to process templates first,
|
|
- # so they'll be overwritten later if necessary.
|
|
- if rule.template:
|
|
- templated_tests = template_builder.get_all_tests(
|
|
- rule.id_, rule.template, local_env_yaml)
|
|
-
|
|
- for relative_path in templated_tests:
|
|
- output_path = os.path.join(dest_path, relative_path)
|
|
-
|
|
- # If there's a separator in the file name, it means we
|
|
- # have nested directories to deal with.
|
|
- if os.path.sep in relative_path:
|
|
- parts = os.path.split(relative_path)[:-1]
|
|
- for subdir_index in range(len(parts)):
|
|
- # We need to expand all directories in correct
|
|
- # order, preserving any previous directories (as
|
|
- # they're nested). Use the star operator to splat
|
|
- # array parts into arguments to os.path.join(...).
|
|
- new_directory = os.path.join(dest_path, *parts[:subdir_index])
|
|
- os.mkdir(new_directory)
|
|
-
|
|
- # Write out the test content to the desired location on
|
|
- # disk.
|
|
- with open(output_path, 'w') as output_fp:
|
|
- test_content = templated_tests[relative_path]
|
|
- print(test_content, file=output_fp)
|
|
-
|
|
- # Walk the test directory, writing all tests into the output
|
|
- # directory, recursively.
|
|
- tests_dir_path = os.path.join(dirpath, "tests")
|
|
- tests_dir_path = os.path.abspath(tests_dir_path)
|
|
- for dirpath, dirnames, filenames in os.walk(tests_dir_path):
|
|
- for dirname in dirnames:
|
|
- # We want to recreate the correct path under the temporary
|
|
- # directory. Resolve it to a relative path from the tests/
|
|
- # directory.
|
|
- dir_path = _rel_abs_path(os.path.join(dirpath, dirname), tests_dir_path)
|
|
- assert '../' not in dir_path
|
|
- tmp_dir_path = os.path.join(dest_path, dir_path)
|
|
- os.mkdir(tmp_dir_path)
|
|
-
|
|
- for filename in filenames:
|
|
- # We want to recreate the correct path under the temporary
|
|
- # directory. Resolve it to a relative path from the tests/
|
|
- # directory. Assumption: directories should be created
|
|
- # prior to recursing into them, so we don't need to handle
|
|
- # if a file's parent directory doesn't yet exist under the
|
|
- # destination.
|
|
- src_test_path = os.path.join(dirpath, filename)
|
|
- rel_test_path = _rel_abs_path(src_test_path, tests_dir_path)
|
|
- dest_test_path = os.path.join(dest_path, rel_test_path)
|
|
-
|
|
- # Rather than performing an OS-level copy, we need to
|
|
- # first parse the test with jinja and then write it back
|
|
- # out to the destination.
|
|
- parsed_test = process_file(src_test_path, local_env_yaml)
|
|
- with open(dest_test_path, 'w') as output_fp:
|
|
- print(parsed_test, file=output_fp)
|
|
-
|
|
+ template_rule_tests(product, product_yaml, template_builder, tmpdir, dirpath)
|
|
except Exception as exp:
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
raise exp
|
|
|
|
From 9bd6bf1bb5a51a69dc30d4d6eaa2e159c2ac0446 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Wed, 14 Jul 2021 08:23:27 -0400
|
|
Subject: [PATCH 11/19] Refactor template_rule_tests into write_* helpers
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 93 ++++++++++++++++++----------------
|
|
1 file changed, 50 insertions(+), 43 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 4e77bd888fe..1f3cad807e6 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -320,49 +320,27 @@ def load_rule_and_env(rule_dir_path, env_yaml, product=None):
|
|
return rule, local_env_yaml
|
|
|
|
|
|
-def template_rule_tests(product, product_yaml, template_builder, tmpdir, dirpath):
|
|
- """
|
|
- For a given rule directory, templates all contained tests into the output
|
|
- (tmpdir) directory.
|
|
- """
|
|
-
|
|
- # Load the rule and its environment
|
|
- rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
-
|
|
- # Create the destination directory.
|
|
- dest_path = os.path.join(tmpdir, rule.id_)
|
|
- os.mkdir(dest_path)
|
|
-
|
|
- # The priority order is rule-specific tests over templated tests.
|
|
- # That is, for any test under rule_id/tests with a name matching a
|
|
- # test under shared/templates/<template_name>/tests/, the former
|
|
- # will preferred. This means we need to process templates first,
|
|
- # so they'll be overwritten later if necessary.
|
|
- if rule.template:
|
|
- templated_tests = template_builder.get_all_tests(rule.id_, rule.template,
|
|
- local_env_yaml)
|
|
-
|
|
- for relative_path in templated_tests:
|
|
- output_path = os.path.join(dest_path, relative_path)
|
|
-
|
|
- # If there's a separator in the file name, it means we
|
|
- # have nested directories to deal with.
|
|
- if os.path.sep in relative_path:
|
|
- parts = os.path.split(relative_path)[:-1]
|
|
- for subdir_index in range(len(parts)):
|
|
- # We need to expand all directories in correct
|
|
- # order, preserving any previous directories (as
|
|
- # they're nested). Use the star operator to splat
|
|
- # array parts into arguments to os.path.join(...).
|
|
- new_directory = os.path.join(dest_path, *parts[:subdir_index])
|
|
- os.mkdir(new_directory)
|
|
-
|
|
- # Write out the test content to the desired location on
|
|
- # disk.
|
|
- with open(output_path, 'w') as output_fp:
|
|
- test_content = templated_tests[relative_path]
|
|
- print(test_content, file=output_fp)
|
|
-
|
|
+def write_rule_templated_tests(dest_path, relative_path, test_content):
|
|
+ output_path = os.path.join(dest_path, relative_path)
|
|
+
|
|
+ # If there's a separator in the file name, it means we have nested
|
|
+ # directories to deal with.
|
|
+ if os.path.sep in relative_path:
|
|
+ parts = os.path.split(relative_path)[:-1]
|
|
+ for subdir_index in range(len(parts)):
|
|
+ # We need to expand all directories in the correct order,
|
|
+ # preserving any previous directories (as they're nested).
|
|
+ # Use the star operator to splat array parts into arguments
|
|
+ # to os.path.join(...).
|
|
+ new_directory = os.path.join(dest_path, *parts[:subdir_index])
|
|
+ os.mkdir(new_directory)
|
|
+
|
|
+ # Write out the test content to the desired location on disk.
|
|
+ with open(output_path, 'w') as output_fp:
|
|
+ print(test_content, file=output_fp)
|
|
+
|
|
+
|
|
+def write_rule_dir_tests(local_env_yaml, dest_path, dirpath):
|
|
# Walk the test directory, writing all tests into the output
|
|
# directory, recursively.
|
|
tests_dir_path = os.path.join(dirpath, "tests")
|
|
@@ -396,6 +374,35 @@ def template_rule_tests(product, product_yaml, template_builder, tmpdir, dirpath
|
|
print(parsed_test, file=output_fp)
|
|
|
|
|
|
+def template_rule_tests(product, product_yaml, template_builder, tmpdir, dirpath):
|
|
+ """
|
|
+ For a given rule directory, templates all contained tests into the output
|
|
+ (tmpdir) directory.
|
|
+ """
|
|
+
|
|
+ # Load the rule and its environment
|
|
+ rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
+
|
|
+ # Create the destination directory.
|
|
+ dest_path = os.path.join(tmpdir, rule.id_)
|
|
+ os.mkdir(dest_path)
|
|
+
|
|
+ # The priority order is rule-specific tests over templated tests.
|
|
+ # That is, for any test under rule_id/tests with a name matching a
|
|
+ # test under shared/templates/<template_name>/tests/, the former
|
|
+ # will preferred. This means we need to process templates first,
|
|
+ # so they'll be overwritten later if necessary.
|
|
+ if rule.template:
|
|
+ templated_tests = template_builder.get_all_tests(rule.id_, rule.template,
|
|
+ local_env_yaml)
|
|
+
|
|
+ for relative_path in templated_tests:
|
|
+ test_content = templated_tests[relative_path]
|
|
+ write_rule_templated_tests(dest_path, relative_path, test_content)
|
|
+
|
|
+ write_rule_dir_tests(local_env_yaml, dest_path, dirpath)
|
|
+
|
|
+
|
|
def template_tests(product=None):
|
|
"""
|
|
Create a temporary directory with test cases parsed via jinja using
|
|
|
|
From 5b74ff0aa8ef1b085ef9d6c0148283835a2917f8 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Wed, 14 Jul 2021 08:24:10 -0400
|
|
Subject: [PATCH 12/19] Remove unnecessary assertion
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 1 -
|
|
1 file changed, 1 deletion(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 1f3cad807e6..9c232dcad02 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -351,7 +351,6 @@ def write_rule_dir_tests(local_env_yaml, dest_path, dirpath):
|
|
# directory. Resolve it to a relative path from the tests/
|
|
# directory.
|
|
dir_path = _rel_abs_path(os.path.join(dirpath, dirname), tests_dir_path)
|
|
- assert '../' not in dir_path
|
|
tmp_dir_path = os.path.join(dest_path, dir_path)
|
|
os.mkdir(tmp_dir_path)
|
|
|
|
|
|
From 0ab1b95de66d60cf135c985697d6269464777de9 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Wed, 14 Jul 2021 08:25:26 -0400
|
|
Subject: [PATCH 13/19] Remove unnecessary _rel_abs_path
|
|
|
|
As pointed out by Matej, the extra abspath is unnecessary prior to
|
|
calling relpath. Remove our helper and switch to calling relpath
|
|
directly.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 15 ++-------------
|
|
1 file changed, 2 insertions(+), 13 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 9c232dcad02..04117359203 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -253,17 +253,6 @@ def _make_file_root_owned(tarinfo):
|
|
return tarinfo
|
|
|
|
|
|
-def _rel_abs_path(current_path, base_path):
|
|
- """
|
|
- Return the value of the current path, relative to the base path, but
|
|
- resolving paths absolutely first. This helps when walking a nested
|
|
- directory structure and want to get the subtree relative to the original
|
|
- path
|
|
- """
|
|
- tmp_path = os.path.abspath(current_path)
|
|
- return os.path.relpath(current_path, base_path)
|
|
-
|
|
-
|
|
def get_product_context(product=None):
|
|
"""
|
|
Returns a product YAML context if any product is specified. Hard-coded to
|
|
@@ -350,7 +339,7 @@ def write_rule_dir_tests(local_env_yaml, dest_path, dirpath):
|
|
# We want to recreate the correct path under the temporary
|
|
# directory. Resolve it to a relative path from the tests/
|
|
# directory.
|
|
- dir_path = _rel_abs_path(os.path.join(dirpath, dirname), tests_dir_path)
|
|
+ dir_path = os.path.relpath(os.path.join(dirpath, dirname), tests_dir_path)
|
|
tmp_dir_path = os.path.join(dest_path, dir_path)
|
|
os.mkdir(tmp_dir_path)
|
|
|
|
@@ -362,7 +351,7 @@ def write_rule_dir_tests(local_env_yaml, dest_path, dirpath):
|
|
# if a file's parent directory doesn't yet exist under the
|
|
# destination.
|
|
src_test_path = os.path.join(dirpath, filename)
|
|
- rel_test_path = _rel_abs_path(src_test_path, tests_dir_path)
|
|
+ rel_test_path = os.path.relpath(src_test_path, tests_dir_path)
|
|
dest_test_path = os.path.join(dest_path, rel_test_path)
|
|
|
|
# Rather than performing an OS-level copy, we need to
|
|
|
|
From 0d2dadf00682efba465b5378803a68893dc62038 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Wed, 14 Jul 2021 14:23:59 -0400
|
|
Subject: [PATCH 14/19] Only template rules with variables
|
|
|
|
The audit_rules_privileged_commands_unix2_chkpwd rule lacks variables on
|
|
most products, in addition to its limited prodtype. Because the existing
|
|
test harness lacks understanding of prodtype (for including rules in the
|
|
tarball), we don't check it yet either. This causes issues when the
|
|
template is present but has empty variables on the particular product
|
|
(such as when this rule is executed under Ubuntu)
|
|
|
|
Ultimately we should probably check prodtype in the future, but it
|
|
outside the scope of this PR.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 4 ++--
|
|
1 file changed, 2 insertions(+), 2 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 04117359203..44105e3b7ae 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -380,7 +380,7 @@ def template_rule_tests(product, product_yaml, template_builder, tmpdir, dirpath
|
|
# test under shared/templates/<template_name>/tests/, the former
|
|
# will preferred. This means we need to process templates first,
|
|
# so they'll be overwritten later if necessary.
|
|
- if rule.template:
|
|
+ if rule.template and rule.template['vars']:
|
|
templated_tests = template_builder.get_all_tests(rule.id_, rule.template,
|
|
local_env_yaml)
|
|
|
|
@@ -534,7 +534,7 @@ def iterate_over_rules(product=None):
|
|
all_tests = dict()
|
|
|
|
# Start
|
|
- if rule.template:
|
|
+ if rule.template and rule.template['vars']:
|
|
templated_tests = template_builder.get_all_tests(
|
|
rule.id_, rule.template, local_env_yaml)
|
|
all_tests.update(templated_tests)
|
|
|
|
From e00262230d86b39d77dc1d75ffce5c048d5a4652 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Mon, 19 Jul 2021 09:06:02 -0400
|
|
Subject: [PATCH 15/19] Document new JINJA/Template testing scenarios
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/README.md | 32 ++++++++++++++++++++++++++++++++
|
|
1 file changed, 32 insertions(+)
|
|
|
|
diff --git a/tests/README.md b/tests/README.md
|
|
index 6b5b4497baf..0b3dfabc6f5 100644
|
|
--- a/tests/README.md
|
|
+++ b/tests/README.md
|
|
@@ -147,6 +147,7 @@ the rule that `oscap` should return when the rule is evaluated.
|
|
very important to keep this naming form.
|
|
|
|
For example:
|
|
+
|
|
* `something.pass.sh`: Success scenario - script is expected to prepare machine
|
|
in such way that the rule is expected to pass.
|
|
* `something.fail.sh`: Fail scenario - script is expected to break machine so
|
|
@@ -200,6 +201,37 @@ Using `platform` and `variables` metadata:
|
|
echo "KerberosAuthentication $auth_enabled" >> /etc/ssh/sshd_config
|
|
```
|
|
|
|
+### Augmenting using Jinja macros
|
|
+
|
|
+Each scenario script is processed under the same jinja context as the
|
|
+corresponding OVAL and remediation content. This means that product-specific
|
|
+information is known to the scenario scripts at upload time (for example,
|
|
+`{{{ grub2_boot_path }}}`), allowing them to work across products. This
|
|
+also means Jinja macros such as `{{{ bash_package_install(...) }}}` work to
|
|
+install/remove specific packages during the course of testing (such as, if
|
|
+it is desired to both install and remove a package in the same scenario for
|
|
+the `package_installed` rules).
|
|
+
|
|
+Note that this does have some limitations: knowledge of the profile (and the
|
|
+variables it has and the values they take) is still not provided to the test
|
|
+scenario. The above `# profiles` or `# variables` directives will still have
|
|
+to be used to add any profile-specific information.
|
|
+
|
|
+### Augmenting using `shared/templates`
|
|
+
|
|
+Additionally, we have enabled test scenarios located under the templated
|
|
+directory, `shared/templates/.../tests`. Unlike with build-time content,
|
|
+`tests` does not need to be located in the template's manifest (at
|
|
+`template.yml`). Instead, SSGTS will automatically parse each rule and
|
|
+prefer rule-directory-specific test scenarios over any templated scenarios
|
|
+that the rule uses. (E.g., if `installed.pass.sh` is present in the
|
|
+template `package_installed` and in the `tests/` subdirectory of the rule
|
|
+directory, the latter takes precedence over the former).
|
|
+
|
|
+In addition to the Jinja context described above, the contents of the template
|
|
+variables (after processing in `template.py`) are also available to the
|
|
+test scenario. This enables template-specific checking.
|
|
+
|
|
## Example of adding new test scenarios
|
|
|
|
Let's add test scenarios for rule `accounts_password_minlen_login_defs`.
|
|
|
|
From c696ebf0001bd6bcced40aafea58cfbc6b870cc1 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Tue, 27 Jul 2021 08:17:47 -0400
|
|
Subject: [PATCH 16/19] Correctly handle generation of templated tests
|
|
|
|
When rules lacked tests inside the rule directory but had them via a
|
|
template, SSGTS would ignore them and claim the rule wasn't found. Fix
|
|
this bug to allow template-only tests.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 30 ++++++++++++++++++++++--------
|
|
1 file changed, 22 insertions(+), 8 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 44105e3b7ae..0cb5451e31b 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -334,6 +334,13 @@ def write_rule_dir_tests(local_env_yaml, dest_path, dirpath):
|
|
# directory, recursively.
|
|
tests_dir_path = os.path.join(dirpath, "tests")
|
|
tests_dir_path = os.path.abspath(tests_dir_path)
|
|
+
|
|
+ # Note that the tests/ directory may not always exist any more. In
|
|
+ # particular, when a rule uses a template, tests may be present there
|
|
+ # but not present in the actual rule directory.
|
|
+ if not os.path.exists(tests_dir_path):
|
|
+ return
|
|
+
|
|
for dirpath, dirnames, filenames in os.walk(tests_dir_path):
|
|
for dirname in dirnames:
|
|
# We want to recreate the correct path under the temporary
|
|
@@ -420,7 +427,7 @@ def template_tests(product=None):
|
|
# /group_a/rule_a/tests/something.pass.sh -> /rule_a/something.pass.sh
|
|
for dirpath, dirnames, _ in walk_through_benchmark_dirs(product):
|
|
# Skip anything that isn't obviously a rule.
|
|
- if "tests" not in dirnames or not is_rule_dir(dirpath):
|
|
+ if not is_rule_dir(dirpath):
|
|
continue
|
|
|
|
template_rule_tests(product, product_yaml, template_builder, tmpdir, dirpath)
|
|
@@ -519,7 +526,7 @@ def iterate_over_rules(product=None):
|
|
_SHARED_TEMPLATES, empty, empty)
|
|
|
|
for dirpath, dirnames, filenames in walk_through_benchmark_dirs(product):
|
|
- if "rule.yml" in filenames and "tests" in dirnames:
|
|
+ if is_rule_dir(dirpath):
|
|
short_rule_id = os.path.basename(dirpath)
|
|
|
|
# Load the rule itself to check for a template.
|
|
@@ -543,13 +550,14 @@ def iterate_over_rules(product=None):
|
|
# like the behavior in template_tests, this will overwrite any
|
|
# templated tests with the same file name.
|
|
tests_dir = os.path.join(dirpath, "tests")
|
|
- tests_dir_files = os.listdir(tests_dir)
|
|
- for test_case in tests_dir_files:
|
|
- test_path = os.path.join(tests_dir, test_case)
|
|
- if os.path.isdir(test_path):
|
|
- continue
|
|
+ if os.path.exists(tests_dir):
|
|
+ tests_dir_files = os.listdir(tests_dir)
|
|
+ for test_case in tests_dir_files:
|
|
+ test_path = os.path.join(tests_dir, test_case)
|
|
+ if os.path.isdir(test_path):
|
|
+ continue
|
|
|
|
- all_tests[test_case] = process_file(test_path, local_env_yaml)
|
|
+ all_tests[test_case] = process_file(test_path, local_env_yaml)
|
|
|
|
# Filter out everything except the shell test scenarios.
|
|
# Other files in rule directories are editor swap files
|
|
@@ -557,6 +565,12 @@ def iterate_over_rules(product=None):
|
|
allowed_scripts = filter(lambda x: x.endswith(".sh"), all_tests)
|
|
content_mapping = {x: all_tests[x] for x in allowed_scripts}
|
|
|
|
+ # Skip any rules that lack any content. This ensures that if we
|
|
+ # end up with rules with a template lacking tests and without any
|
|
+ # rule directory tests, we don't include the empty rule here.
|
|
+ if not content_mapping:
|
|
+ continue
|
|
+
|
|
full_rule_id = OSCAP_RULE + short_rule_id
|
|
result = Rule(
|
|
directory=tests_dir, id=full_rule_id, short_id=short_rule_id,
|
|
|
|
From 0661e505c1294a7d5bf6a59813afabff48f31b4c Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Tue, 27 Jul 2021 12:23:25 -0400
|
|
Subject: [PATCH 17/19] Template tests with matching prodtype
|
|
|
|
When prodtype is known to the test system, we can skip templating any
|
|
rule that has a prodtype that doesn't match. This saves time during
|
|
building the bundle and restricts us to only writing tests we care
|
|
about and can potentially use.
|
|
|
|
Note that there might be a mismatch between (datastream, rule): if a
|
|
rule is updated on disk but the datastream isn't regenerated, you might
|
|
run into weird edge cases where this either over- or under-provisions,
|
|
but the same issue would likely occur previously.
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 22 ++++++++++++++++++++++
|
|
1 file changed, 22 insertions(+)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 0cb5451e31b..946d7152af1 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -378,6 +378,17 @@ def template_rule_tests(product, product_yaml, template_builder, tmpdir, dirpath
|
|
# Load the rule and its environment
|
|
rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
|
|
+ # Before we get too far, we wish to search the rule YAML to see if
|
|
+ # it is applicable to the current product. If we have a product
|
|
+ # and the rule isn't applicable for the product, there's no point
|
|
+ # in continuing with the rest of the loading. This should speed up
|
|
+ # the loading of the templated tests. Note that we've already
|
|
+ # parsed the prodtype into local_env_yaml
|
|
+ if product and local_env_yaml['products']:
|
|
+ prodtypes = local_env_yaml['products']
|
|
+ if "all" not in prodtypes and product not in prodtypes:
|
|
+ return
|
|
+
|
|
# Create the destination directory.
|
|
dest_path = os.path.join(tmpdir, rule.id_)
|
|
os.mkdir(dest_path)
|
|
@@ -532,6 +543,17 @@ def iterate_over_rules(product=None):
|
|
# Load the rule itself to check for a template.
|
|
rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
|
|
+ # Before we get too far, we wish to search the rule YAML to see if
|
|
+ # it is applicable to the current product. If we have a product
|
|
+ # and the rule isn't applicable for the product, there's no point
|
|
+ # in continuing with the rest of the loading. This should speed up
|
|
+ # the loading of the templated tests. Note that we've already
|
|
+ # parsed the prodtype into local_env_yaml
|
|
+ if product and local_env_yaml['products']:
|
|
+ prodtypes = local_env_yaml['products']
|
|
+ if "all" not in prodtypes and product not in prodtypes:
|
|
+ continue
|
|
+
|
|
# All tests is a mapping from path (in the tarball) to contents
|
|
# of the test case. This is necessary because later code (which
|
|
# attempts to parse headers from the test case) don't have easy
|
|
|
|
From f1c0bbd7fb8af5f0af96bf3d442166724fcc7f94 Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Tue, 27 Jul 2021 14:51:53 -0400
|
|
Subject: [PATCH 18/19] Use process_file_with_macros rather than process_file
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/common.py | 6 +++---
|
|
1 file changed, 3 insertions(+), 3 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 946d7152af1..291e0f5c9ad 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -15,7 +15,7 @@
|
|
from ssg.constants import MULTI_PLATFORM_MAPPING
|
|
from ssg.constants import FULL_NAME_TO_PRODUCT_MAPPING
|
|
from ssg.constants import OSCAP_RULE
|
|
-from ssg.jinja import process_file
|
|
+from ssg.jinja import process_file_with_macros
|
|
from ssg.products import product_yaml_path, load_product_yaml
|
|
from ssg.rules import get_rule_dir_yaml, is_rule_dir
|
|
from ssg.rule_yaml import parse_prodtype
|
|
@@ -364,7 +364,7 @@ def write_rule_dir_tests(local_env_yaml, dest_path, dirpath):
|
|
# Rather than performing an OS-level copy, we need to
|
|
# first parse the test with jinja and then write it back
|
|
# out to the destination.
|
|
- parsed_test = process_file(src_test_path, local_env_yaml)
|
|
+ parsed_test = process_file_with_macros(src_test_path, local_env_yaml)
|
|
with open(dest_test_path, 'w') as output_fp:
|
|
print(parsed_test, file=output_fp)
|
|
|
|
@@ -579,7 +579,7 @@ def iterate_over_rules(product=None):
|
|
if os.path.isdir(test_path):
|
|
continue
|
|
|
|
- all_tests[test_case] = process_file(test_path, local_env_yaml)
|
|
+ all_tests[test_case] = process_file_with_macros(test_path, local_env_yaml)
|
|
|
|
# Filter out everything except the shell test scenarios.
|
|
# Other files in rule directories are editor swap files
|
|
|
|
From 0f22db3a49bf8ccb14e0ea52e7884a525fa37f6f Mon Sep 17 00:00:00 2001
|
|
From: Alexander Scheel <alex.scheel@canonical.com>
|
|
Date: Thu, 29 Jul 2021 07:45:41 -0400
|
|
Subject: [PATCH 19/19] Add an option to not run duplicate templated tests
|
|
|
|
Signed-off-by: Alexander Scheel <alex.scheel@canonical.com>
|
|
---
|
|
tests/ssg_test_suite/combined.py | 4 ++--
|
|
tests/ssg_test_suite/common.py | 9 ++++++---
|
|
tests/ssg_test_suite/rule.py | 19 +++++++++++++++----
|
|
tests/test_suite.py | 10 ++++++++++
|
|
4 files changed, 33 insertions(+), 9 deletions(-)
|
|
|
|
diff --git a/tests/ssg_test_suite/combined.py b/tests/ssg_test_suite/combined.py
|
|
index 05270353235..4ef8898f602 100644
|
|
--- a/tests/ssg_test_suite/combined.py
|
|
+++ b/tests/ssg_test_suite/combined.py
|
|
@@ -39,10 +39,10 @@ def __init__(self, test_env):
|
|
self.results = list()
|
|
self._current_result = None
|
|
|
|
- def _rule_should_be_tested(self, rule, rules_to_be_tested):
|
|
+ def _rule_should_be_tested(self, rule, rules_to_be_tested, tested_templates):
|
|
if rule.short_id not in rules_to_be_tested:
|
|
return False
|
|
- return True
|
|
+ return not self._rule_template_been_tested(rule, tested_templates)
|
|
|
|
def _modify_parameters(self, script, params):
|
|
# If there is no profiles metadata in a script we will use
|
|
diff --git a/tests/ssg_test_suite/common.py b/tests/ssg_test_suite/common.py
|
|
index 291e0f5c9ad..132a004323f 100644
|
|
--- a/tests/ssg_test_suite/common.py
|
|
+++ b/tests/ssg_test_suite/common.py
|
|
@@ -31,7 +31,7 @@
|
|
"Scenario_conditions",
|
|
("backend", "scanning_mode", "remediated_by", "datastream"))
|
|
Rule = namedtuple(
|
|
- "Rule", ["directory", "id", "short_id", "files"])
|
|
+ "Rule", ["directory", "id", "short_id", "files", "template"])
|
|
|
|
SSG_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
|
|
@@ -542,6 +542,7 @@ def iterate_over_rules(product=None):
|
|
|
|
# Load the rule itself to check for a template.
|
|
rule, local_env_yaml = load_rule_and_env(dirpath, product_yaml, product)
|
|
+ template_name = None
|
|
|
|
# Before we get too far, we wish to search the rule YAML to see if
|
|
# it is applicable to the current product. If we have a product
|
|
@@ -562,11 +563,13 @@ def iterate_over_rules(product=None):
|
|
# templating system.
|
|
all_tests = dict()
|
|
|
|
- # Start
|
|
+ # Start by checking for templating tests and provision them if
|
|
+ # present.
|
|
if rule.template and rule.template['vars']:
|
|
templated_tests = template_builder.get_all_tests(
|
|
rule.id_, rule.template, local_env_yaml)
|
|
all_tests.update(templated_tests)
|
|
+ template_name = rule.template['name']
|
|
|
|
# Add additional tests from the local rule directory. Note that,
|
|
# like the behavior in template_tests, this will overwrite any
|
|
@@ -596,7 +599,7 @@ def iterate_over_rules(product=None):
|
|
full_rule_id = OSCAP_RULE + short_rule_id
|
|
result = Rule(
|
|
directory=tests_dir, id=full_rule_id, short_id=short_rule_id,
|
|
- files=content_mapping)
|
|
+ files=content_mapping, template=template_name)
|
|
yield result
|
|
|
|
|
|
diff --git a/tests/ssg_test_suite/rule.py b/tests/ssg_test_suite/rule.py
|
|
index 5ad7eb8ed27..b707326179f 100644
|
|
--- a/tests/ssg_test_suite/rule.py
|
|
+++ b/tests/ssg_test_suite/rule.py
|
|
@@ -211,13 +211,23 @@ def _final_scan_went_ok(self, runner, rule_id):
|
|
logging.error(msg)
|
|
return success
|
|
|
|
- def _rule_should_be_tested(self, rule, rules_to_be_tested):
|
|
+ def _rule_template_been_tested(self, rule, tested_templates):
|
|
+ if rule.template is None:
|
|
+ return False
|
|
+ if self.test_env.duplicate_templates:
|
|
+ return False
|
|
+ if rule.template in tested_templates:
|
|
+ return True
|
|
+ tested_templates.add(rule.template)
|
|
+ return False
|
|
+
|
|
+ def _rule_should_be_tested(self, rule, rules_to_be_tested, tested_templates):
|
|
if 'ALL' in rules_to_be_tested:
|
|
# don't select rules that are not present in benchmark
|
|
if not xml_operations.find_rule_in_benchmark(
|
|
self.datastream, self.benchmark_id, rule.id):
|
|
return False
|
|
- return True
|
|
+ return not self._rule_template_been_tested(rule, tested_templates)
|
|
else:
|
|
for rule_to_be_tested in rules_to_be_tested:
|
|
# we check for a substring
|
|
@@ -226,7 +236,7 @@ def _rule_should_be_tested(self, rule, rules_to_be_tested):
|
|
else:
|
|
pattern = OSCAP_RULE + rule_to_be_tested
|
|
if fnmatch.fnmatch(rule.id, pattern):
|
|
- return True
|
|
+ return not self._rule_template_been_tested(rule, tested_templates)
|
|
return False
|
|
|
|
def _ensure_package_present_for_all_scenarios(self, scenarios_by_rule):
|
|
@@ -250,8 +260,9 @@ def _prepare_environment(self, scenarios_by_rule):
|
|
|
|
def _get_rules_to_test(self, target):
|
|
rules_to_test = []
|
|
+ tested_templates = set()
|
|
for rule in common.iterate_over_rules(self.test_env.product):
|
|
- if not self._rule_should_be_tested(rule, target):
|
|
+ if not self._rule_should_be_tested(rule, target, tested_templates):
|
|
continue
|
|
if not xml_operations.find_rule_in_benchmark(
|
|
self.datastream, self.benchmark_id, rule.id):
|
|
diff --git a/tests/test_suite.py b/tests/test_suite.py
|
|
index 00da15329a5..445a53f41d8 100755
|
|
--- a/tests/test_suite.py
|
|
+++ b/tests/test_suite.py
|
|
@@ -116,6 +116,15 @@ def parse_args():
|
|
"or remediation done by using remediation roles "
|
|
"that are saved to disk beforehand.")
|
|
|
|
+ common_parser.add_argument(
|
|
+ "--duplicate-templates",
|
|
+ dest="duplicate_templates",
|
|
+ default=False,
|
|
+ action="store_true",
|
|
+ help="Execute all tests even for tests using shared templates; "
|
|
+ "otherwise, executes one test per template type"
|
|
+ )
|
|
+
|
|
subparsers = parser.add_subparsers(dest="subparser_name",
|
|
help="Subcommands: profile, rule, combined")
|
|
subparsers.required = True
|
|
@@ -345,6 +354,7 @@ def normalize_passed_arguments(options):
|
|
# Add in product to the test environment. This is independent of actual
|
|
# test environment type so we do it after creation.
|
|
options.test_env.product = options.product
|
|
+ options.test_env.duplicate_templates = options.duplicate_templates
|
|
|
|
try:
|
|
benchmark_cpes = xml_operations.benchmark_get_applicable_platforms(
|