From 35130e5528a52161630fd1c93572e1b2b7cd911b Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 11 Aug 2026 23:11:36 +0200 Subject: feat(node): audit logging + local admin UI rewrite Add SQLite audit store for legal compliance (LCEN/DSA): logs user IP, actions (handshake, file download/upload/delete, stream, chat), and timestamps. Retention: 1 year, with cleanup method. WebRTC transport now logs all user actions to the audit store with remote IP extraction from the ICE transport. Local web UI rewritten as a proper admin dashboard: - Stats cards (groups, files, peers) - Connected peers table with IP, username, group, state - Group cards with file listings and shared directory info - Audit log page with event/user filtering - Dark theme, responsive, auto-refresh - JSON API: /api/status, /api/groups, /api/peers, /api/audit, /api/config Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-node/tests/test_audit.py | 76 +++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 packages/meshbay-node/tests/test_audit.py (limited to 'packages/meshbay-node/tests') diff --git a/packages/meshbay-node/tests/test_audit.py b/packages/meshbay-node/tests/test_audit.py new file mode 100644 index 0000000..b2ed15e --- /dev/null +++ b/packages/meshbay-node/tests/test_audit.py @@ -0,0 +1,76 @@ +"""Tests for the node audit store (legal compliance IP/action logging).""" + +import time + +import pytest + +from meshbay_node.audit import AuditStore + + +@pytest.fixture +async def audit(tmp_path): + store = AuditStore(db_path=tmp_path / "audit.db") + await store.open() + yield store + await store.close() + + +@pytest.mark.asyncio +async def test_log_and_retrieve(audit): + await audit.log_event( + user_id="u1", event="handshake", ip="1.2.3.4", + username="alice", group_id="g1", detail="test") + await audit.log_event( + user_id="u2", event="file_download", ip="5.6.7.8", + username="bob", group_id="g1", detail="video.mp4") + + entries = await audit.get_entries() + assert len(entries) == 2 + assert entries[0].event == "file_download" + assert entries[0].ip == "5.6.7.8" + assert entries[1].event == "handshake" + + +@pytest.mark.asyncio +async def test_filter_by_event(audit): + await audit.log_event(user_id="u1", event="handshake", ip="1.1.1.1") + await audit.log_event(user_id="u1", event="file_download", ip="1.1.1.1") + await audit.log_event(user_id="u1", event="handshake", ip="1.1.1.1") + + entries = await audit.get_entries(event="handshake") + assert len(entries) == 2 + assert all(e.event == "handshake" for e in entries) + + +@pytest.mark.asyncio +async def test_filter_by_user(audit): + await audit.log_event(user_id="u1", event="handshake") + await audit.log_event(user_id="u2", event="handshake") + + entries = await audit.get_entries(user_id="u1") + assert len(entries) == 1 + assert entries[0].user_id == "u1" + + +@pytest.mark.asyncio +async def test_entry_count(audit): + assert await audit.entry_count() == 0 + await audit.log_event(user_id="u1", event="connect") + await audit.log_event(user_id="u2", event="connect") + assert await audit.entry_count() == 2 + + +@pytest.mark.asyncio +async def test_cleanup_old_entries(audit): + await audit.log_event(user_id="u1", event="old") + # manually backdate + await audit._db.execute( + "UPDATE audit_log SET timestamp = ? WHERE event = 'old'", + (time.time() - 400 * 86400,)) + await audit._db.commit() + + await audit.log_event(user_id="u2", event="recent") + + deleted = await audit.cleanup(retention_days=365) + assert deleted == 1 + assert await audit.entry_count() == 1 -- cgit v1.2.3