Blame SOURCES/scap-security-guide-0.1.58-templated_tests-PR_7211.patch

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