Repository: freedomofpress/securedrop

Issue:
Stop pinning the "builder" docker image
## Description

We build our packages in a container using a custom docker image (see `molecule/builder-focal`). The way the image is currently used is that it's frozen/pinned, pushed to quay.io, and then the person building the packages will pull it down and use it, as is. But if there are any missing security updates, the build will fail after the fact and then you need to go through the hurdle of updating the builder, rebuilding packages, etc.

Also during the last builder update, I noticed that the logic to detect pending security updates itself was buggy, because it simulates a dist-upgrade and sees if any packages will be pulled in from focal-security, except we don't intend to do a dist-upgrade, so it complaining that a package not in the container was a pending security update!

The theory is that pinning the builder helps with reproducible builds, except it doesn't because the build isn't reproducible for other reasons.

## Proposal

We just build the image on the fly. At runtime we see if there are any pending updates and if so, bail. The deployer can rebuild the image locally and restart the package building. For reproducibility, we will implement #6356, but that shouldn't block this.


Discussion:

Resolving patch:
diff --git a/install_files/ansible-base/roles/build-ossec-deb-pkg/library/ossec_urls.py b/install_files/ansible-base/roles/build-ossec-deb-pkg/library/ossec_urls.py
deleted file mode 100644
--- a/install_files/ansible-base/roles/build-ossec-deb-pkg/library/ossec_urls.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/env python
-DOCUMENTATION = """
----
-module: ossec_urls
-short_description: Gather facts for OSSEC download URLs
-description:
-  - Gather version, checksum, and URL info for OSSEC downloads
-author:
-    - Conor Schaefer (@conorsch)
-    - Freedom of the Press Foundation (@freedomofpress)
-requirements:
-    - requests
-options:
-  ossec_version:
-    description:
-      - version number of release to download
-    default: "3.6.0"
-    required: no
-notes:
-  - The OSSEC version to download is hardcoded to avoid surprises.
-    If you want a newer version than the current default, you should
-    pass the version in via I(ossec_version).
-"""
-EXAMPLES = """
-- ossec_urls:
-    ossec_version: "3.6.0"
-"""
-
-
-HAS_REQUESTS = True
-try:
-    import requests  # lgtm [py/unused-import] # noqa: F401
-except ImportError:
-    HAS_REQUESTS = False
-
-
-class OSSECURLs:
-    def __init__(self, ossec_version):
-        self.REPO_URL = "https://github.com/ossec/ossec-hids"
-        self.ossec_version = ossec_version
-        self.ansible_facts = dict(
-            ossec_version=self.ossec_version,
-            ossec_tarball_filename=self.ossec_tarball_filename,
-            ossec_tarball_url=self.ossec_tarball_url,
-            ossec_signature_filename=self.ossec_signature_filename,
-            ossec_signature_url=self.ossec_signature_url,
-        )
-
-    @property
-    def ossec_tarball_filename(self):
-        return f"ossec-hids-{self.ossec_version}.tar.gz"
-
-    @property
-    def ossec_tarball_url(self):
-        return self.REPO_URL + f"/archive/{self.ossec_version}.tar.gz"
-
-    @property
-    def ossec_signature_url(self):
-        return self.REPO_URL + "/releases/download/{}/{}".format(
-            self.ossec_version, self.ossec_signature_filename
-        )
-
-    @property
-    def ossec_signature_filename(self):
-        return f"ossec-hids-{self.ossec_version}.tar.gz.asc"
-
-
-def main():
-    module = AnsibleModule(  # noqa: F405
-        argument_spec=dict(
-            ossec_version=dict(default="3.6.0"),
-        ),
-        supports_check_mode=False,
-    )
-    if not HAS_REQUESTS:
-        module.fail_json(msg="requests required for this module")
-
-    ossec_version = module.params["ossec_version"]
-    try:
-        ossec_config = OSSECURLs(ossec_version=ossec_version)
-    except Exception:
-        msg = (
-            "Failed to find checksum information for OSSEC v{}."
-            "Ensure you have the proper release specified, "
-            "and check the download page to confirm: "
-            "http://www.ossec.net/?page_id=19".format(ossec_version)
-        )
-        module.fail_json(msg=msg)
-
-    results = ossec_config.ansible_facts
-
-    if results:
-        module.exit_json(changed=False, ansible_facts=results)
-    else:
-        msg = "Failed to fetch OSSEC URL facts."
-        module.fail_json(msg=msg)
-
-
-from ansible.module_utils.basic import *  # noqa E402,F403
-
-main()


