feat: add suspicious traffic detection dashboard widget with sensitive port monitoring and security posture indicator
Add dashboard_suspicious_traffic to detect external connections to sensitive ports (SSH/RDP/SMB/VNC/PostgreSQL/MySQL/Redis) from outside IPAM subnets with severity classification, extend Dashboard type with security_posture/suspicious_traffic/last_syncs fields, implement BarList component for traffic visualization with percentage bars and byte formatting, add security posture card
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AlertTriangle, Boxes, Network, Server, ShieldAlert } from "lucide-react";
|
||||
import { Activity, AlertTriangle, Boxes, Network, Radar, Server, ShieldAlert, ShieldCheck, Wifi } from "lucide-react";
|
||||
|
||||
import { api, Dashboard as DashboardData } from "../api/client";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
@@ -8,16 +8,78 @@ const cards = [
|
||||
["clusters", "Clusters", Server],
|
||||
["nodes", "Nodes", Boxes],
|
||||
["workloads", "VMs/LXCs", Network],
|
||||
["networks", "Networks", Network],
|
||||
["open_policy_violations", "Policy Violations", ShieldAlert],
|
||||
["networks", "Networks", Wifi],
|
||||
["open_policy_violations", "Signals", ShieldAlert],
|
||||
] as const;
|
||||
|
||||
function formatBytes(value: number) {
|
||||
if (!value) {
|
||||
return "0 B";
|
||||
}
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
|
||||
return `${(value / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
|
||||
}
|
||||
|
||||
function BarList({ items }: { items: Array<{ name: string; bytes: number }> }) {
|
||||
const max = Math.max(...items.map((item) => item.bytes), 1);
|
||||
if (!items.length) {
|
||||
return <div className="border-t border-border py-4 text-sm text-slate-500">No flow telemetry collected yet.</div>;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-3 border-t border-border pt-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.name} className="grid gap-1">
|
||||
<div className="flex items-center justify-between gap-3 text-sm">
|
||||
<span className="truncate font-medium">{item.name}</span>
|
||||
<span className="shrink-0 text-slate-500">{formatBytes(item.bytes)}</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
|
||||
<div className="h-full rounded-full bg-accent" style={{ width: `${Math.max((item.bytes / max) * 100, 4)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Dashboard() {
|
||||
const { data } = useQuery({ queryKey: ["dashboard"], queryFn: () => api<DashboardData>("/dashboard") });
|
||||
const suspicious = data?.suspicious_traffic ?? [];
|
||||
const postureStable = data?.security_posture !== "attention";
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Dashboard" subtitle="Operational overview across clusters, networks, policies, and sync health." />
|
||||
<section className="mb-5 rounded-md border border-border bg-panel p-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`grid h-11 w-11 place-items-center rounded-md ${postureStable ? "bg-accent/10 text-accent" : "bg-danger/10 text-danger"}`}>
|
||||
{postureStable ? <ShieldCheck size={22} /> : <Radar size={22} />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold">{postureStable ? "Control plane stable" : "Attention required"}</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
{postureStable ? "No suspicious traffic signals or faulty nodes detected." : `${suspicious.length} suspicious traffic signal${suspicious.length === 1 ? "" : "s"} require review.`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-sm">
|
||||
<div className="rounded-md border border-border px-3 py-2">
|
||||
<div className="text-xs text-slate-500">Telemetry</div>
|
||||
<div className="font-medium">{(data?.top_talkers ?? []).length ? "Active" : "Waiting"}</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-border px-3 py-2">
|
||||
<div className="text-xs text-slate-500">Faulty Nodes</div>
|
||||
<div className="font-medium">{data?.faulty_nodes.length ?? 0}</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-border px-3 py-2">
|
||||
<div className="text-xs text-slate-500">Signals</div>
|
||||
<div className="font-medium">{suspicious.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||
{cards.map(([key, label, Icon]) => (
|
||||
<div key={key} className="rounded-md border border-border bg-panel p-4">
|
||||
@@ -29,27 +91,63 @@ export function Dashboard() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 grid gap-4 lg:grid-cols-2">
|
||||
<div className="mt-5 grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
|
||||
<section className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="mb-3 flex items-center gap-2 font-medium">
|
||||
<Radar size={18} />
|
||||
Suspicious Traffic
|
||||
</div>
|
||||
{suspicious.length ? (
|
||||
<div className="divide-y divide-border border-t border-border">
|
||||
{suspicious.map((item) => (
|
||||
<div key={`${item.source}-${item.destination}-${item.port}`} className="grid gap-2 py-3 md:grid-cols-[1fr_auto] md:items-center">
|
||||
<div>
|
||||
<div className="font-medium">{item.source} -> {item.destination}</div>
|
||||
<div className="text-xs text-slate-500">{item.protocol}:{item.port} · {item.reason}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className={`rounded-md px-2 py-1 text-xs ${item.severity === "high" ? "bg-danger/10 text-danger" : "bg-amber-500/10 text-amber-500"}`}>{item.severity}</span>
|
||||
<span className="text-slate-500">{formatBytes(item.bytes)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-t border-border py-4 text-sm text-slate-500">No suspicious traffic detected from current flow telemetry.</div>
|
||||
)}
|
||||
</section>
|
||||
<section className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="mb-3 flex items-center gap-2 font-medium">
|
||||
<Activity size={18} />
|
||||
Top Talkers
|
||||
</div>
|
||||
<BarList items={data?.top_talkers ?? []} />
|
||||
</section>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-4 lg:grid-cols-2">
|
||||
<section className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="mb-3 flex items-center gap-2 font-medium">
|
||||
<AlertTriangle size={18} />
|
||||
Faulty Nodes
|
||||
</div>
|
||||
{(data?.faulty_nodes ?? []).map((node) => (
|
||||
{(data?.faulty_nodes ?? []).length ? (data?.faulty_nodes ?? []).map((node) => (
|
||||
<div key={node.id} className="flex justify-between border-t border-border py-3 text-sm">
|
||||
<span>{node.name}</span>
|
||||
<span className="text-danger">{node.status}</span>
|
||||
</div>
|
||||
))}
|
||||
)) : <div className="border-t border-border py-3 text-sm text-slate-500">All known nodes are online.</div>}
|
||||
</section>
|
||||
<section className="rounded-md border border-border bg-panel p-4">
|
||||
<div className="mb-3 font-medium">Top Talkers</div>
|
||||
{(data?.top_talkers ?? []).length ? (data?.top_talkers ?? []).map((item) => (
|
||||
<div key={item.name} className="flex justify-between border-t border-border py-3 text-sm">
|
||||
<span>{item.name}</span>
|
||||
<span>{Math.round(item.bytes / 1_000_000)} MB</span>
|
||||
<div className="mb-3 font-medium">Recent Cluster Sync</div>
|
||||
{(data?.last_syncs ?? []).length ? (data?.last_syncs ?? []).map((cluster) => (
|
||||
<div key={cluster.id} className="flex justify-between gap-3 border-t border-border py-3 text-sm">
|
||||
<div>
|
||||
<div className="font-medium">{cluster.name}</div>
|
||||
<div className="text-xs text-slate-500">{cluster.provider} · {cluster.at ? new Date(cluster.at).toLocaleString() : "never synced"}</div>
|
||||
</div>
|
||||
<span className={cluster.status === "failed" ? "text-danger" : "text-accent"}>{cluster.status ?? "unknown"}</span>
|
||||
</div>
|
||||
)) : <div className="border-t border-border py-3 text-sm text-slate-500">No flow telemetry collected yet.</div>}
|
||||
)) : <div className="border-t border-border py-3 text-sm text-slate-500">No cluster sync history yet.</div>}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user