170 lines
6.6 KiB
Python
170 lines
6.6 KiB
Python
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Static checks for package metadata and install hardening."""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import tomllib
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from enodia_sentinel import __version__
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _read(path: str) -> str:
|
|
return (ROOT / path).read_text()
|
|
|
|
|
|
class TestArchPackaging(unittest.TestCase):
|
|
def test_pkgbuild_version_matches_project(self):
|
|
pkgbuild = _read("packaging/PKGBUILD")
|
|
pyproject = tomllib.loads(_read("pyproject.toml"))
|
|
docs_version = json.loads(_read("docs/VERSION.json"))
|
|
|
|
pkgver = re.search(r"^pkgver=([^\n]+)$", pkgbuild, re.M)
|
|
self.assertIsNotNone(pkgver)
|
|
self.assertEqual(pkgver.group(1), pyproject["project"]["version"])
|
|
self.assertEqual(pkgver.group(1), __version__)
|
|
self.assertEqual(docs_version["package_version"], pkgver.group(1))
|
|
|
|
def test_pkgbuild_installs_hardened_runtime_files(self):
|
|
pkgbuild = _read("packaging/PKGBUILD")
|
|
for expected in (
|
|
"/usr/bin/enodia-sentinel",
|
|
"/usr/lib/systemd/system/enodia-sentinel.service",
|
|
"/usr/lib/systemd/system/enodia-sentinel-web.service",
|
|
"/usr/share/libalpm/hooks/enodia-sentinel-fim.hook",
|
|
"/usr/lib/tmpfiles.d/enodia-sentinel.conf",
|
|
"/usr/share/doc/$pkgname/PACKAGING.md",
|
|
"/usr/share/doc/$pkgname/VERSION.json",
|
|
"/usr/share/doc/$pkgname/examples/enodia-sentinel-ebpf.conf",
|
|
"/var/log/enodia-sentinel",
|
|
):
|
|
self.assertIn(expected, pkgbuild)
|
|
self.assertIn("backup=('etc/enodia-sentinel.toml')", pkgbuild)
|
|
self.assertIn("install -dm750", pkgbuild)
|
|
|
|
def test_alpm_hook_uses_absolute_sentinel_path(self):
|
|
hook = _read("packaging/enodia-sentinel-fim.hook")
|
|
self.assertIn("Exec = /usr/bin/enodia-sentinel fim-update", hook)
|
|
self.assertNotIn("/usr/bin/env enodia-sentinel", hook)
|
|
self.assertIn("When = PostTransaction", hook)
|
|
|
|
def test_tmpfiles_declares_private_log_dir(self):
|
|
tmpfiles = _read("packaging/enodia-sentinel.tmpfiles")
|
|
self.assertIn("d /var/log/enodia-sentinel 0750 root root -", tmpfiles)
|
|
|
|
def test_build_script_uses_clean_package_build(self):
|
|
script = _read("packaging/build-package.sh")
|
|
self.assertIn("set -eu", script)
|
|
self.assertIn("command -v makepkg", script)
|
|
self.assertIn("--cleanbuild", script)
|
|
|
|
|
|
def _git_worktree_dirty() -> int:
|
|
"""Mirror the dirty computation in release-artifacts.sh so the assertion
|
|
holds whether the tree is clean (work committed) or dirty (mid-change)."""
|
|
def q(*args: str) -> subprocess.CompletedProcess:
|
|
return subprocess.run(["git", *args], cwd=ROOT,
|
|
capture_output=True, text=True)
|
|
if q("diff", "--quiet").returncode != 0:
|
|
return 1
|
|
if q("diff", "--cached", "--quiet").returncode != 0:
|
|
return 1
|
|
if q("ls-files", "--others", "--exclude-standard").stdout.strip():
|
|
return 1
|
|
return 0
|
|
|
|
|
|
class TestReleaseArtifacts(unittest.TestCase):
|
|
def test_release_script_generates_checksummed_artifacts(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
env = os.environ.copy()
|
|
env.update({
|
|
"DISTDIR": d,
|
|
"RELEASE_ALLOW_DIRTY": "1",
|
|
"RELEASE_SIGN": "0",
|
|
})
|
|
result = subprocess.run(
|
|
[str(ROOT / "packaging" / "release-artifacts.sh")],
|
|
cwd=ROOT,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=True,
|
|
)
|
|
self.assertIn("Signing skipped", result.stdout)
|
|
|
|
version = tomllib.loads(_read("pyproject.toml"))["project"]["version"]
|
|
archive = Path(d) / f"enodia-sentinel-{version}.tar.gz"
|
|
manifest = Path(d) / "RELEASE_MANIFEST.json"
|
|
checksums = Path(d) / "SHA256SUMS"
|
|
|
|
self.assertTrue(archive.is_file())
|
|
self.assertTrue(manifest.is_file())
|
|
self.assertTrue(checksums.is_file())
|
|
self.assertFalse((Path(d) / "SHA256SUMS.asc").exists())
|
|
|
|
data = json.loads(manifest.read_text())
|
|
self.assertEqual(data["project"], "enodia-sentinel")
|
|
self.assertEqual(data["version"], version)
|
|
self.assertEqual(data["dirty"], _git_worktree_dirty())
|
|
self.assertIn(archive.name, data["artifacts"])
|
|
self.assertIn("SHA256SUMS", data["artifacts"])
|
|
|
|
expected = {
|
|
archive.name: hashlib.sha256(archive.read_bytes()).hexdigest(),
|
|
manifest.name: hashlib.sha256(manifest.read_bytes()).hexdigest(),
|
|
}
|
|
actual = {}
|
|
for line in checksums.read_text().splitlines():
|
|
digest, name = line.split(maxsplit=1)
|
|
actual[name] = digest
|
|
self.assertEqual(actual, expected)
|
|
|
|
def test_release_script_supports_gpg_signing(self):
|
|
script = _read("packaging/release-artifacts.sh")
|
|
self.assertIn("RELEASE_SIGN", script)
|
|
self.assertIn("RELEASE_SIGNING_KEY", script)
|
|
self.assertIn("--armor --detach-sign", script)
|
|
self.assertIn("SHA256SUMS.asc", script)
|
|
|
|
|
|
class TestSourceInstallPackaging(unittest.TestCase):
|
|
def test_makefile_installs_docs_and_tmpfiles(self):
|
|
makefile = _read("Makefile")
|
|
self.assertIn("TMPFILESDIR := /etc/tmpfiles.d", makefile)
|
|
self.assertIn("packaging/enodia-sentinel.tmpfiles", makefile)
|
|
self.assertIn("docs/PACKAGING.md", makefile)
|
|
self.assertIn("docs/VERSION.json", makefile)
|
|
self.assertIn("systemd/enodia-sentinel-ebpf.conf", makefile)
|
|
|
|
def test_packaging_guide_documents_distro_paths(self):
|
|
guide = _read("docs/PACKAGING.md")
|
|
for phrase in (
|
|
"Arch is the primary packaged target",
|
|
"sudo pacman -U enodia-sentinel-*.pkg.tar.zst",
|
|
"There is no native `.deb` package yet",
|
|
"There is no native RPM package yet",
|
|
"Package-manager hooks use absolute paths",
|
|
):
|
|
self.assertIn(phrase, guide)
|
|
|
|
|
|
class TestTrayDesktopEntry(unittest.TestCase):
|
|
def test_desktop_file_launches_tray_entry_point(self):
|
|
text = _read("packaging/enodia-sentinel-tray.desktop")
|
|
self.assertIn("[Desktop Entry]", text)
|
|
self.assertIn("Type=Application", text)
|
|
self.assertIn("Exec=enodia-sentinel-tray", text)
|
|
self.assertRegex(text, r"(?m)^Name=.*Sentinel")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|