inital commit
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
import Database from "bun:sqlite";
|
||||
import { resolve } from "path";
|
||||
import { ensureDataDir } from "./paths";
|
||||
|
||||
const dataDir = ensureDataDir();
|
||||
const dbPath = resolve(dataDir, "overlord.db");
|
||||
const db = new Database(dbPath);
|
||||
|
||||
const auditQueue: AuditLogEntry[] = [];
|
||||
const BATCH_SIZE = 50;
|
||||
const BATCH_INTERVAL_MS = 1000;
|
||||
let flushTimeout: Timer | null = null;
|
||||
|
||||
const VERBOSE_AUDIT = process.env.VERBOSE_AUDIT === "true";
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
target_client_id TEXT,
|
||||
details TEXT,
|
||||
success INTEGER NOT NULL,
|
||||
error_message TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
db.run(
|
||||
`CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_logs(timestamp DESC);`,
|
||||
);
|
||||
db.run(
|
||||
`CREATE INDEX IF NOT EXISTS idx_audit_username ON audit_logs(username);`,
|
||||
);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action);`);
|
||||
|
||||
export enum AuditAction {
|
||||
CLIENT_FIRST_CONNECT = "client_first_connect",
|
||||
CLIENT_RECONNECT = "client_reconnect",
|
||||
CLIENT_DISCONNECT = "client_disconnect",
|
||||
LOGIN = "login",
|
||||
LOGOUT = "logout",
|
||||
LOGIN_FAILED = "login_failed",
|
||||
COMMAND = "command",
|
||||
FILE_DOWNLOAD = "file_download",
|
||||
FILE_UPLOAD = "file_upload",
|
||||
FILE_DELETE = "file_delete",
|
||||
FILE_LIST = "file_list",
|
||||
FILE_MKDIR = "file_mkdir",
|
||||
FILE_READ = "file_read",
|
||||
FILE_WRITE = "file_write",
|
||||
FILE_SEARCH = "file_search",
|
||||
FILE_COPY = "file_copy",
|
||||
FILE_MOVE = "file_move",
|
||||
FILE_CHMOD = "file_chmod",
|
||||
FILE_EXECUTE = "file_execute",
|
||||
FILE_ZIP = "file_zip",
|
||||
CONSOLE_START = "console_start",
|
||||
CONSOLE_STOP = "console_stop",
|
||||
CONSOLE_INPUT = "console_input",
|
||||
DESKTOP_START = "desktop_start",
|
||||
DESKTOP_STOP = "desktop_stop",
|
||||
DESKTOP_MOUSE = "desktop_mouse",
|
||||
DESKTOP_KEYBOARD = "desktop_keyboard",
|
||||
PROCESS_LIST = "process_list",
|
||||
PROCESS_KILL = "process_kill",
|
||||
SCREENSHOT = "screenshot",
|
||||
DISCONNECT = "disconnect",
|
||||
RECONNECT = "reconnect",
|
||||
UNINSTALL = "uninstall",
|
||||
AGENT_UPDATE = "agent_update",
|
||||
SCRIPT_EXECUTE = "script_execute",
|
||||
SILENT_EXECUTE = "silent_execute",
|
||||
}
|
||||
|
||||
export interface AuditLogEntry {
|
||||
id?: number;
|
||||
timestamp: number;
|
||||
username: string;
|
||||
ip: string;
|
||||
action: AuditAction | string;
|
||||
targetClientId?: string;
|
||||
details?: string;
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface AuditLogFilters {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
username?: string;
|
||||
action?: string;
|
||||
actions?: string[];
|
||||
targetClientId?: string;
|
||||
startDate?: number;
|
||||
endDate?: number;
|
||||
successOnly?: boolean;
|
||||
}
|
||||
|
||||
export function logAudit(entry: AuditLogEntry): void {
|
||||
auditQueue.push(entry);
|
||||
|
||||
if (VERBOSE_AUDIT) {
|
||||
const status = entry.success ? "✓" : "✗";
|
||||
const target = entry.targetClientId
|
||||
? ` [client: ${entry.targetClientId.substring(0, 8)}...]`
|
||||
: "";
|
||||
console.log(
|
||||
`[audit] ${status} ${entry.username}@${entry.ip} - ${entry.action}${target}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (auditQueue.length >= BATCH_SIZE) {
|
||||
flushAuditLogs();
|
||||
} else if (!flushTimeout) {
|
||||
flushTimeout = setTimeout(flushAuditLogs, BATCH_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
function flushAuditLogs(): void {
|
||||
if (flushTimeout) {
|
||||
clearTimeout(flushTimeout);
|
||||
flushTimeout = null;
|
||||
}
|
||||
|
||||
if (auditQueue.length === 0) return;
|
||||
|
||||
const logsToWrite = auditQueue.splice(0, auditQueue.length);
|
||||
|
||||
try {
|
||||
const insert = db.prepare(
|
||||
`INSERT INTO audit_logs (timestamp, username, ip, action, target_client_id, details, success, error_message)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
);
|
||||
|
||||
const transaction = db.transaction((logs: AuditLogEntry[]) => {
|
||||
for (const entry of logs) {
|
||||
insert.run(
|
||||
entry.timestamp,
|
||||
entry.username,
|
||||
entry.ip,
|
||||
entry.action,
|
||||
entry.targetClientId || null,
|
||||
entry.details || null,
|
||||
entry.success ? 1 : 0,
|
||||
entry.errorMessage || null,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
transaction(logsToWrite);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[audit] Failed to flush ${logsToWrite.length} audit entries:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function flushAuditLogsSync(): void {
|
||||
flushAuditLogs();
|
||||
}
|
||||
|
||||
export function getAuditLogs(filters: AuditLogFilters = {}): {
|
||||
logs: AuditLogEntry[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
} {
|
||||
const page = filters.page || 1;
|
||||
const pageSize = filters.pageSize || 50;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const where: string[] = [];
|
||||
const params: any[] = [];
|
||||
|
||||
if (filters.username) {
|
||||
where.push("username = ?");
|
||||
params.push(filters.username);
|
||||
}
|
||||
|
||||
if (filters.action) {
|
||||
where.push("action = ?");
|
||||
params.push(filters.action);
|
||||
}
|
||||
|
||||
if (filters.actions && filters.actions.length > 0) {
|
||||
const placeholders = filters.actions.map(() => "?").join(", ");
|
||||
where.push(`action IN (${placeholders})`);
|
||||
params.push(...filters.actions);
|
||||
}
|
||||
|
||||
if (filters.targetClientId) {
|
||||
where.push("target_client_id = ?");
|
||||
params.push(filters.targetClientId);
|
||||
}
|
||||
|
||||
if (filters.startDate) {
|
||||
where.push("timestamp >= ?");
|
||||
params.push(filters.startDate);
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
where.push("timestamp <= ?");
|
||||
params.push(filters.endDate);
|
||||
}
|
||||
|
||||
if (filters.successOnly) {
|
||||
where.push("success = 1");
|
||||
}
|
||||
|
||||
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
||||
|
||||
const countQuery = `SELECT COUNT(*) as total FROM audit_logs ${whereSql}`;
|
||||
const countResult = db.query(countQuery).get(...params) as { total: number };
|
||||
const total = countResult?.total || 0;
|
||||
|
||||
const logsQuery = `
|
||||
SELECT id, timestamp, username, ip, action, target_client_id, details, success, error_message
|
||||
FROM audit_logs
|
||||
${whereSql}
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const logs = db.query(logsQuery).all(...params, pageSize, offset) as any[];
|
||||
|
||||
return {
|
||||
logs: logs.map((row) => ({
|
||||
id: row.id,
|
||||
timestamp: row.timestamp,
|
||||
username: row.username,
|
||||
ip: row.ip,
|
||||
action: row.action,
|
||||
targetClientId: row.target_client_id || undefined,
|
||||
details: row.details || undefined,
|
||||
success: row.success === 1,
|
||||
errorMessage: row.error_message || undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAuditStats(): {
|
||||
totalLogs: number;
|
||||
last24h: number;
|
||||
failedLogins: number;
|
||||
topUsers: Array<{ username: string; count: number }>;
|
||||
topActions: Array<{ action: string; count: number }>;
|
||||
} {
|
||||
const now = Date.now();
|
||||
const yesterday = now - 24 * 60 * 60 * 1000;
|
||||
|
||||
const totalLogs = (
|
||||
db.query("SELECT COUNT(*) as count FROM audit_logs").get() as any
|
||||
).count;
|
||||
const last24h = (
|
||||
db
|
||||
.query("SELECT COUNT(*) as count FROM audit_logs WHERE timestamp >= ?")
|
||||
.get(yesterday) as any
|
||||
).count;
|
||||
const failedLogins = (
|
||||
db
|
||||
.query(
|
||||
"SELECT COUNT(*) as count FROM audit_logs WHERE action = ? AND success = 0",
|
||||
)
|
||||
.get(AuditAction.LOGIN_FAILED) as any
|
||||
).count;
|
||||
|
||||
const topUsers = db
|
||||
.query(
|
||||
`
|
||||
SELECT username, COUNT(*) as count
|
||||
FROM audit_logs
|
||||
WHERE timestamp >= ?
|
||||
GROUP BY username
|
||||
ORDER BY count DESC
|
||||
LIMIT 5
|
||||
`,
|
||||
)
|
||||
.all(yesterday) as Array<{ username: string; count: number }>;
|
||||
|
||||
const topActions = db
|
||||
.query(
|
||||
`
|
||||
SELECT action, COUNT(*) as count
|
||||
FROM audit_logs
|
||||
WHERE timestamp >= ?
|
||||
GROUP BY action
|
||||
ORDER BY count DESC
|
||||
LIMIT 10
|
||||
`,
|
||||
)
|
||||
.all(yesterday) as Array<{ action: string; count: number }>;
|
||||
|
||||
return {
|
||||
totalLogs,
|
||||
last24h,
|
||||
failedLogins,
|
||||
topUsers,
|
||||
topActions,
|
||||
};
|
||||
}
|
||||
|
||||
export function cleanupOldAuditLogs(daysToKeep: number = 90): number {
|
||||
const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000;
|
||||
const result = db.run("DELETE FROM audit_logs WHERE timestamp < ?", [
|
||||
cutoffTime,
|
||||
]);
|
||||
const deleted = result.changes;
|
||||
|
||||
if (deleted > 0) {
|
||||
console.log(
|
||||
`[audit] Cleaned up ${deleted} audit logs older than ${daysToKeep} days`,
|
||||
);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
process.on("beforeExit", () => {
|
||||
flushAuditLogs();
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
flushAuditLogs();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
flushAuditLogs();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
console.log("[audit] Audit logging system initialized");
|
||||
Reference in New Issue
Block a user