chore(security): enhance environment configuration, CI workflows, and wallet daemon with security improvements

- Restructure .env.example with security-focused documentation, service-specific environment file references, and AWS Secrets Manager integration
- Update CLI tests workflow to single Python 3.13 version, add pytest-mock dependency, and consolidate test execution with coverage
- Add comprehensive security validation to package publishing workflow with manual approval gates, secret scanning, and release
This commit is contained in:
oib
2026-03-03 10:33:46 +01:00
parent 00d00cb964
commit f353e00172
220 changed files with 42506 additions and 921 deletions

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends
from .deps import get_receipt_service, get_keystore, get_ledger
from .models import ReceiptVerificationModel, from_validation_result
from .keystore.service import KeystoreService
from .keystore.persistent_service import PersistentKeystoreService
from .ledger_mock import SQLiteLedgerAdapter
from .receipts.service import ReceiptVerifierService

View File

@@ -23,7 +23,7 @@ from .models import (
WalletDescriptor,
from_validation_result,
)
from .keystore.service import KeystoreService
from .keystore.persistent_service import PersistentKeystoreService
from .ledger_mock import SQLiteLedgerAdapter
from .receipts.service import ReceiptValidationResult, ReceiptVerifierService
from .security import RateLimiter, wipe_buffer
@@ -85,7 +85,7 @@ def verify_receipt_history(
@router.get("/wallets", response_model=WalletListResponse, summary="List wallets")
def list_wallets(
keystore: KeystoreService = Depends(get_keystore),
keystore: PersistentKeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> WalletListResponse:
descriptors = []
@@ -102,7 +102,7 @@ def list_wallets(
def create_wallet(
request: WalletCreateRequest,
http_request: Request,
keystore: KeystoreService = Depends(get_keystore),
keystore: PersistentKeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> WalletCreateResponse:
_enforce_limit("wallet-create", http_request)
@@ -113,11 +113,13 @@ def create_wallet(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid base64 secret") from exc
try:
ip_address = http_request.client.host if http_request.client else "unknown"
record = keystore.create_wallet(
wallet_id=request.wallet_id,
password=request.password,
secret=secret,
metadata=request.metadata,
ip_address=ip_address
)
except ValueError as exc:
raise HTTPException(
@@ -137,16 +139,18 @@ def unlock_wallet(
wallet_id: str,
request: WalletUnlockRequest,
http_request: Request,
keystore: KeystoreService = Depends(get_keystore),
keystore: PersistentKeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> WalletUnlockResponse:
_enforce_limit("wallet-unlock", http_request, wallet_id)
try:
secret = bytearray(keystore.unlock_wallet(wallet_id, request.password))
ledger.record_event(wallet_id, "unlocked", {"success": True})
ip_address = http_request.client.host if http_request.client else "unknown"
secret = bytearray(keystore.unlock_wallet(wallet_id, request.password, ip_address))
ledger.record_event(wallet_id, "unlocked", {"success": True, "ip_address": ip_address})
logger.info("Unlocked wallet", extra={"wallet_id": wallet_id})
except (KeyError, ValueError):
ledger.record_event(wallet_id, "unlocked", {"success": False})
ip_address = http_request.client.host if http_request.client else "unknown"
ledger.record_event(wallet_id, "unlocked", {"success": False, "ip_address": ip_address})
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
finally:
if "secret" in locals():
@@ -160,7 +164,7 @@ def sign_payload(
wallet_id: str,
request: WalletSignRequest,
http_request: Request,
keystore: KeystoreService = Depends(get_keystore),
keystore: PersistentKeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> WalletSignResponse:
_enforce_limit("wallet-sign", http_request, wallet_id)
@@ -170,11 +174,13 @@ def sign_payload(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid base64 message") from exc
try:
signature = keystore.sign_message(wallet_id, request.password, message)
ledger.record_event(wallet_id, "sign", {"success": True})
ip_address = http_request.client.host if http_request.client else "unknown"
signature = keystore.sign_message(wallet_id, request.password, message, ip_address)
ledger.record_event(wallet_id, "sign", {"success": True, "ip_address": ip_address})
logger.debug("Signed payload", extra={"wallet_id": wallet_id})
except (KeyError, ValueError):
ledger.record_event(wallet_id, "sign", {"success": False})
ip_address = http_request.client.host if http_request.client else "unknown"
ledger.record_event(wallet_id, "sign", {"success": False, "ip_address": ip_address})
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
signature_b64 = base64.b64encode(signature).decode()

View File

@@ -6,6 +6,7 @@ from fastapi import Depends
from .keystore.service import KeystoreService
from .ledger_mock import SQLiteLedgerAdapter
from .keystore.persistent_service import PersistentKeystoreService
from .receipts.service import ReceiptVerifierService
from .settings import Settings, settings
@@ -22,8 +23,8 @@ def get_receipt_service(config: Settings = Depends(get_settings)) -> ReceiptVeri
@lru_cache
def get_keystore() -> KeystoreService:
return KeystoreService()
def get_keystore(config: Settings = Depends(get_settings)) -> PersistentKeystoreService:
return PersistentKeystoreService(db_path=config.ledger_db_path.parent / "keystore.db")
def get_ledger(config: Settings = Depends(get_settings)) -> SQLiteLedgerAdapter:

View File

@@ -0,0 +1,396 @@
"""
Persistent Keystore Service - Fixes data loss on restart
Replaces the in-memory-only keystore with database persistence
"""
from __future__ import annotations
import json
import sqlite3
import threading
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, Iterable, List, Optional
from secrets import token_bytes
from nacl.signing import SigningKey
from ..crypto.encryption import EncryptionSuite, EncryptionError
from ..security import validate_password_rules, wipe_buffer
@dataclass
class WalletRecord:
"""Wallet record with database persistence"""
wallet_id: str
public_key: str
salt: bytes
nonce: bytes
ciphertext: bytes
metadata: Dict[str, str]
created_at: str
updated_at: str
class PersistentKeystoreService:
"""Persistent keystore with database storage and proper encryption"""
def __init__(self, db_path: Optional[Path] = None, encryption: Optional[EncryptionSuite] = None) -> None:
self.db_path = db_path or Path("./data/keystore.db")
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._encryption = encryption or EncryptionSuite()
self._lock = threading.Lock()
self._init_database()
def _init_database(self):
"""Initialize database schema"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
conn.execute("""
CREATE TABLE IF NOT EXISTS wallets (
wallet_id TEXT PRIMARY KEY,
public_key TEXT NOT NULL,
salt BLOB NOT NULL,
nonce BLOB NOT NULL,
ciphertext BLOB NOT NULL,
metadata TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS wallet_access_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet_id TEXT NOT NULL,
action TEXT NOT NULL,
timestamp TEXT NOT NULL,
success INTEGER NOT NULL,
ip_address TEXT,
FOREIGN KEY (wallet_id) REFERENCES wallets (wallet_id)
)
""")
# Indexes for performance
conn.execute("CREATE INDEX IF NOT EXISTS idx_wallets_created_at ON wallets(created_at)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_access_log_wallet_id ON wallet_access_log(wallet_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_access_log_timestamp ON wallet_access_log(timestamp)")
conn.commit()
finally:
conn.close()
def list_wallets(self) -> List[str]:
"""List all wallet IDs"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("SELECT wallet_id FROM wallets ORDER BY created_at DESC")
return [row[0] for row in cursor.fetchall()]
finally:
conn.close()
def list_records(self) -> Iterable[WalletRecord]:
"""List all wallet records"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, public_key, salt, nonce, ciphertext, metadata, created_at, updated_at
FROM wallets
ORDER BY created_at DESC
""")
for row in cursor.fetchall():
metadata = json.loads(row[5])
yield WalletRecord(
wallet_id=row[0],
public_key=row[1],
salt=row[2],
nonce=row[3],
ciphertext=row[4],
metadata=metadata,
created_at=row[6],
updated_at=row[7]
)
finally:
conn.close()
def get_wallet(self, wallet_id: str) -> Optional[WalletRecord]:
"""Get wallet record by ID"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, public_key, salt, nonce, ciphertext, metadata, created_at, updated_at
FROM wallets
WHERE wallet_id = ?
""", (wallet_id,))
row = cursor.fetchone()
if row:
metadata = json.loads(row[5])
return WalletRecord(
wallet_id=row[0],
public_key=row[1],
salt=row[2],
nonce=row[3],
ciphertext=row[4],
metadata=metadata,
created_at=row[6],
updated_at=row[7]
)
return None
finally:
conn.close()
def create_wallet(
self,
wallet_id: str,
password: str,
secret: Optional[bytes] = None,
metadata: Optional[Dict[str, str]] = None,
ip_address: Optional[str] = None
) -> WalletRecord:
"""Create a new wallet with database persistence"""
with self._lock:
# Check if wallet already exists
if self.get_wallet(wallet_id):
raise ValueError("wallet already exists")
validate_password_rules(password)
metadata_map = {str(k): str(v) for k, v in (metadata or {}).items()}
if secret is None:
signing_key = SigningKey.generate()
secret_bytes = signing_key.encode()
else:
if len(secret) != SigningKey.seed_size:
raise ValueError("secret key must be 32 bytes")
secret_bytes = secret
signing_key = SigningKey(secret_bytes)
salt = token_bytes(self._encryption.salt_bytes)
nonce = token_bytes(self._encryption.nonce_bytes)
ciphertext = self._encryption.encrypt(password=password, plaintext=secret_bytes, salt=salt, nonce=nonce)
now = datetime.utcnow().isoformat()
conn = sqlite3.connect(self.db_path)
try:
conn.execute("""
INSERT INTO wallets (wallet_id, public_key, salt, nonce, ciphertext, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
wallet_id,
signing_key.verify_key.encode().hex(),
salt,
nonce,
ciphertext,
json.dumps(metadata_map),
now,
now
))
# Log creation
conn.execute("""
INSERT INTO wallet_access_log (wallet_id, action, timestamp, success, ip_address)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, "created", now, 1, ip_address))
conn.commit()
finally:
conn.close()
record = WalletRecord(
wallet_id=wallet_id,
public_key=signing_key.verify_key.encode().hex(),
salt=salt,
nonce=nonce,
ciphertext=ciphertext,
metadata=metadata_map,
created_at=now,
updated_at=now
)
return record
def unlock_wallet(self, wallet_id: str, password: str, ip_address: Optional[str] = None) -> bytes:
"""Unlock wallet and return secret key"""
record = self.get_wallet(wallet_id)
if record is None:
self._log_access(wallet_id, "unlock_failed", False, ip_address)
raise KeyError("wallet not found")
try:
secret = self._encryption.decrypt(password=password, ciphertext=record.ciphertext, salt=record.salt, nonce=record.nonce)
self._log_access(wallet_id, "unlock_success", True, ip_address)
return secret
except EncryptionError as exc:
self._log_access(wallet_id, "unlock_failed", False, ip_address)
raise ValueError("failed to decrypt wallet") from exc
def delete_wallet(self, wallet_id: str) -> bool:
"""Delete a wallet and all its access logs"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Delete access logs first
conn.execute("DELETE FROM wallet_access_log WHERE wallet_id = ?", (wallet_id,))
# Delete wallet
cursor = conn.execute("DELETE FROM wallets WHERE wallet_id = ?", (wallet_id,))
conn.commit()
return cursor.rowcount > 0
finally:
conn.close()
def sign_message(self, wallet_id: str, password: str, message: bytes, ip_address: Optional[str] = None) -> bytes:
"""Sign a message with wallet's private key"""
try:
secret_bytes = bytearray(self.unlock_wallet(wallet_id, password, ip_address))
try:
signing_key = SigningKey(bytes(secret_bytes))
signed = signing_key.sign(message)
self._log_access(wallet_id, "sign_success", True, ip_address)
return signed.signature
finally:
wipe_buffer(secret_bytes)
except (KeyError, ValueError) as exc:
self._log_access(wallet_id, "sign_failed", False, ip_address)
raise
def update_metadata(self, wallet_id: str, metadata: Dict[str, str]) -> bool:
"""Update wallet metadata"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
now = datetime.utcnow().isoformat()
metadata_json = json.dumps(metadata)
cursor = conn.execute("""
UPDATE wallets
SET metadata = ?, updated_at = ?
WHERE wallet_id = ?
""", (metadata_json, now, wallet_id))
conn.commit()
return cursor.rowcount > 0
finally:
conn.close()
def _log_access(self, wallet_id: str, action: str, success: bool, ip_address: Optional[str] = None):
"""Log wallet access for audit trail"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
now = datetime.utcnow().isoformat()
conn.execute("""
INSERT INTO wallet_access_log (wallet_id, action, timestamp, success, ip_address)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, action, now, int(success), ip_address))
conn.commit()
except Exception:
# Don't fail the main operation if logging fails
pass
finally:
conn.close()
def get_access_log(self, wallet_id: str, limit: int = 50) -> List[Dict]:
"""Get access log for a wallet"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT action, timestamp, success, ip_address
FROM wallet_access_log
WHERE wallet_id = ?
ORDER BY timestamp DESC
LIMIT ?
""", (wallet_id, limit))
return [
{
"action": row[0],
"timestamp": row[1],
"success": bool(row[2]),
"ip_address": row[3]
}
for row in cursor.fetchall()
]
finally:
conn.close()
def get_statistics(self) -> Dict[str, Any]:
"""Get keystore statistics"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Wallet count
wallet_count = conn.execute("SELECT COUNT(*) FROM wallets").fetchone()[0]
# Recent activity
recent_creations = conn.execute("""
SELECT COUNT(*) FROM wallets
WHERE created_at > datetime('now', '-24 hours')
""").fetchone()[0]
recent_access = conn.execute("""
SELECT COUNT(*) FROM wallet_access_log
WHERE timestamp > datetime('now', '-24 hours')
""").fetchone()[0]
# Access success rate
total_access = conn.execute("SELECT COUNT(*) FROM wallet_access_log").fetchone()[0]
successful_access = conn.execute("SELECT COUNT(*) FROM wallet_access_log WHERE success = 1").fetchone()[0]
success_rate = (successful_access / total_access * 100) if total_access > 0 else 0
return {
"total_wallets": wallet_count,
"created_last_24h": recent_creations,
"access_last_24h": recent_access,
"access_success_rate": round(success_rate, 2),
"database_path": str(self.db_path)
}
finally:
conn.close()
def backup_keystore(self, backup_path: Path) -> bool:
"""Create a backup of the keystore database"""
try:
with self._lock:
conn = sqlite3.connect(self.db_path)
backup_conn = sqlite3.connect(backup_path)
conn.backup(backup_conn)
conn.close()
backup_conn.close()
return True
except Exception:
return False
def verify_integrity(self) -> Dict[str, Any]:
"""Verify database integrity"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Run integrity check
result = conn.execute("PRAGMA integrity_check").fetchall()
# Check foreign key constraints
fk_check = conn.execute("PRAGMA foreign_key_check").fetchall()
return {
"integrity_check": result,
"foreign_key_check": fk_check,
"is_valid": len(result) == 1 and result[0][0] == "ok"
}
finally:
conn.close()
# Import datetime for the module
from datetime import datetime

View File

@@ -0,0 +1,283 @@
"""
SQLite Ledger Adapter for Wallet Daemon
Production-ready ledger implementation (replacing missing mock)
"""
from __future__ import annotations
import json
import sqlite3
import threading
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
@dataclass
class LedgerRecord:
"""Ledger record for wallet events"""
wallet_id: str
event_type: str
timestamp: datetime
data: Dict[str, Any]
success: bool = True
@dataclass
class WalletMetadata:
"""Wallet metadata stored in ledger"""
wallet_id: str
public_key: str
metadata: Dict[str, str]
created_at: datetime
updated_at: datetime
class SQLiteLedgerAdapter:
"""Production-ready SQLite ledger adapter"""
def __init__(self, db_path: Optional[Path] = None):
self.db_path = db_path or Path("./data/wallet_ledger.db")
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.Lock()
self._init_database()
def _init_database(self):
"""Initialize database schema"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Create wallet metadata table
conn.execute("""
CREATE TABLE IF NOT EXISTS wallet_metadata (
wallet_id TEXT PRIMARY KEY,
public_key TEXT NOT NULL,
metadata TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
# Create events table
conn.execute("""
CREATE TABLE IF NOT EXISTS wallet_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet_id TEXT NOT NULL,
event_type TEXT NOT NULL,
timestamp TEXT NOT NULL,
data TEXT NOT NULL,
success INTEGER NOT NULL,
FOREIGN KEY (wallet_id) REFERENCES wallet_metadata (wallet_id)
)
""")
# Create indexes for performance
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_wallet_id ON wallet_events(wallet_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_timestamp ON wallet_events(timestamp)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_type ON wallet_events(event_type)")
conn.commit()
finally:
conn.close()
def upsert_wallet(self, wallet_id: str, public_key: str, metadata: Dict[str, str]) -> None:
"""Insert or update wallet metadata"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
now = datetime.utcnow().isoformat()
metadata_json = json.dumps(metadata)
# Try update first
cursor = conn.execute("""
UPDATE wallet_metadata
SET public_key = ?, metadata = ?, updated_at = ?
WHERE wallet_id = ?
""", (public_key, metadata_json, now, wallet_id))
# If no rows updated, insert new
if cursor.rowcount == 0:
conn.execute("""
INSERT INTO wallet_metadata (wallet_id, public_key, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, public_key, metadata_json, now, now))
conn.commit()
finally:
conn.close()
def get_wallet(self, wallet_id: str) -> Optional[WalletMetadata]:
"""Get wallet metadata"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, public_key, metadata, created_at, updated_at
FROM wallet_metadata
WHERE wallet_id = ?
""", (wallet_id,))
row = cursor.fetchone()
if row:
metadata = json.loads(row[2])
return WalletMetadata(
wallet_id=row[0],
public_key=row[1],
metadata=metadata,
created_at=datetime.fromisoformat(row[3]),
updated_at=datetime.fromisoformat(row[4])
)
return None
finally:
conn.close()
def record_event(self, wallet_id: str, event_type: str, data: Dict[str, Any]) -> None:
"""Record a wallet event"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
now = datetime.utcnow().isoformat()
data_json = json.dumps(data)
success = data.get("success", True)
conn.execute("""
INSERT INTO wallet_events (wallet_id, event_type, timestamp, data, success)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, event_type, now, data_json, int(success)))
conn.commit()
finally:
conn.close()
def get_wallet_events(self, wallet_id: str, limit: int = 50) -> List[LedgerRecord]:
"""Get events for a wallet"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, event_type, timestamp, data, success
FROM wallet_events
WHERE wallet_id = ?
ORDER BY timestamp DESC
LIMIT ?
""", (wallet_id, limit))
events = []
for row in cursor.fetchall():
data = json.loads(row[3])
events.append(LedgerRecord(
wallet_id=row[0],
event_type=row[1],
timestamp=datetime.fromisoformat(row[2]),
data=data,
success=bool(row[4])
))
return events
finally:
conn.close()
def get_all_wallets(self) -> List[WalletMetadata]:
"""Get all wallets"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, public_key, metadata, created_at, updated_at
FROM wallet_metadata
ORDER BY created_at DESC
""")
wallets = []
for row in cursor.fetchall():
metadata = json.loads(row[2])
wallets.append(WalletMetadata(
wallet_id=row[0],
public_key=row[1],
metadata=metadata,
created_at=datetime.fromisoformat(row[3]),
updated_at=datetime.fromisoformat(row[4])
))
return wallets
finally:
conn.close()
def get_statistics(self) -> Dict[str, Any]:
"""Get ledger statistics"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Wallet count
wallet_count = conn.execute("SELECT COUNT(*) FROM wallet_metadata").fetchone()[0]
# Event counts by type
event_stats = conn.execute("""
SELECT event_type, COUNT(*) as count
FROM wallet_events
GROUP BY event_type
""").fetchall()
# Recent activity
recent_events = conn.execute("""
SELECT COUNT(*) FROM wallet_events
WHERE timestamp > datetime('now', '-24 hours')
""").fetchone()[0]
return {
"total_wallets": wallet_count,
"event_breakdown": dict(event_stats),
"events_last_24h": recent_events,
"database_path": str(self.db_path)
}
finally:
conn.close()
def delete_wallet(self, wallet_id: str) -> bool:
"""Delete a wallet and all its events"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Delete events first (foreign key constraint)
conn.execute("DELETE FROM wallet_events WHERE wallet_id = ?", (wallet_id,))
# Delete wallet metadata
cursor = conn.execute("DELETE FROM wallet_metadata WHERE wallet_id = ?", (wallet_id,))
conn.commit()
return cursor.rowcount > 0
finally:
conn.close()
def backup_ledger(self, backup_path: Path) -> bool:
"""Create a backup of the ledger database"""
try:
with self._lock:
conn = sqlite3.connect(self.db_path)
backup_conn = sqlite3.connect(backup_path)
conn.backup(backup_conn)
conn.close()
backup_conn.close()
return True
except Exception:
return False
def verify_integrity(self) -> Dict[str, Any]:
"""Verify database integrity"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Run integrity check
result = conn.execute("PRAGMA integrity_check").fetchall()
# Check foreign key constraints
fk_check = conn.execute("PRAGMA foreign_key_check").fetchall()
return {
"integrity_check": result,
"foreign_key_check": fk_check,
"is_valid": len(result) == 1 and result[0][0] == "ok"
}
finally:
conn.close()