Files
nousresearch--hermes-agent/tests/hermes_cli/test_security_audit_startup.py
T
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

164 lines
6.4 KiB
Python

"""Tests for the startup security posture audit (hermes_cli.security_audit_startup)."""
from __future__ import annotations
import os
from pathlib import Path
import pytest
import hermes_cli.security_audit_startup as audit
@pytest.fixture(autouse=True)
def _reset_audit_sentinel():
audit._AUDIT_RAN = False
yield
audit._AUDIT_RAN = False
# ── root check ────────────────────────────────────────────────────────────
def test_root_check_flags_uid_zero(monkeypatch):
monkeypatch.setattr(audit, "_is_root", lambda: True)
msg = audit._running_as_root()
assert msg and "ROOT" in msg
def test_root_check_silent_for_non_root(monkeypatch):
monkeypatch.setattr(audit, "_is_root", lambda: False)
assert audit._running_as_root() is None
# ── SSH password-auth check ─────────────────────────────────────────────────
def test_ssh_password_auth_enabled_explicit_yes(monkeypatch):
monkeypatch.setattr(
audit, "_iter_sshd_config_lines",
lambda: ["PasswordAuthentication yes", "PermitRootLogin no"],
)
msg = audit._ssh_password_auth_enabled()
assert msg and "password authentication is enabled" in msg.lower()
def test_ssh_password_auth_disabled(monkeypatch):
monkeypatch.setattr(
audit, "_iter_sshd_config_lines",
lambda: ["PasswordAuthentication no"],
)
assert audit._ssh_password_auth_enabled() is None
def test_ssh_password_auth_default_is_yes(monkeypatch):
"""No explicit directive → sshd default is 'yes' → warn (with qualifier)."""
monkeypatch.setattr(
audit, "_iter_sshd_config_lines",
lambda: ["PermitRootLogin prohibit-password"],
)
msg = audit._ssh_password_auth_enabled()
assert msg and "default" in msg.lower()
def test_ssh_check_silent_when_no_config(monkeypatch):
"""No sshd config readable (e.g. Windows / SSH not installed) → no finding."""
monkeypatch.setattr(audit, "_iter_sshd_config_lines", lambda: [])
assert audit._ssh_password_auth_enabled() is None
def test_ssh_last_directive_wins(monkeypatch):
monkeypatch.setattr(
audit, "_iter_sshd_config_lines",
lambda: ["PasswordAuthentication yes", "PasswordAuthentication no"],
)
assert audit._ssh_password_auth_enabled() is None
# ── container / volume-mount check ──────────────────────────────────────────
def test_container_no_mount_flags(monkeypatch, tmp_path):
monkeypatch.setattr(audit, "_in_container", lambda: True)
monkeypatch.setattr(audit, "_path_is_mounted", lambda p: False)
msg = audit._container_no_volume_mount(tmp_path / ".hermes")
assert msg and "persistent volume" in msg
def test_container_with_mount_silent(monkeypatch, tmp_path):
monkeypatch.setattr(audit, "_in_container", lambda: True)
monkeypatch.setattr(audit, "_path_is_mounted", lambda p: True)
assert audit._container_no_volume_mount(tmp_path / ".hermes") is None
def test_not_in_container_silent(monkeypatch, tmp_path):
monkeypatch.setattr(audit, "_in_container", lambda: False)
assert audit._container_no_volume_mount(tmp_path / ".hermes") is None
# ── network listener without auth ──────────────────────────────────────────
def test_api_server_network_no_key_flags(monkeypatch):
monkeypatch.delenv("API_SERVER_KEY", raising=False)
cfg = {"platforms": {"api_server": {"enabled": True, "extra": {"host": "0.0.0.0", "key": ""}}}}
findings = audit._network_listener_without_auth(cfg)
assert any("NO API_SERVER_KEY" in f for f in findings)
def test_api_server_loopback_silent(monkeypatch):
cfg = {"platforms": {"api_server": {"enabled": True, "extra": {"host": "127.0.0.1", "key": ""}}}}
assert audit._network_listener_without_auth(cfg) == []
def test_api_server_with_key_silent(monkeypatch):
cfg = {"platforms": {"api_server": {"enabled": True, "extra": {"host": "0.0.0.0", "key": "a-strong-key-1234567890"}}}}
assert audit._network_listener_without_auth(cfg) == []
# ── orchestration + logging ─────────────────────────────────────────────────
def test_run_security_audit_aggregates(monkeypatch, tmp_path):
monkeypatch.setattr(audit, "_is_root", lambda: True)
monkeypatch.setattr(audit, "_iter_sshd_config_lines", lambda: ["PasswordAuthentication yes"])
monkeypatch.setattr(audit, "_in_container", lambda: False)
findings = audit.run_security_audit(hermes_home=tmp_path, config={})
assert len(findings) == 2 # root + ssh
def test_run_security_audit_clean_posture(monkeypatch, tmp_path):
monkeypatch.setattr(audit, "_is_root", lambda: False)
monkeypatch.setattr(audit, "_iter_sshd_config_lines", lambda: ["PasswordAuthentication no"])
monkeypatch.setattr(audit, "_in_container", lambda: False)
assert audit.run_security_audit(hermes_home=tmp_path, config={}) == []
def test_log_startup_security_warnings_emits_and_is_idempotent(monkeypatch, tmp_path, caplog):
import logging
monkeypatch.setattr(audit, "_is_root", lambda: True)
monkeypatch.setattr(audit, "_iter_sshd_config_lines", lambda: [])
monkeypatch.setattr(audit, "_in_container", lambda: False)
with caplog.at_level(logging.WARNING, logger="hermes.security_audit"):
first = audit.log_startup_security_warnings(hermes_home=tmp_path, config={})
assert len(first) == 1
assert any("ROOT" in r.message for r in caplog.records)
# Second call is a no-op (idempotent within a process) unless forced.
second = audit.log_startup_security_warnings(hermes_home=tmp_path, config={})
assert second == []
forced = audit.log_startup_security_warnings(hermes_home=tmp_path, config={}, force=True)
assert len(forced) == 1
def test_audit_never_raises_on_broken_check(monkeypatch, tmp_path):
def _boom():
raise RuntimeError("boom")
monkeypatch.setattr(audit, "_is_root", _boom)
# Must not propagate — the broken check is swallowed, others still run.
findings = audit.run_security_audit(hermes_home=tmp_path, config={})
assert isinstance(findings, list)