# SPDX-License-Identifier: GPL-3.0-or-later """Host posture tests — pure evaluators driven by injected content/stat facts, plus a run() integration test with every reader monkeypatched, so nothing here needs root or a live /etc.""" import unittest from enodia_sentinel import posture from enodia_sentinel.alert import Severity from enodia_sentinel.config import Config class TestSshd(unittest.TestCase): def test_resolve_is_first_wins_and_skips_comments(self): eff = posture.resolve_sshd([ "# a comment", "PasswordAuthentication no", "PasswordAuthentication yes", # later value ignored (sshd first-wins) "PermitRootLogin yes # inline comment", ]) self.assertEqual(eff["passwordauthentication"], "no") self.assertEqual(eff["permitrootlogin"], "yes") def test_root_login_yes_is_high(self): finds = list(posture.sshd_findings({"permitrootlogin": "yes"})) sig = {f.signature: f for f in finds} self.assertIn("ssh_root_login", sig) self.assertEqual(sig["ssh_root_login"].severity, Severity.HIGH) def test_prohibit_password_root_login_is_clean(self): finds = [f.signature for f in posture.sshd_findings({"permitrootlogin": "prohibit-password", "passwordauthentication": "no"})] self.assertNotIn("ssh_root_login", finds) def test_password_auth_default_yes_flagged(self): # Unset PasswordAuthentication defaults to yes, so absence is a finding. finds = {f.signature: f for f in posture.sshd_findings({})} self.assertIn("ssh_password_auth", finds) self.assertIn("default", finds["ssh_password_auth"].detail) def test_empty_password_is_critical(self): finds = {f.signature: f for f in posture.sshd_findings({"permitemptypasswords": "yes", "passwordauthentication": "no"})} self.assertEqual(finds["ssh_empty_password"].severity, Severity.CRITICAL) class TestSudoers(unittest.TestCase): def test_nopasswd_flagged(self): finds = list(posture.sudoers_findings([ ("/etc/sudoers", "alice ALL=(ALL) NOPASSWD: ALL\n", 0o440)])) self.assertEqual([f.signature for f in finds], ["sudo_nopasswd"]) self.assertIn("alice", finds[0].detail) def test_commented_nopasswd_ignored(self): finds = list(posture.sudoers_findings([ ("/etc/sudoers", "# alice ALL=(ALL) NOPASSWD: ALL\n", 0o440)])) self.assertEqual(finds, []) def test_writable_sudoers_file_flagged_high(self): finds = {f.signature: f for f in posture.sudoers_findings([ ("/etc/sudoers.d/bad", "root ALL=(ALL) ALL\n", 0o646)])} self.assertEqual(finds["sudoers_writable"].severity, Severity.HIGH) def test_no_authenticate_default_flagged(self): finds = [f.signature for f in posture.sudoers_findings([ ("/etc/sudoers", "Defaults !authenticate\n", 0o440)])] self.assertIn("sudo_no_authenticate", finds) class TestPath(unittest.TestCase): def test_world_writable_path_dir_is_high(self): finds = {f.signature: f for f in posture.path_findings([("/usr/local/bin", 0o40777, 0)])} self.assertEqual(finds["path_world_writable"].severity, Severity.HIGH) def test_nonroot_owner_is_medium(self): finds = {f.signature: f for f in posture.path_findings([("/opt/bin", 0o40755, 1000)])} self.assertEqual(finds["path_nonroot_owner"].severity, Severity.MEDIUM) def test_clean_root_owned_dir_no_finding(self): self.assertEqual(list(posture.path_findings([("/usr/bin", 0o40755, 0)])), []) class TestSensitiveFiles(unittest.TestCase): def test_world_readable_shadow_is_high(self): finds = {f.signature: f for f in posture.sensitive_file_findings([("/etc/shadow", 0o100644)])} self.assertEqual(finds["sensitive_file_readable"].severity, Severity.HIGH) def test_world_writable_passwd_is_critical(self): finds = {f.signature: f for f in posture.sensitive_file_findings([("/etc/passwd", 0o100666)])} self.assertEqual(finds["sensitive_file_writable"].severity, Severity.CRITICAL) def test_normal_perms_clean(self): facts = [("/etc/shadow", 0o100600), ("/etc/passwd", 0o100644)] self.assertEqual(list(posture.sensitive_file_findings(facts)), []) def test_unknown_path_ignored(self): self.assertEqual( list(posture.sensitive_file_findings([("/etc/motd", 0o100666)])), []) class TestRunIntegration(unittest.TestCase): """Drive run() with every reader monkeypatched to known facts.""" def setUp(self): self.cfg = Config() self._saved = {n: getattr(posture, n) for n in ("read_sshd_lines", "read_sudoers", "read_path_dirs", "read_sensitive_files")} import enodia_sentinel.pkgdb as pkgdb self._saved_sig = pkgdb.siglevel_alert pkgdb.siglevel_alert = lambda *a, **k: None def tearDown(self): for n, fn in self._saved.items(): setattr(posture, n, fn) import enodia_sentinel.pkgdb as pkgdb pkgdb.siglevel_alert = self._saved_sig def test_clean_host_yields_nothing(self): posture.read_sshd_lines = lambda p: ["PermitRootLogin no", "PasswordAuthentication no"] posture.read_sudoers = lambda m, d: [("/etc/sudoers", "root ALL=(ALL) ALL\n", 0o440)] posture.read_path_dirs = lambda dirs: [("/usr/bin", 0o40755, 0)] posture.read_sensitive_files = lambda *a, **k: [("/etc/shadow", 0o100600)] self.assertEqual(list(posture.run(self.cfg)), []) def test_messy_host_aggregates_findings(self): posture.read_sshd_lines = lambda p: ["PermitRootLogin yes"] posture.read_sudoers = lambda m, d: [ ("/etc/sudoers", "bob ALL=(ALL) NOPASSWD: ALL\n", 0o440)] posture.read_path_dirs = lambda dirs: [("/usr/local/bin", 0o40777, 0)] posture.read_sensitive_files = lambda *a, **k: [("/etc/shadow", 0o100644)] sigs = {f.signature for f in posture.run(self.cfg)} self.assertSetEqual( sigs, {"ssh_root_login", "ssh_password_auth", "sudo_nopasswd", "path_world_writable", "sensitive_file_readable"}) if __name__ == "__main__": unittest.main()