From 1a1ceb2cb18a5031ec2b55793dbbc8b58daadab9 Mon Sep 17 00:00:00 2001 From: Jason Staack Date: Thu, 12 Mar 2026 23:44:00 -0500 Subject: [PATCH] feat(10-01): add audit event logging to config backup operations - config_snapshot_created event after successful snapshot INSERT - config_snapshot_skipped_duplicate event on dedup match - config_diff_generated event after diff INSERT - config_backup_manual_trigger event on manual trigger success - All log_action calls wrapped in try/except for safety Co-Authored-By: Claude Opus 4.6 --- .planning/ROADMAP.md | 4 +-- backend/app/routers/config_backups.py | 17 +++++++++++ backend/app/services/config_diff_service.py | 21 ++++++++++++++ .../services/config_snapshot_subscriber.py | 28 +++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index d37c40d..8f8e072 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -161,10 +161,10 @@ Plans: 2. `config_snapshot_skipped_duplicate` audit event logged when a duplicate snapshot is detected 3. `config_diff_generated` audit event logged when a diff is created between snapshots 4. `config_backup_manual_trigger` audit event logged when an operator triggers a manual backup -**Plans**: TBD +**Plans**: 1 plan Plans: -- [ ] 10-01: Audit event emission for all config backup operations +- [ ] 10-01-PLAN.md — Audit event emission for all config backup operations ## Progress diff --git a/backend/app/routers/config_backups.py b/backend/app/routers/config_backups.py index 19a134e..682417e 100644 --- a/backend/app/routers/config_backups.py +++ b/backend/app/routers/config_backups.py @@ -837,6 +837,23 @@ async def trigger_config_snapshot( reply_data = json.loads(reply.data) if reply_data.get("status") == "success": + try: + from app.services.audit_service import log_action + await log_action( + db, + tenant_id, + current_user.user_id, + "config_backup_manual_trigger", + resource_type="config_snapshot", + device_id=device_id, + details={ + "sha256_hash": reply_data.get("sha256_hash"), + "triggered_by": str(current_user.user_id), + }, + ip_address=request.client.host if request.client else None, + ) + except Exception: + pass return { "status": "success", "sha256_hash": reply_data.get("sha256_hash"), diff --git a/backend/app/services/config_diff_service.py b/backend/app/services/config_diff_service.py index 54fb96d..336a4a5 100644 --- a/backend/app/services/config_diff_service.py +++ b/backend/app/services/config_diff_service.py @@ -162,6 +162,27 @@ async def generate_and_store_diff( lines_removed, ) + try: + from app.services.audit_service import log_action + import uuid as _uuid + await log_action( + db=None, + tenant_id=_uuid.UUID(tenant_id), + user_id=None, + action="config_diff_generated", + resource_type="config_diff", + resource_id=str(diff_id), + device_id=_uuid.UUID(device_id), + details={ + "old_snapshot_id": str(old_snapshot_id), + "new_snapshot_id": new_snapshot_id, + "lines_added": lines_added, + "lines_removed": lines_removed, + }, + ) + except Exception: + pass + # 11. Parse structured changes (best-effort) try: changes = parse_diff_changes(diff_text) diff --git a/backend/app/services/config_snapshot_subscriber.py b/backend/app/services/config_snapshot_subscriber.py index b7ca23d..63de766 100644 --- a/backend/app/services/config_snapshot_subscriber.py +++ b/backend/app/services/config_snapshot_subscriber.py @@ -11,6 +11,7 @@ import asyncio import json import logging import time +import uuid as _uuid from datetime import datetime, timezone from typing import Any, Optional @@ -20,6 +21,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError from app.config import settings from app.database import AdminAsyncSessionLocal +from app.services.audit_service import log_action from app.services.config_diff_service import generate_and_store_diff from app.services.openbao_service import OpenBaoTransitService @@ -111,6 +113,18 @@ async def handle_config_snapshot(msg) -> None: device_id, ) config_snapshot_dedup_skipped_total.inc() + try: + await log_action( + db=None, + tenant_id=_uuid.UUID(tenant_id), + user_id=None, + action="config_snapshot_skipped_duplicate", + resource_type="config_snapshot", + device_id=_uuid.UUID(device_id), + details={"sha256_hash": sha256_hash}, + ) + except Exception: + pass await msg.ack() return @@ -173,6 +187,20 @@ async def handle_config_snapshot(msg) -> None: await msg.nak() return + try: + await log_action( + db=None, + tenant_id=_uuid.UUID(tenant_id), + user_id=None, + action="config_snapshot_created", + resource_type="config_snapshot", + resource_id=str(new_snapshot_id), + device_id=_uuid.UUID(device_id), + details={"sha256_hash": sha256_hash}, + ) + except Exception: + pass + # --- Diff generation (best-effort) --- try: await generate_and_store_diff(device_id, tenant_id, str(new_snapshot_id), session)