mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 18:56:53 +00:00
feat(audit-log): signal rail, day-banded stream, anomaly detection (#682)
Add a Stream view to the Audit Log that leads with a four-tile signal rail (events, actors, failure rate with inline sparkline, peak hour) and presents the feed grouped by day with severity dots, relative times, and inline anomaly callouts. The existing Table view is preserved behind a toggle for power users. Anomaly flags are computed at read time against strictly prior history and returned on demand via ?with_anomalies=1: - unusual_hour: hour outside the actor's central 7-day window - new_ip: IP unseen for this actor in the last 30 days - first_seen_actor: no prior history in the 30-day window New /audit-log/stats endpoint returns the signal-rail aggregates over 24h/7d/30d windows; stats are derived from a single 30-day scan.
This commit is contained in:
+35
-1
@@ -27,6 +27,7 @@ import { AutoHealService } from './services/AutoHealService';
|
||||
import { DockerEventManager } from './services/DockerEventManager';
|
||||
import { ImageUpdateService } from './services/ImageUpdateService';
|
||||
import { UpdatePreviewService } from './services/UpdatePreviewService';
|
||||
import { annotateEntries, computeAuditStats, HISTORY_WINDOW_MS } from './services/AuditAnomalyService';
|
||||
import { templateService } from './services/TemplateService';
|
||||
import { ErrorParser } from './utils/ErrorParser';
|
||||
import { NodeRegistry } from './services/NodeRegistry';
|
||||
@@ -6238,11 +6239,25 @@ app.get('/api/audit-log', async (req: Request, res: Response): Promise<void> =>
|
||||
const search = req.query.search as string | undefined;
|
||||
const from = req.query.from ? parseInt(req.query.from as string) : undefined;
|
||||
const to = req.query.to ? parseInt(req.query.to as string) : undefined;
|
||||
const withAnomalies = req.query.with_anomalies === '1';
|
||||
|
||||
if (isDebugEnabled()) {
|
||||
console.log(`[Audit:diag] Query: page=${page} limit=${limit} username=${username || '-'} method=${method || '-'} search=${search || '-'}`);
|
||||
}
|
||||
const result = DatabaseService.getInstance().getAuditLogs({ page, limit, username, method, from, to, search });
|
||||
const db = DatabaseService.getInstance();
|
||||
const result = db.getAuditLogs({ page, limit, username, method, from, to, search });
|
||||
|
||||
if (withAnomalies && result.entries.length > 0) {
|
||||
const now = Date.now();
|
||||
const historyFrom = now - HISTORY_WINDOW_MS;
|
||||
const oldestInPage = result.entries.reduce(
|
||||
(min, e) => Math.min(min, e.timestamp),
|
||||
result.entries[0].timestamp
|
||||
);
|
||||
const history = db.getAuditLogsInRange(historyFrom, oldestInPage);
|
||||
res.json({ ...result, entries: annotateEntries(result.entries, history, now) });
|
||||
return;
|
||||
}
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('[AuditLog] Failed to fetch audit log:', error);
|
||||
@@ -6250,6 +6265,25 @@ app.get('/api/audit-log', async (req: Request, res: Response): Promise<void> =>
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/audit-log/stats', async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requireAdmiral(req, res)) return;
|
||||
if (!requirePermission(req, res, 'system:audit')) return;
|
||||
|
||||
try {
|
||||
const now = Date.now();
|
||||
const db = DatabaseService.getInstance();
|
||||
const cutoff24h = now - 24 * 60 * 60 * 1000;
|
||||
const cutoff7d = now - 7 * 24 * 60 * 60 * 1000;
|
||||
const last30d = db.getAuditLogsInRange(now - HISTORY_WINDOW_MS, now);
|
||||
const last7d = last30d.filter(e => e.timestamp >= cutoff7d);
|
||||
const last24h = last7d.filter(e => e.timestamp >= cutoff24h);
|
||||
res.json(computeAuditStats({ now, last24h, last7d, last30d }));
|
||||
} catch (error) {
|
||||
console.error('[AuditLog] Failed to compute audit stats:', error);
|
||||
res.status(500).json({ error: 'Failed to compute audit stats' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/audit-log/export', async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requireAdmiral(req, res)) return;
|
||||
if (!requirePermission(req, res, 'system:audit')) return;
|
||||
|
||||
Reference in New Issue
Block a user