feat(warden): WARDEN-WP-0003 — test coverage, permissions, status --state-dir

- File permissions: os.chmod(cert, 0o600) after every sign in LocalCA and
  VaultCA; chmod(privkey, 0o600) and chmod(pubkey, 0o644) after generate_keypair
- Scorecard: add check_file_permissions() that flags world/group-readable
  cert and key files; run_scorecard now returns 6 checks
- warden status --state-dir: bypasses config loading entirely for operators
  who have a cert but no warden.yaml installed
- tests/test_vault.py: 11 VaultCA unit tests covering success, HTTP 403,
  RequestError, missing token, missing role, missing pubkey, TTL enforcement,
  eviction, signatures log, and cert mode 600
- tests/test_ca.py: generate_keypair tests (paths, args, overwrite, error,
  permissions) and cert mode 600 assertion after sign
- tests/test_scorecard.py: file_permissions check tests (pass, fail cert,
  fail keys dir); scorecard count updated to 6
- tests/test_cli.py: covers sign, issue, status, scorecard, inventory, log,
  cleanup commands using CliRunner and tmp config/inventory files
- tests/test_integration.py: @pytest.mark.integration tests against real
  ssh-keygen; excluded from default suite via pyproject addopts
- pyproject.toml: addopts = "-m 'not integration'", integration marker declared

All 100 unit tests pass; 3 integration tests pass; ruff clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 17:05:38 +02:00
parent cd1e385bc1
commit f3547acd0b
11 changed files with 974 additions and 55 deletions

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timezone
from warden.scorecard import (
check_actor_name_prefixes,
check_all_actors_have_principals,
check_file_permissions,
check_no_stale_certs,
check_no_expired_certs,
check_ttl_policy,
@@ -100,7 +101,7 @@ def test_run_scorecard_clean(tmp_path):
)
results = run_scorecard(tmp_path, inv)
assert all(r.passed for r in results)
assert len(results) == 5
assert len(results) == 6
# ---------------------------------------------------------------------------
@@ -160,6 +161,52 @@ def test_ttl_policy_skips_unknown_actor(tmp_path):
assert result.passed # unknown actor skipped, not a violation
# ---------------------------------------------------------------------------
# check_file_permissions
# ---------------------------------------------------------------------------
def test_file_permissions_no_state_dir():
result = check_file_permissions(Path("/nonexistent/state/dir"))
assert result.passed
def test_file_permissions_empty_dir(tmp_path):
result = check_file_permissions(tmp_path)
assert result.passed
def test_file_permissions_pass(tmp_path):
cert = tmp_path / "agt-bridge-cert.pub"
cert.write_text("fake")
cert.chmod(0o600)
result = check_file_permissions(tmp_path)
assert result.passed
def test_file_permissions_fail_world_readable(tmp_path):
cert = tmp_path / "agt-bridge-cert.pub"
cert.write_text("fake")
cert.chmod(0o644)
result = check_file_permissions(tmp_path)
assert not result.passed
assert "agt-bridge-cert.pub" in result.detail
def test_file_permissions_keys_dir(tmp_path):
keys_dir = tmp_path / "keys"
keys_dir.mkdir()
key = keys_dir / "agt-test_ed25519"
key.write_text("fake key")
key.chmod(0o644)
result = check_file_permissions(tmp_path)
assert not result.passed
assert "keys/agt-test_ed25519" in result.detail
# ---------------------------------------------------------------------------
# (continuation)
# ---------------------------------------------------------------------------
def test_stale_certs_detail_suggests_cleanup(tmp_path):
cert_path = tmp_path / "agt-bridge-cert.pub"
cert_path.write_text("fake")