Test patch:
diff --git a/builder/tests/test_ossec_package.py b/builder/tests/test_ossec_package.py
new file mode 100644
--- /dev/null
+++ b/builder/tests/test_ossec_package.py
@@ -0,0 +1,77 @@
+import re
+import subprocess
+from pathlib import Path
+
+OSSEC_VERSION = "3.6.0"
+
+SECUREDROP_ROOT = Path(
+    subprocess.check_output(["git", "rev-parse", "--show-toplevel"]).decode().strip()
+)
+BUILD_DIRECTORY = SECUREDROP_ROOT / "build/focal"
+
+
+def test_ossec_binaries_are_present_agent():
+    """
+    Inspect the package contents to ensure all ossec agent binaries are properly
+    included in the package.
+    """
+    wanted_files = [
+        "/var/ossec/bin/agent-auth",
+        "/var/ossec/bin/ossec-syscheckd",
+        "/var/ossec/bin/ossec-agentd",
+        "/var/ossec/bin/manage_agents",
+        "/var/ossec/bin/ossec-control",
+        "/var/ossec/bin/ossec-logcollector",
+        "/var/ossec/bin/util.sh",
+        "/var/ossec/bin/ossec-execd",
+    ]
+    path = BUILD_DIRECTORY / f"ossec-agent_{OSSEC_VERSION}+focal_amd64.deb"
+    contents = subprocess.check_output(["dpkg-deb", "-c", str(path)]).decode()
+    for wanted_file in wanted_files:
+        assert re.search(
+            r"^.* .{}$".format(wanted_file),
+            contents,
+            re.M,
+        )
+
+
+def test_ossec_binaries_are_present_server():
+    """
+    Inspect the package contents to ensure all ossec server binaries are properly
+    included in the package.
+    """
+    wanted_files = [
+        "/var/ossec/bin/ossec-maild",
+        "/var/ossec/bin/ossec-remoted",
+        "/var/ossec/bin/ossec-syscheckd",
+        "/var/ossec/bin/ossec-makelists",
+        "/var/ossec/bin/ossec-logtest",
+        "/var/ossec/bin/syscheck_update",
+        "/var/ossec/bin/ossec-reportd",
+        "/var/ossec/bin/ossec-agentlessd",
+        "/var/ossec/bin/manage_agents",
+        "/var/ossec/bin/rootcheck_control",
+        "/var/ossec/bin/ossec-control",
+        "/var/ossec/bin/ossec-dbd",
+        "/var/ossec/bin/ossec-csyslogd",
+        "/var/ossec/bin/ossec-regex",
+        "/var/ossec/bin/agent_control",
+        "/var/ossec/bin/ossec-monitord",
+        "/var/ossec/bin/clear_stats",
+        "/var/ossec/bin/ossec-logcollector",
+        "/var/ossec/bin/list_agents",
+        "/var/ossec/bin/verify-agent-conf",
+        "/var/ossec/bin/syscheck_control",
+        "/var/ossec/bin/util.sh",
+        "/var/ossec/bin/ossec-analysisd",
+        "/var/ossec/bin/ossec-execd",
+        "/var/ossec/bin/ossec-authd",
+    ]
+    path = BUILD_DIRECTORY / f"ossec-server_{OSSEC_VERSION}+focal_amd64.deb"
+    contents = subprocess.check_output(["dpkg-deb", "-c", str(path)]).decode()
+    for wanted_file in wanted_files:
+        assert re.search(
+            r"^.* .{}$".format(wanted_file),
+            contents,
+            re.M,
+        )
diff --git a/builder/tests/test_securedrop_deb_package.py b/builder/tests/test_securedrop_deb_package.py
new file mode 100644
--- /dev/null
+++ b/builder/tests/test_securedrop_deb_package.py
@@ -0,0 +1,102 @@
+import os
+import subprocess
+import tempfile
+from pathlib import Path
+
+import pytest
+
+SECUREDROP_ROOT = Path(
+    subprocess.check_output(["git", "rev-parse", "--show-toplevel"]).decode().strip()
+)
+DEB_PATHS = list((SECUREDROP_ROOT / "build/focal").glob("*.deb"))
+
+
+@pytest.fixture(scope="module")
+def securedrop_app_code_contents() -> str:
+    """
+    Returns the content listing of the securedrop-app-code Debian package.
+    """
+    try:
+        path = [pkg for pkg in DEB_PATHS if pkg.name.startswith("securedrop-app-code")][0]
+    except IndexError:
+        raise RuntimeError("Unable to find securedrop-app-code package in build/ folder")
+    return subprocess.check_output(["dpkg-deb", "--contents", path]).decode()
+
+
+@pytest.mark.parametrize("deb", DEB_PATHS)
+def test_deb_packages_appear_installable(deb: Path) -> None:
+    """
+    Confirms that a dry-run of installation reports no errors.
+    Simple check for valid Debian package structure, but not thorough.
+    When run on a malformed package, `dpkg` will report:
+
+       dpkg-deb: error: `foo.deb' is not a debian format archive
+
+    Testing application behavior is left to the functional tests.
+    """
+
+    # Normally this is called as root, but we can get away with simply
+    # adding sbin to the path
+    path = os.getenv("PATH") + ":/usr/sbin"
+    subprocess.check_call(["dpkg", "--install", "--dry-run", deb], env={"PATH": path})
+
+
+@pytest.mark.parametrize("deb", DEB_PATHS)
+def test_deb_package_contains_expected_conffiles(deb: Path):
+    """
+    Ensures the `securedrop-app-code` package declares only allow-listed
+    `conffiles`. Several files in `/etc/` would automatically be marked
+    conffiles, which would break unattended updates to critical package
+    functionality such as AppArmor profiles. This test validates overrides
+    in the build logic to unset those conffiles.
+
+    The same applies to `securedrop-config` too.
+    """
+    if not deb.name.startswith(("securedrop-app-code", "securedrop-config")):
+        return
+
+    with tempfile.TemporaryDirectory() as tmpdir:
+        subprocess.check_call(["dpkg-deb", "--control", deb, tmpdir])
+        conffiles_path = Path(tmpdir) / "conffiles"
+        assert conffiles_path.exists()
+        # No files are currently allow-listed to be conffiles
+        assert conffiles_path.read_text().rstrip() == ""
+
+
+@pytest.mark.parametrize(
+    "path",
+    (
+        "/var/www/securedrop/.well-known/pki-validation/",
+        "/var/www/securedrop/translations/messages.pot",
+        "/var/www/securedrop/translations/de_DE/LC_MESSAGES/messages.mo",
+    ),
+)
+def test_app_code_paths(securedrop_app_code_contents: str, path: str):
+    """
+    Ensures the `securedrop-app-code` package contains the specified paths
+    """
+    for line in securedrop_app_code_contents.splitlines():
+        if line.endswith(path):
+            assert True
+            return
+
+    assert False, "not found"
+
+
+@pytest.mark.parametrize(
+    "path",
+    (
+        "/var/www/securedrop/static/.webassets-cache/",
+        "/var/www/securedrop/static/gen/",
+        "/var/www/securedrop/config.py",
+        "/var/www/static/i/custom_logo.png",
+        ".j2",
+    ),
+)
+def test_app_code_paths_missing(securedrop_app_code_contents: str, path: str):
+    """
+    Ensures the `securedrop-app-code` package do *NOT* contain the specified paths
+    """
+    for line in securedrop_app_code_contents.splitlines():
+        if line.endswith(path):
+            assert False, line
diff --git a/molecule/builder-focal/tests/conftest.py b/molecule/builder-focal/tests/conftest.py
deleted file mode 100644
--- a/molecule/builder-focal/tests/conftest.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""
-Import variables from vars.yml and inject into testutils namespace
-"""
-
-import subprocess
-from pathlib import Path
-
-import pytest
-
-
-@pytest.fixture(scope="session")
-def securedrop_root() -> Path:
-    """
-    Returns the root of the SecureDrop working tree for the test session.
-    """
-    return Path(
-        subprocess.run(["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE, check=True)
-        .stdout.decode("utf-8")
-        .strip()
-    )
diff --git a/molecule/builder-focal/tests/test_build_dependencies.py b/molecule/builder-focal/tests/test_build_dependencies.py
deleted file mode 100644
--- a/molecule/builder-focal/tests/test_build_dependencies.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import os
-
-import pytest
-
-SECUREDROP_TARGET_DISTRIBUTION = os.environ.get("SECUREDROP_TARGET_DISTRIBUTION")
-SECUREDROP_PYTHON_VERSION = os.environ.get("SECUREDROP_PYTHON_VERSION", "3.8")
-DH_VIRTUALENV_VERSION = "1.2.2"
-
-testinfra_hosts = [f"docker://{SECUREDROP_TARGET_DISTRIBUTION}-sd-app"]
-
-
-@pytest.mark.xfail(reason="This check conflicts with the concept of pegging" "dependencies")
-def test_build_all_packages_updated(host):
-    """
-    Ensure a dist-upgrade has already been run, by checking that no
-    packages are eligible for upgrade currently. This will ensure that
-    all upgrades, security and otherwise, have been applied to the VM
-    used to build packages.
-    """
-    c = host.run("apt-get --simulate -y dist-upgrade")
-    assert c.rc == 0
-    assert "No packages will be installed, upgraded, or removed." in c.stdout
-
-
-def test_python_version(host):
-    """
-    The Python 3 version shouldn't change between LTS releases, but we're
-    pulling in some packages from Debian for dh-virtualenv support, so
-    we must be careful not to change Python as well.
-    """
-    c = host.run("python3 --version")
-    version_string = f"Python {SECUREDROP_PYTHON_VERSION}"
-    assert c.stdout.startswith(version_string)
-
-
-def test_dh_virtualenv(host):
-    """
-    Confirm the expected version of dh-virtualenv is found.
-    """
-    expected_version = DH_VIRTUALENV_VERSION
-    version_string = f"dh_virtualenv {expected_version}"
-    c = host.run("dh_virtualenv --version")
-    assert c.stdout.startswith(version_string)
diff --git a/molecule/builder-focal/tests/test_legacy_paths.py b/molecule/builder-focal/tests/test_legacy_paths.py
deleted file mode 100644
--- a/molecule/builder-focal/tests/test_legacy_paths.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import pytest
-
-
-@pytest.mark.parametrize(
-    "build_path",
-    [
-        "/tmp/build-",
-        "/tmp/rsync-filter",
-        "/tmp/src_install_files",
-        "/tmp/build-securedrop-keyring",
-        "/tmp/build-securedrop-ossec-agent",
-        "/tmp/build-securedrop-ossec-server",
-    ],
-)
-def test_build_ossec_apt_dependencies(host, build_path):
-    """
-    Ensure that unwanted build paths are absent. Most of these were created
-    as unwanted side-effects during CI-related changes to the build scripts.
-
-    All paths are rightly considered "legacy" and should never be present on
-    the build host. This test is strictly for guarding against regressions.
-    """
-    assert not host.file(build_path).exists
diff --git a/molecule/builder-focal/tests/test_securedrop_deb_package.py b/molecule/builder-focal/tests/test_securedrop_deb_package.py
deleted file mode 100644
--- a/molecule/builder-focal/tests/test_securedrop_deb_package.py
+++ /dev/null
@@ -1,510 +0,0 @@
-import os
-import re
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Pattern, Tuple
-
-import pytest
-import yaml
-from testinfra.host import Host
-
-SECUREDROP_TARGET_DISTRIBUTION = os.environ.get("SECUREDROP_TARGET_DISTRIBUTION")
-testinfra_hosts = [f"docker://{SECUREDROP_TARGET_DISTRIBUTION}-sd-dpkg-verification"]
-
-
-def extract_package_name_from_filepath(filepath: str) -> str:
-    """
-    Helper function to infer intended package name from
-    the absolute filepath, using a rather garish regex.
-    E.g., given:
-       securedrop-ossec-agent-2.8.2+0.3.10-amd64.deb
-
-    returns:
-
-       securedrop-ossec-agent
-
-    which can then be used for comparisons in dpkg output.
-    """
-    deb_basename = os.path.basename(filepath)
-    package_match = re.search(r"^([a-z\-]+(?!\d))", deb_basename)
-    assert package_match
-    package_name = package_match.groups()[0]
-    assert deb_basename.startswith(package_name)
-    return package_name
-
-
-def load_securedrop_test_vars() -> Dict[str, Any]:
-    """
-    Loads vars.yml into a dictionary.
-    """
-    filepath = os.path.join(os.path.dirname(__file__), "vars.yml")
-    test_vars = yaml.safe_load(open(filepath))
-
-    # Tack on target OS for use in tests
-    test_vars["securedrop_target_distribution"] = os.environ.get("SECUREDROP_TARGET_DISTRIBUTION")
-
-    return test_vars
-
-
-securedrop_test_vars = load_securedrop_test_vars()
-
-
-def make_deb_paths() -> Dict[str, Path]:
-    """
-    Helper function to retrieve module-namespace test vars and format
-    the strings to interpolate version info. Keeps the test vars DRY
-    in terms of version info, and required since we can't rely on
-    Jinja-based evaluation of the YAML files (so we can't trivially
-    reuse vars in other var values, as is the case with Ansible).
-    """
-
-    substitutions = dict(
-        securedrop_version=securedrop_test_vars["securedrop_version"],
-        ossec_version=securedrop_test_vars["ossec_version"],
-        keyring_version=securedrop_test_vars["keyring_version"],
-        securedrop_target_distribution=securedrop_test_vars["securedrop_target_distribution"],
-    )
-
-    return {
-        package_name: Path(path.format(**substitutions))
-        for package_name, path in securedrop_test_vars["deb_paths"].items()
-    }
-
-
-deb_paths = make_deb_paths()
-
-
-@pytest.fixture(scope="module")
-def securedrop_app_code_contents(host) -> str:
-    """
-    Returns the content listing of the securedrop-app-code Debian package on the host.
-    """
-    return host.run("dpkg-deb --contents {}".format(deb_paths["securedrop_app_code"])).stdout
-
-
-def deb_tags() -> List[Tuple[Path, str]]:
-    """
-    Helper function to build array of package and tag tuples
-    for lintian.
-    """
-    tags = []
-
-    for deb in deb_paths.values():
-        for tag in securedrop_test_vars["lintian_tags"]:
-            tags.append((deb, tag))
-
-    return tags
-
-
-def get_source_paths(
-    starting_path: Path,
-    pattern: str = "**/*",
-    skip: Optional[List[Pattern]] = None,
-) -> List[Path]:
-    """
-    Returns paths under starting_path that match the given pattern.
-
-    The starting_path argument specifies where to look for files. The
-    optional pattern and skip parameters can be used to filter the
-    results.
-    """
-    paths = sorted(p for p in starting_path.glob(pattern))
-    if skip:
-        paths = [p for p in paths if not any(s.search(str(p)) for s in skip)]
-    assert paths
-    return paths
-
-
-def get_static_asset_paths(securedrop_root: Path, pattern: str) -> List[Path]:
-    """
-    Returns static assets matching the pattern.
-    """
-    static_dir = securedrop_root / "securedrop/static"
-    skip = [re.compile(p) for p in [r"\.map$", r"\.webassets-cache$", "custom_logo.png$"]]
-    return get_source_paths(static_dir, pattern, skip)
-
-
-def verify_static_assets(securedrop_app_code_contents: str, securedrop_root: Path, pattern) -> None:
-    """
-    Verifies that the securedrop-app-code package contains the given static assets.
-
-    Example: verify_assets(securedrop_app_code_contents, "icons/**.png")
-    """
-    for asset_path in get_static_asset_paths(securedrop_root, pattern):
-        assert re.search(
-            r"^.*\./var/www/" "{}$".format(asset_path.relative_to(securedrop_root)),
-            securedrop_app_code_contents,
-            re.M,
-        )
-
-
-@pytest.mark.parametrize("deb", deb_paths.values())
-def test_build_deb_packages(host: Host, deb: Path) -> None:
-    """
-    Sanity check the built Debian packages for Control field
-    values and general package structure.
-    """
-    deb_package = host.file(str(deb))
-    assert deb_package.is_file
-
-
-@pytest.mark.parametrize("deb", deb_paths.values())
-def test_deb_packages_appear_installable(host: Host, deb: Path) -> None:
-    """
-    Confirms that a dry-run of installation reports no errors.
-    Simple check for valid Debian package structure, but not thorough.
-    When run on a malformed package, `dpkg` will report:
-
-       dpkg-deb: error: `foo.deb' is not a debian format archive
-
-    Testing application behavior is left to the functional tests.
-    """
-
-    package_name = extract_package_name_from_filepath(str(deb))
-    assert deb.name.startswith(package_name)
-
-    # sudo is required to call `dpkg --install`, even as dry-run.
-    with host.sudo():
-        c = host.run(f"dpkg --install --dry-run {deb}")
-        assert f"Selecting previously unselected package {package_name}" in c.stdout
-        regex = f"Preparing to unpack [./]+{re.escape(deb.name)} ..."
-        assert re.search(regex, c.stdout, re.M)
-        assert c.rc == 0
-
-
-@pytest.mark.parametrize("deb", deb_paths.values())
-def test_deb_package_control_fields(host: Host, deb: Path) -> None:
-    """
-    Ensure Debian Control fields are populated as expected in the package.
-    These checks are rather superficial, and don't actually confirm that the
-    .deb files are not broken. At a later date, consider integration tests
-    that actually use these built files during an Ansible provisioning run.
-    """
-    package_name = extract_package_name_from_filepath(str(deb))
-    # The `--field` option will display all fields if none are specified.
-    c = host.run(f"dpkg-deb --field {deb}")
-
-    assert "Maintainer: SecureDrop Team <securedrop@freedom.press>" in c.stdout
-    arch_all = (
-        "securedrop-config",
-        "securedrop-keyring",
-        "securedrop-ossec-agent",
-        "securedrop-ossec-server",
-    )
-    # Some packages are architecture independent
-    if package_name in arch_all:
-        assert "Architecture: all" in c.stdout
-    else:
-        assert "Architecture: amd64" in c.stdout
-
-    assert f"Package: {package_name}" in c.stdout
-    assert c.rc == 0
-
-
-@pytest.mark.parametrize("deb", deb_paths.values())
-def test_deb_package_control_fields_homepage(host: Host, deb: Path):
-    # The `--field` option will display all fields if none are specified.
-    c = host.run(f"dpkg-deb --field {deb}")
-    # The OSSEC source packages will have a different homepage;
-    # all other packages should set securedrop.org as homepage.
-    if deb.name.startswith("ossec-"):
-        assert "Homepage: http://ossec.net" in c.stdout
-    else:
-        assert "Homepage: https://securedrop.org" in c.stdout
-
-
-def test_securedrop_app_code_contains_no_config_file(securedrop_app_code_contents: str):
-    """
-    Ensures the `securedrop-app-code` package does not ship a `config.py`
-    file. Doing so would clobber the site-specific changes made via Ansible.
-    """
-    assert not re.search(r"^ ./var/www/securedrop/config.py$", securedrop_app_code_contents, re.M)
-
-
-def test_securedrop_app_code_contains_pot_file(securedrop_app_code_contents: str):
-    """
-    Ensures the `securedrop-app-code` package has the
-    messages.pot file
-    """
-    assert re.search(
-        "^.* ./var/www/securedrop/translations/messages.pot$",
-        securedrop_app_code_contents,
-        re.M,
-    )
-
-
-def test_securedrop_app_code_contains_mo_files(
-    securedrop_root: Path, securedrop_app_code_contents: str
-) -> None:
-    """
-    Ensures the `securedrop-app-code` package has a compiled version of each .po file.
-    """
-    po_paths = get_source_paths(securedrop_root / "securedrop/translations", "**/*.po")
-    mo_paths = [p.with_suffix(".mo") for p in po_paths]
-    for mo in mo_paths:
-        assert re.search(
-            r"^.* \./var/www/" "{}$".format(mo.relative_to(securedrop_root)),
-            securedrop_app_code_contents,
-            re.M,
-        )
-
-
-def test_deb_package_contains_no_generated_assets(securedrop_app_code_contents: str):
-    """
-    Ensures the `securedrop-app-code` package does not ship minified
-    static assets previously built at runtime via Flask-Assets under /gen, and
-    which may be present in the source directory used to build from.
-    """
-    # static/gen/ directory not should exist
-    assert not re.search(
-        r"^.*\./var/www/securedrop" "/static/gen/$", securedrop_app_code_contents, re.M
-    )
-
-    # static/.webassets-cache/ directory should not exist
-    assert not re.search(
-        r"^.*\./var/www/securedrop" "/static/.webassets-cache/$",
-        securedrop_app_code_contents,
-        re.M,
-    )
-
-
-@pytest.mark.parametrize("deb", deb_paths.values())
-def test_deb_package_contains_expected_conffiles(host: Host, deb: Path):
-    """
-    Ensures the `securedrop-app-code` package declares only allow-listed
-    `conffiles`. Several files in `/etc/` would automatically be marked
-    conffiles, which would break unattended updates to critical package
-    functionality such as AppArmor profiles. This test validates overrides
-    in the build logic to unset those conffiles.
-
-    The same applies to `securedrop-config` too.
-    """
-    if not deb.name.startswith(("securedrop-app-code", "securedrop-config")):
-        return
-
-    mktemp = host.run("mktemp -d")
-    tmpdir = mktemp.stdout.strip()
-    # The `--raw-extract` flag includes `DEBIAN/` dir with control files
-    host.run(f"dpkg-deb --raw-extract {deb} {tmpdir}")
-    conffiles_path = os.path.join(tmpdir, "DEBIAN", "conffiles")
-    f = host.file(conffiles_path)
-
-    assert f.is_file
-
-    conffiles = f.content_string.rstrip()
-
-    # No files are currently allow-listed to be conffiles
-    assert conffiles == ""
-
-
-def test_securedrop_app_code_contains_css(securedrop_app_code_contents: str) -> None:
-    """
-    Ensures the `securedrop-app-code` package contains necessary css files
-    """
-    for css_type in ["journalist", "source"]:
-        assert re.search(
-            r"^.*\./var/www/securedrop/static/" "css/{}.css$".format(css_type),
-            securedrop_app_code_contents,
-            re.M,
-        )
-
-
-def test_securedrop_app_code_contains_static_fonts(
-    securedrop_root: Path, securedrop_app_code_contents: str
-):
-    """
-    Ensures the `securedrop-app-code` package contains everything under securedrop/fonts.
-    """
-    verify_static_assets(securedrop_app_code_contents, securedrop_root, "fonts/*.*")
-
-
-def test_securedrop_app_code_contains_static_i(
-    securedrop_root: Path, securedrop_app_code_contents: str
-):
-    """
-    Ensures the `securedrop-app-code` package contains everything under securedrop/static/i.
-    """
-    verify_static_assets(securedrop_app_code_contents, securedrop_root, "i/**/*.*")
-
-
-def test_securedrop_app_code_contains_static_icons(
-    securedrop_root: Path, securedrop_app_code_contents: str
-):
-    """
-    Ensures the `securedrop-app-code` package contains all the icons.
-    """
-    verify_static_assets(securedrop_app_code_contents, securedrop_root, "icons/*.png")
-
-
-def test_securedrop_app_code_contains_static_javascript(
-    securedrop_root: Path, securedrop_app_code_contents: str
-):
-    """
-    Ensures the `securedrop-app-code` package contains all the JavaScript.
-    """
-    verify_static_assets(securedrop_app_code_contents, securedrop_root, "js/*.js")
-
-
-@pytest.mark.parametrize("deb, tag", deb_tags())
-def test_deb_package_lintian(host: Host, deb: Path, tag: str):
-    """
-    Ensures lintian likes our Debian packages.
-    """
-    c = host.run(f"lintian --tags {tag} --no-tag-display-limit {deb}")
-    assert len(c.stdout) == 0
-
-
-def test_deb_app_package_contains_https_validate_dir(securedrop_app_code_contents: str):
-    """
-    Ensures the `securedrop-app-code` package ships with a validation
-    '.well-known/pki-validation' directory
-    """
-    # well-known/pki-validation directory should exist
-    assert re.search(
-        r"^.*\./var/www/securedrop/" ".well-known/pki-validation/$",
-        securedrop_app_code_contents,
-        re.M,
-    )
-
-
-def test_control_helper_files_are_present(host: Host):
-    """
-    Inspect the package info to get a list of helper scripts
-    that should be shipped with the package, e.g. postinst, prerm, etc.
-    Necessary due to package build logic retooling.
-
-    Example output from package info, for reference:
-
-      $ dpkg-deb --info securedrop-app-code_0.12.0~rc1_amd64.deb
-      new debian package, version 2.0.
-      size 13583186 bytes: control archive=11713 bytes.
-           62 bytes,     2 lines      conffiles
-          657 bytes,    10 lines      control
-        26076 bytes,   298 lines      md5sums
-         5503 bytes,   159 lines   *  postinst             #!/bin/bash
-
-    Note that the actual output will have trailing whitespace, removed
-    from this text description to satisfy linters.
-    """
-    wanted_files = [
-        "conffiles",
-        "config",
-        "control",
-        "postinst",
-        "postrm",
-        "preinst",
-        "prerm",
-    ]
-    c = host.run("dpkg-deb --info {}".format(deb_paths["securedrop_app_code"]))
-    for wanted_file in wanted_files:
-        assert re.search(
-            r"^\s+?\d+ bytes,\s+\d+ lines[\s*]+" + wanted_file + r"\s+.*$",
-            c.stdout,
-            re.M,
-        )
-
-
-@pytest.mark.parametrize("deb", deb_paths.values())
-def test_jinja_files_not_present(host: Host, deb: Path):
-    """
-    Make sure that jinja (.j2) files were not copied over
-    as-is into the debian packages.
-    """
-
-    c = host.run(f"dpkg-deb --contents {deb}")
-    # There shouldn't be any files with a .j2 ending
-    assert not re.search(r"^.*\.j2$", c.stdout, re.M)
-
-
-def test_ossec_binaries_are_present_agent(host: Host):
-    """
-    Inspect the package contents to ensure all ossec agent binaries are properly
-    included in the package.
-    """
-    wanted_files = [
-        "/var/ossec/bin/agent-auth",
-        "/var/ossec/bin/ossec-syscheckd",
-        "/var/ossec/bin/ossec-agentd",
-        "/var/ossec/bin/manage_agents",
-        "/var/ossec/bin/ossec-control",
-        "/var/ossec/bin/ossec-logcollector",
-        "/var/ossec/bin/util.sh",
-        "/var/ossec/bin/ossec-execd",
-    ]
-    c = host.run("dpkg-deb -c {}".format(deb_paths["ossec_agent"]))
-    for wanted_file in wanted_files:
-        assert re.search(
-            rf"^.* .{wanted_file}$",
-            c.stdout,
-            re.M,
-        )
-
-
-def test_ossec_binaries_are_present_server(host: Host):
-    """
-    Inspect the package contents to ensure all ossec server binaries are properly
-    included in the package.
-    """
-    wanted_files = [
-        "/var/ossec/bin/ossec-maild",
-        "/var/ossec/bin/ossec-remoted",
-        "/var/ossec/bin/ossec-syscheckd",
-        "/var/ossec/bin/ossec-makelists",
-        "/var/ossec/bin/ossec-logtest",
-        "/var/ossec/bin/syscheck_update",
-        "/var/ossec/bin/ossec-reportd",
-        "/var/ossec/bin/ossec-agentlessd",
-        "/var/ossec/bin/manage_agents",
-        "/var/ossec/bin/rootcheck_control",
-        "/var/ossec/bin/ossec-control",
-        "/var/ossec/bin/ossec-dbd",
-        "/var/ossec/bin/ossec-csyslogd",
-        "/var/ossec/bin/ossec-regex",
-        "/var/ossec/bin/agent_control",
-        "/var/ossec/bin/ossec-monitord",
-        "/var/ossec/bin/clear_stats",
-        "/var/ossec/bin/ossec-logcollector",
-        "/var/ossec/bin/list_agents",
-        "/var/ossec/bin/verify-agent-conf",
-        "/var/ossec/bin/syscheck_control",
-        "/var/ossec/bin/util.sh",
-        "/var/ossec/bin/ossec-analysisd",
-        "/var/ossec/bin/ossec-execd",
-        "/var/ossec/bin/ossec-authd",
-    ]
-    c = host.run("dpkg-deb --contents {}".format(deb_paths["ossec_server"]))
-    for wanted_file in wanted_files:
-        assert re.search(
-            rf"^.* .{wanted_file}$",
-            c.stdout,
-            re.M,
-        )
-
-
-def test_config_package_contains_expected_files(host: Host) -> None:
-    """
-    Inspect the package contents to ensure all config files are included in
-    the package.
-    """
-    wanted_files = [
-        "/etc/profile.d/securedrop_additions.sh",
-        "/opt/securedrop/20auto-upgrades",
-        "/opt/securedrop/50unattended-upgrades",
-        "/opt/securedrop/reboot-flag",
-    ]
-    c = host.run("dpkg-deb --contents {}".format(deb_paths["securedrop_config"]))
-    for wanted_file in wanted_files:
-        assert re.search(
-            rf"^.* .{wanted_file}$",
-            c.stdout,
-            re.M,
-        )
-
-
-def test_app_package_does_not_contain_custom_logo(
-    securedrop_app_code_contents: str,
-) -> None:
-    """
-    Inspect the package contents to ensure custom_logo.png is not present. This
-    is because custom_logo.png supersedes logo.png.
-    """
-    assert "/var/www/static/i/custom_logo.png" not in securedrop_app_code_contents
diff --git a/molecule/builder-focal/tests/test_security_updates.py b/molecule/builder-focal/tests/test_security_updates.py
deleted file mode 100644
--- a/molecule/builder-focal/tests/test_security_updates.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import os
-import re
-from subprocess import check_output
-
-import pytest
-
-SECUREDROP_TARGET_DISTRIBUTION = os.environ.get("SECUREDROP_TARGET_DISTRIBUTION")
-testinfra_hosts = [f"docker://{SECUREDROP_TARGET_DISTRIBUTION}-sd-sec-update"]
-
-
-def test_should_run():
-    command = ["git", "describe", "--all"]
-    version = check_output(command).decode("utf8")[0:-1]
-    candidates = (
-        r"(^tags/[\d]+\.[\d]+\.[\d]+-rc[\d]+)|"
-        r"(^tags/[\d]+\.[\d]+\.[\d]+)|"
-        r"(^heads/release/[\d]+\.[\d]+\.[\d]+)|"
-        r"(^heads/update-builder.*)"
-    )
-    result = re.match(candidates, version)
-    if result:
-        return True
-    else:
-        return False
-
-
-@pytest.mark.skipif(not test_should_run(), reason="Only tested for RCs and builder updates")
-def test_ensure_no_updates_avail(host):
-    """
-    Test to make sure that there are no security-updates in the
-    base builder container.
-    """
-    # Filter out all the security repos to their own file
-    # without this change all the package updates appeared as if they were
-    # coming from normal ubuntu update channel (since they get posted to both)
-    host.run('egrep "^deb.*security" /etc/apt/sources.list > /tmp/sec.list')
-
-    dist_upgrade_simulate = host.run(
-        "apt-get -s dist-upgrade "
-        "-oDir::Etc::Sourcelist=/tmp/sec.list "
-        '|grep "^Inst" |grep -i security'
-    )
-
-    # If the grep was successful that means security package updates found
-    # otherwise we get a non-zero exit code so no updates needed.
-    assert dist_upgrade_simulate.rc != 0
diff --git a/molecule/builder-focal/tests/vars.yml b/molecule/builder-focal/tests/vars.yml
deleted file mode 100644
--- a/molecule/builder-focal/tests/vars.yml
+++ /dev/null
@@ -1,21 +0,0 @@
----
-securedrop_version: "2.6.0~rc1"
-ossec_version: "3.6.0"
-keyring_version: "0.1.6"
-
-# These values will be interpolated with values populated above
-# via helper functions in the tests.
-deb_paths:
-  securedrop_app_code: /tmp/build/securedrop-app-code_{securedrop_version}+{securedrop_target_distribution}_amd64.deb
-  securedrop_ossec_agent: /tmp/build/securedrop-ossec-agent_{ossec_version}+{securedrop_version}+{securedrop_target_distribution}_all.deb
-  securedrop_ossec_server: /tmp/build/securedrop-ossec-server_{ossec_version}+{securedrop_version}+{securedrop_target_distribution}_all.deb
-  ossec_server: /tmp/build/ossec-server-{ossec_version}+{securedrop_target_distribution}-amd64.deb
-  ossec_agent: /tmp/build/ossec-agent-{ossec_version}+{securedrop_target_distribution}-amd64.deb
-  securedrop_keyring: /tmp/build/securedrop-keyring_{keyring_version}+{securedrop_version}+{securedrop_target_distribution}_all.deb
-  securedrop_config: /tmp/build/securedrop-config_{securedrop_version}+{securedrop_target_distribution}_all.deb
-
-lintian_tags:
-  # - non-standard-file-perm
-  - package-contains-vcs-control-file
-  - package-installs-python-bytecode
-  # - wrong-file-owner-uid-or-gid