211 lines
8.4 KiB
Python
211 lines
8.4 KiB
Python
import os
|
||
import sys
|
||
import time
|
||
import sqlite3
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import Optional, Dict, Any, List, Tuple
|
||
from datetime import datetime
|
||
|
||
logger = logging.getLogger("AGYRemoteAudit")
|
||
|
||
DATA_DIR = Path("/root/telegram-agy-bot/data")
|
||
DB_PATH = DATA_DIR / "memory.db"
|
||
|
||
|
||
class RemoteAuditLogger:
|
||
"""Manages audit logging for SSH, FTP, and remote operations into SQLite."""
|
||
|
||
def __init__(self, db_path: Path = DB_PATH):
|
||
self.db_path = db_path
|
||
self._init_db()
|
||
|
||
def _get_connection(self) -> sqlite3.Connection:
|
||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||
conn = sqlite3.connect(str(self.db_path), timeout=15.0)
|
||
conn.row_factory = sqlite3.Row
|
||
conn.execute("PRAGMA journal_mode=WAL;")
|
||
conn.execute("PRAGMA synchronous=NORMAL;")
|
||
return conn
|
||
|
||
def _init_db(self):
|
||
"""Initializes remote_audit_logs table and indexes."""
|
||
try:
|
||
with self._get_connection() as conn:
|
||
conn.execute("""
|
||
CREATE TABLE IF NOT EXISTS remote_audit_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
timestamp REAL NOT NULL,
|
||
created_at TEXT NOT NULL,
|
||
user_id INTEGER NOT NULL,
|
||
project_name TEXT NOT NULL,
|
||
service_type TEXT NOT NULL, -- 'ssh', 'ftp'
|
||
action_type TEXT NOT NULL, -- 'push', 'pull', 'exec', 'test_connection', 'update_config'
|
||
requester TEXT NOT NULL, -- 'ai_agent', 'user_button', 'user_command'
|
||
details TEXT, -- command or parameters (never contains passwords)
|
||
status TEXT NOT NULL, -- 'success', 'failed', 'running'
|
||
result_summary TEXT, -- short summary or error message
|
||
duration_ms REAL DEFAULT 0.0
|
||
);
|
||
""")
|
||
conn.execute("""
|
||
CREATE INDEX IF NOT EXISTS idx_remote_audit_lookup
|
||
ON remote_audit_logs(user_id, project_name, service_type, timestamp DESC);
|
||
""")
|
||
conn.commit()
|
||
except Exception as e:
|
||
logger.error(f"Error initializing remote_audit_logs table: {e}")
|
||
|
||
def log_operation(
|
||
self,
|
||
user_id: int,
|
||
project_name: str,
|
||
service_type: str,
|
||
action_type: str,
|
||
requester: str,
|
||
status: str,
|
||
details: Optional[str] = None,
|
||
result_summary: Optional[str] = None,
|
||
duration_ms: float = 0.0,
|
||
timestamp: Optional[float] = None,
|
||
) -> int:
|
||
"""Logs a remote operation into the audit database."""
|
||
now_ts = timestamp or time.time()
|
||
dt_str = datetime.fromtimestamp(now_ts).strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
# Sanitize details (ensure no raw passwords accidentally entered)
|
||
safe_details = details or ""
|
||
if "password" in safe_details.lower():
|
||
safe_details = "[FILTERED DETAILS]"
|
||
|
||
try:
|
||
with self._get_connection() as conn:
|
||
cur = conn.execute(
|
||
"""
|
||
INSERT INTO remote_audit_logs (
|
||
timestamp, created_at, user_id, project_name,
|
||
service_type, action_type, requester, details,
|
||
status, result_summary, duration_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
now_ts,
|
||
dt_str,
|
||
int(user_id),
|
||
str(project_name),
|
||
str(service_type).lower(),
|
||
str(action_type).lower(),
|
||
str(requester).lower(),
|
||
safe_details[:1000] if safe_details else None,
|
||
str(status).lower(),
|
||
str(result_summary)[:1000] if result_summary else None,
|
||
float(duration_ms),
|
||
),
|
||
)
|
||
conn.commit()
|
||
return cur.lastrowid
|
||
except Exception as e:
|
||
logger.error(f"Error writing to remote_audit_logs: {e}")
|
||
return 0
|
||
|
||
def get_recent_logs(
|
||
self,
|
||
user_id: Optional[int] = None,
|
||
project_name: Optional[str] = None,
|
||
service_type: Optional[str] = None,
|
||
limit: int = 15,
|
||
) -> List[Dict[str, Any]]:
|
||
"""Retrieves recent audit log records."""
|
||
try:
|
||
with self._get_connection() as conn:
|
||
query = "SELECT * FROM remote_audit_logs WHERE 1=1"
|
||
params: List[Any] = []
|
||
if user_id is not None:
|
||
query += " AND user_id = ?"
|
||
params.append(user_id)
|
||
if project_name:
|
||
query += " AND project_name = ?"
|
||
params.append(project_name)
|
||
if service_type:
|
||
query += " AND service_type = ?"
|
||
params.append(service_type)
|
||
|
||
query += " ORDER BY timestamp DESC LIMIT ?"
|
||
params.append(int(limit))
|
||
|
||
cur = conn.execute(query, params)
|
||
rows = cur.fetchall()
|
||
return [dict(r) for r in rows]
|
||
except Exception as e:
|
||
logger.error(f"Error fetching remote_audit_logs: {e}")
|
||
return []
|
||
|
||
def format_logs_for_tg(
|
||
self,
|
||
logs: List[Dict[str, Any]],
|
||
project_name: str,
|
||
is_fa: bool = True,
|
||
) -> str:
|
||
"""Formats audit logs into a clean Telegram HTML message."""
|
||
if not logs:
|
||
if is_fa:
|
||
return (
|
||
f"📜 <b>تاریخچه و لاگ عملیات ریموت</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{project_name}</code>\n\n"
|
||
f"ℹ️ <i>هنوز هیچ عملیات SSH یا FTP برای این پروژه ثبت نشده است.</i>"
|
||
)
|
||
else:
|
||
return (
|
||
f"📜 <b>Remote Operations Audit Log</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{project_name}</code>\n\n"
|
||
f"ℹ️ <i>No SSH or FTP operations logged for this project yet.</i>"
|
||
)
|
||
|
||
lines = []
|
||
if is_fa:
|
||
lines.append(f"📜 <b>تاریخچه عملیات ریموت و SSH/FTP</b>")
|
||
lines.append(f"• 📁 <b>پروژه:</b> <code>{project_name}</code>")
|
||
lines.append(f"• 📊 <b>تعداد لاگهای اخیر:</b> <code>{len(logs)}</code>\n")
|
||
else:
|
||
lines.append(f"📜 <b>Remote Operations & SSH/FTP Audit Log</b>")
|
||
lines.append(f"• 📁 <b>Project:</b> <code>{project_name}</code>")
|
||
lines.append(f"• 📊 <b>Recent records:</b> <code>{len(logs)}</code>\n")
|
||
|
||
for idx, log in enumerate(logs, 1):
|
||
st = log.get("status", "unknown")
|
||
st_icon = "✅" if st == "success" else ("❌" if st == "failed" else "⏳")
|
||
srv = log.get("service_type", "").upper()
|
||
act = log.get("action_type", "")
|
||
req = log.get("requester", "")
|
||
req_label = "🤖 AI" if "ai" in req else ("👤 کاربر" if is_fa else "👤 User")
|
||
dt = log.get("created_at", "")
|
||
dur = log.get("duration_ms", 0.0)
|
||
dur_str = f" ({dur:.1f}ms)" if dur > 0 else ""
|
||
res = log.get("result_summary") or ""
|
||
det = log.get("details") or ""
|
||
|
||
act_fa = {
|
||
"push": "پوش به پروداکشن 🚀",
|
||
"pull": "دریافت از سرور 📥",
|
||
"exec": "اجرای فرمان 💻",
|
||
"test_connection": "تست اتصال 🔍",
|
||
"update_config": "ویرایش تنظیمات ⚙️",
|
||
}.get(act, act)
|
||
|
||
act_display = act_fa if is_fa else act.capitalize()
|
||
|
||
item_text = f"<b>{idx}. {st_icon} [{srv}] {act_display}</b> | {req_label}\n"
|
||
item_text += f" 🕒 <code>{dt}</code>{dur_str}\n"
|
||
if det:
|
||
item_text += f" 📝 <code>{det[:60]}</code>\n"
|
||
if res:
|
||
item_text += f" 💬 <i>{res[:100]}</i>\n"
|
||
|
||
lines.append(item_text)
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
# Global singleton instance
|
||
remote_audit = RemoteAuditLogger()
|