feat: ensemble ML, alert deduplication, larger synthetic dataset

- Replace IsolationForest with voting ensemble (IF + LOF + OC-SVM),
  anomaly flagged when ≥ 2/3 models agree
- Normalise per-process cpu_percent by CPU count to prevent false
  positives from multi-threaded processes (e.g. Firefox 130% → 65%)
- Deduplicate alerts: same PID anomalous within 30s updates last_seen
  and count instead of inserting a new row; WS broadcast only on first
  occurrence
- Show Duration column in Alerts table (e.g. "2м 30с ×28")
- Increase synthetic training data from 2000 to 10000 rows
- Add DB migration for last_seen/count columns on existing databases
This commit is contained in:
kilyabin
2026-05-28 14:25:34 +04:00
parent 20d5e53220
commit 9b7a840ba1
4 changed files with 88 additions and 33 deletions
+40 -16
View File
@@ -26,6 +26,16 @@ function severityLabel(s: Alert['severity']): string {
return 'низкий'
}
function fmtDuration(first: string, last: string | null): string | null {
if (!last) return null
const secs = Math.round((new Date(last).getTime() - new Date(first).getTime()) / 1000)
if (secs < 5) return null
if (secs < 60) return `${secs}с`
const m = Math.floor(secs / 60)
const s = secs % 60
return s > 0 ? `${m}м ${s}с` : `${m}м`
}
interface Props {
wsConnected: boolean
}
@@ -70,7 +80,8 @@ export function Alerts({ wsConnected }: Props) {
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-40">Время</TableHead>
<TableHead className="w-40">Начало</TableHead>
<TableHead className="w-28">Длительность</TableHead>
<TableHead className="w-16">PID</TableHead>
<TableHead>Процесс</TableHead>
<TableHead>Причина</TableHead>
@@ -78,21 +89,34 @@ export function Alerts({ wsConnected }: Props) {
</TableRow>
</TableHeader>
<TableBody>
{data.map((a) => (
<TableRow key={a.id}>
<TableCell className="font-mono text-xs text-muted-foreground whitespace-nowrap">
{fmtFull(a.timestamp)}
</TableCell>
<TableCell className="font-mono text-muted-foreground">{a.pid}</TableCell>
<TableCell className="font-mono">{a.process_name}</TableCell>
<TableCell className="text-sm text-muted-foreground">{a.reason}</TableCell>
<TableCell className="text-center">
<Badge variant={severityVariant(a.severity)}>
{severityLabel(a.severity)}
</Badge>
</TableCell>
</TableRow>
))}
{data.map((a) => {
const duration = fmtDuration(a.timestamp, a.last_seen)
return (
<TableRow key={a.id}>
<TableCell className="font-mono text-xs text-muted-foreground whitespace-nowrap">
{fmtFull(a.timestamp)}
</TableCell>
<TableCell className="font-mono text-xs whitespace-nowrap">
{duration ? (
<span className="text-yellow-400">{duration}</span>
) : (
<span className="text-muted-foreground"></span>
)}
{(a.count ?? 1) > 1 && (
<span className="ml-1 text-muted-foreground">×{a.count}</span>
)}
</TableCell>
<TableCell className="font-mono text-muted-foreground">{a.pid}</TableCell>
<TableCell className="font-mono">{a.process_name}</TableCell>
<TableCell className="text-sm text-muted-foreground">{a.reason}</TableCell>
<TableCell className="text-center">
<Badge variant={severityVariant(a.severity)}>
{severityLabel(a.severity)}
</Badge>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
)}
+2
View File
@@ -17,6 +17,8 @@ export interface ProcessMetric {
export interface Alert {
id: number
timestamp: string
last_seen: string | null
count: number
pid: number
process_name: string
reason: string