feat(firewall): persist firewall snapshots to SQLite (F001/D007)

save_state_report now also writes the address-list + firewall-rule snapshot into
the app.db firewall_snapshots tables, making the audit trail independent of git.
Removes the 'git-visible data/mikrotik' framing.
This commit is contained in:
zwitschi committed 2026-10-24 19:50:43 +02:00
1 parent aef132fe94
commit 4960eef312
1 file changed
+31 -7
+31 -7
View File
@@ -143,9 +143,11 @@ class MikrotikFirewall:
"""Save current address lists and firewall rules for auditing.
Writes structured snapshots to ``<report_dir>/mikrotik/data/`` and to
the common ``data/mikrotik/`` directory (overwritten every run so git
can diff changes to the firewall state over time), plus a timestamped
JSON report under ``<report_dir>/mikrotik/``.
the common ``data/mikrotik/`` directory (scratch mirror for debugging;
**not** a git audit trail), plus a timestamped JSON report under
``<report_dir>/mikrotik/``. The authoritative long-term audit trail is
the SQLite ``firewall_snapshots`` tables (see ``app.db``), which are
appended here regardless of the file writes.
"""
state = {
"router": self.router_ip,
@@ -174,11 +176,31 @@ class MikrotikFirewall:
os.path.join(data_dir, f"firewall_{version}.json"),
state["firewall_rules"][version])
# Persist a SQLite snapshot (authoritative audit trail, independent of
# git). Best-effort: never breaks a router sync if the DB is down.
self._persist_snapshot_to_db(state)
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
report_path = os.path.join(report_dir, "mikrotik", f"{timestamp}.json")
self._write_json(report_path, state)
return report_path
@staticmethod
def _persist_snapshot_to_db(state: dict) -> None:
"""Write the firewall snapshot into the SQLite store (best-effort)."""
try:
from .db import get_connection
from .db import repositories as repo
repo.save_firewall_snapshot(
get_connection(),
address_lists_v4=state["address_lists"]["ipv4"],
address_lists_v6=state["address_lists"]["ipv6"],
rules_v4=state["firewall_rules"]["ipv4"],
rules_v6=state["firewall_rules"]["ipv6"],
)
except Exception as exc:
print(f"WARN: could not persist firewall snapshot to db ({exc})")
def sync_blocklist(self, blocked_ipv4: Iterable[str],
blocked_ipv6: Iterable[str],
dry_run: bool = False,
@@ -186,10 +208,12 @@ class MikrotikFirewall:
"""Synchronize the Blocked list cumulatively and audit the result.
The blocklist is grow-only: targets are added when present in the
complete set (all git history of the blocklist files + the current
working files + this scan's targets) and never removed automatically.
Only router entries absent from the complete set — i.e. never blocked
by the system — are pruned. See ``app.blocklist_history``.
complete cumulative set (git history of the blocklist files + the
current working files + this scan's targets) and never removed
automatically. Only router entries absent from the complete set — i.e.
never blocked by the system — are pruned. The history now lives in the
SQLite blocklist ledger (``app.db`` ``blocklist_entries``); git history
remains a read-only fallback. See ``app.blocklist_history``.
"""
desired_ipv4 = set(blocked_ipv4)