mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 13:32:08 +01:00
refactor: fix biome linting issues and update project documentation
- Fix 36+ biome linting issues reducing errors/warnings from 227 to 191 - Replace explicit 'any' types with proper TypeScript interfaces - Fix React hooks dependencies and useCallback patterns - Resolve unused variables and parameter assignment issues - Improve accessibility with proper label associations - Add comprehensive API documentation for admin and security features - Update README.md with accurate PostgreSQL setup and current tech stack - Create complete documentation for audit logging, CSP monitoring, and batch processing - Fix outdated project information and missing developer workflows
This commit is contained in:
200
components/security/GeographicThreatMap.tsx
Normal file
200
components/security/GeographicThreatMap.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
interface GeographicThreatMapProps {
|
||||
geoDistribution: Record<string, number>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// Simple country code to name mapping for common countries
|
||||
const countryNames: Record<string, string> = {
|
||||
USA: "United States",
|
||||
GBR: "United Kingdom",
|
||||
DEU: "Germany",
|
||||
FRA: "France",
|
||||
JPN: "Japan",
|
||||
CHN: "China",
|
||||
IND: "India",
|
||||
BRA: "Brazil",
|
||||
CAN: "Canada",
|
||||
AUS: "Australia",
|
||||
RUS: "Russia",
|
||||
ESP: "Spain",
|
||||
ITA: "Italy",
|
||||
NLD: "Netherlands",
|
||||
KOR: "South Korea",
|
||||
MEX: "Mexico",
|
||||
CHE: "Switzerland",
|
||||
SWE: "Sweden",
|
||||
NOR: "Norway",
|
||||
DNK: "Denmark",
|
||||
FIN: "Finland",
|
||||
POL: "Poland",
|
||||
BEL: "Belgium",
|
||||
AUT: "Austria",
|
||||
NZL: "New Zealand",
|
||||
SGP: "Singapore",
|
||||
THA: "Thailand",
|
||||
IDN: "Indonesia",
|
||||
MYS: "Malaysia",
|
||||
PHL: "Philippines",
|
||||
VNM: "Vietnam",
|
||||
ARE: "UAE",
|
||||
SAU: "Saudi Arabia",
|
||||
ISR: "Israel",
|
||||
ZAF: "South Africa",
|
||||
EGY: "Egypt",
|
||||
TUR: "Turkey",
|
||||
GRC: "Greece",
|
||||
PRT: "Portugal",
|
||||
CZE: "Czech Republic",
|
||||
HUN: "Hungary",
|
||||
ROU: "Romania",
|
||||
BGR: "Bulgaria",
|
||||
HRV: "Croatia",
|
||||
SVN: "Slovenia",
|
||||
SVK: "Slovakia",
|
||||
EST: "Estonia",
|
||||
LVA: "Latvia",
|
||||
LTU: "Lithuania",
|
||||
LUX: "Luxembourg",
|
||||
MLT: "Malta",
|
||||
CYP: "Cyprus",
|
||||
ISL: "Iceland",
|
||||
IRL: "Ireland",
|
||||
ARG: "Argentina",
|
||||
CHL: "Chile",
|
||||
COL: "Colombia",
|
||||
PER: "Peru",
|
||||
URY: "Uruguay",
|
||||
ECU: "Ecuador",
|
||||
BOL: "Bolivia",
|
||||
PRY: "Paraguay",
|
||||
VEN: "Venezuela",
|
||||
UKR: "Ukraine",
|
||||
BLR: "Belarus",
|
||||
MDA: "Moldova",
|
||||
GEO: "Georgia",
|
||||
ARM: "Armenia",
|
||||
AZE: "Azerbaijan",
|
||||
KAZ: "Kazakhstan",
|
||||
UZB: "Uzbekistan",
|
||||
KGZ: "Kyrgyzstan",
|
||||
TJK: "Tajikistan",
|
||||
TKM: "Turkmenistan",
|
||||
MNG: "Mongolia",
|
||||
};
|
||||
|
||||
export function GeographicThreatMap({
|
||||
geoDistribution,
|
||||
title = "Geographic Threat Distribution",
|
||||
}: GeographicThreatMapProps) {
|
||||
const sortedCountries = Object.entries(geoDistribution)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 12);
|
||||
|
||||
const totalEvents = Object.values(geoDistribution).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0
|
||||
);
|
||||
|
||||
const getThreatLevel = (count: number, total: number) => {
|
||||
const percentage = (count / total) * 100;
|
||||
if (percentage > 50) return { level: "high", color: "destructive" };
|
||||
if (percentage > 20) return { level: "medium", color: "secondary" };
|
||||
if (percentage > 5) return { level: "low", color: "outline" };
|
||||
return { level: "minimal", color: "outline" };
|
||||
};
|
||||
|
||||
const getCountryName = (code: string) => {
|
||||
return countryNames[code] || code;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>
|
||||
Security events by country ({totalEvents} total events)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{sortedCountries.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>No geographic data available</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{sortedCountries.map(([countryCode, count]) => {
|
||||
const threat = getThreatLevel(count, totalEvents);
|
||||
const percentage = ((count / totalEvents) * 100).toFixed(1);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={countryCode}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">
|
||||
{getCountryName(countryCode)}
|
||||
</span>
|
||||
<Badge
|
||||
variant={threat.color as "default" | "secondary" | "destructive" | "outline"}
|
||||
className="text-xs"
|
||||
>
|
||||
{threat.level}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{count} events ({percentage}%)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold">{count}</div>
|
||||
<div className="w-16 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${
|
||||
threat.level === "high"
|
||||
? "bg-red-500"
|
||||
: threat.level === "medium"
|
||||
? "bg-yellow-500"
|
||||
: threat.level === "low"
|
||||
? "bg-blue-500"
|
||||
: "bg-gray-400"
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(100, (count / Math.max(...Object.values(geoDistribution))) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{Object.keys(geoDistribution).length > 12 && (
|
||||
<div className="text-center pt-4 border-t">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
And {Object.keys(geoDistribution).length - 12} more
|
||||
countries...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
274
components/security/SecurityAlertsTable.tsx
Normal file
274
components/security/SecurityAlertsTable.tsx
Normal file
@ -0,0 +1,274 @@
|
||||
"use client";
|
||||
|
||||
import { AlertTriangle, CheckCircle, Eye, EyeOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
interface SecurityAlert {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
severity: string;
|
||||
type: string;
|
||||
title: string;
|
||||
description: string;
|
||||
eventType: string;
|
||||
context: Record<string, unknown>;
|
||||
metadata: Record<string, unknown>;
|
||||
acknowledged: boolean;
|
||||
}
|
||||
|
||||
interface SecurityAlertsTableProps {
|
||||
alerts: SecurityAlert[];
|
||||
onAcknowledge: (_alertId: string) => void;
|
||||
}
|
||||
|
||||
export function SecurityAlertsTable({
|
||||
alerts,
|
||||
onAcknowledge,
|
||||
}: SecurityAlertsTableProps) {
|
||||
const [showAcknowledged, setShowAcknowledged] = useState(false);
|
||||
const [selectedAlert, setSelectedAlert] = useState<SecurityAlert | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity?.toLowerCase()) {
|
||||
case "critical":
|
||||
return "destructive";
|
||||
case "high":
|
||||
return "destructive";
|
||||
case "medium":
|
||||
return "secondary";
|
||||
case "low":
|
||||
return "outline";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAlerts = alerts.filter(
|
||||
(alert) => showAcknowledged || !alert.acknowledged
|
||||
);
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
const formatAlertType = (type: string) => {
|
||||
return type
|
||||
.replace(/_/g, " ")
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-semibold">Security Alerts</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{filteredAlerts.length} alerts{" "}
|
||||
{showAcknowledged ? "total" : "active"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowAcknowledged(!showAcknowledged)}
|
||||
>
|
||||
{showAcknowledged ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
{showAcknowledged ? "Hide Acknowledged" : "Show All"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{filteredAlerts.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-8">
|
||||
<CheckCircle className="h-12 w-12 text-green-500 mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No Active Alerts</h3>
|
||||
<p className="text-muted-foreground text-center">
|
||||
All security alerts have been addressed. System is operating
|
||||
normally.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Severity</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>Timestamp</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredAlerts.map((alert) => (
|
||||
<TableRow
|
||||
key={alert.id}
|
||||
className={alert.acknowledged ? "opacity-60" : ""}
|
||||
>
|
||||
<TableCell>
|
||||
<Badge variant={getSeverityColor(alert.severity)}>
|
||||
{alert.severity}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<span className="font-medium">
|
||||
{formatAlertType(alert.type)}
|
||||
</span>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{alert.eventType}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<span className="font-medium">{alert.title}</span>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{alert.description}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm">
|
||||
{formatTimestamp(alert.timestamp)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{alert.acknowledged ? (
|
||||
<Badge variant="outline">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
Acknowledged
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setSelectedAlert(alert)}
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
</Button>
|
||||
{!alert.acknowledged && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onAcknowledge(alert.id)}
|
||||
>
|
||||
Acknowledge
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Alert Details Modal */}
|
||||
{selectedAlert && (
|
||||
<Card className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
|
||||
<Card className="max-w-2xl w-full max-h-[80vh] overflow-auto">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<CardTitle>{selectedAlert.title}</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getSeverityColor(selectedAlert.severity)}>
|
||||
{selectedAlert.severity}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{formatAlertType(selectedAlert.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedAlert(null)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Description</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{selectedAlert.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Context</h4>
|
||||
<div className="bg-muted p-3 rounded-md">
|
||||
<pre className="text-xs overflow-auto">
|
||||
{JSON.stringify(selectedAlert.context, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedAlert.metadata &&
|
||||
Object.keys(selectedAlert.metadata).length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Metadata</h4>
|
||||
<div className="bg-muted p-3 rounded-md">
|
||||
<pre className="text-xs overflow-auto">
|
||||
{JSON.stringify(selectedAlert.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatTimestamp(selectedAlert.timestamp)}
|
||||
</span>
|
||||
{!selectedAlert.acknowledged && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onAcknowledge(selectedAlert.id);
|
||||
setSelectedAlert(null);
|
||||
}}
|
||||
>
|
||||
Acknowledge Alert
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
489
components/security/SecurityConfigModal.tsx
Normal file
489
components/security/SecurityConfigModal.tsx
Normal file
@ -0,0 +1,489 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
interface SecurityConfig {
|
||||
thresholds: {
|
||||
failedLoginsPerMinute: number;
|
||||
failedLoginsPerHour: number;
|
||||
rateLimitViolationsPerMinute: number;
|
||||
cspViolationsPerMinute: number;
|
||||
adminActionsPerHour: number;
|
||||
massDataAccessThreshold: number;
|
||||
suspiciousIPThreshold: number;
|
||||
};
|
||||
alerting: {
|
||||
enabled: boolean;
|
||||
channels: string[];
|
||||
suppressDuplicateMinutes: number;
|
||||
escalationTimeoutMinutes: number;
|
||||
};
|
||||
retention: {
|
||||
alertRetentionDays: number;
|
||||
metricsRetentionDays: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecurityConfigModalProps {
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export function SecurityConfigModal({
|
||||
onClose,
|
||||
onSave,
|
||||
}: SecurityConfigModalProps) {
|
||||
const [config, setConfig] = useState<SecurityConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const loadConfig = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/security-monitoring");
|
||||
if (!response.ok) throw new Error("Failed to load config");
|
||||
|
||||
const data = await response.json();
|
||||
setConfig(data.config);
|
||||
} catch (error) {
|
||||
console.error("Error loading config:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, [loadConfig]);
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!config) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch("/api/admin/security-monitoring", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to save config");
|
||||
|
||||
onSave();
|
||||
} catch (error) {
|
||||
console.error("Error saving config:", error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateThreshold = (
|
||||
key: keyof SecurityConfig["thresholds"],
|
||||
value: number
|
||||
) => {
|
||||
if (!config) return;
|
||||
setConfig({
|
||||
...config,
|
||||
thresholds: {
|
||||
...config.thresholds,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateAlerting = (
|
||||
key: keyof SecurityConfig["alerting"],
|
||||
value: unknown
|
||||
) => {
|
||||
if (!config) return;
|
||||
setConfig({
|
||||
...config,
|
||||
alerting: {
|
||||
...config.alerting,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateRetention = (
|
||||
key: keyof SecurityConfig["retention"],
|
||||
value: number
|
||||
) => {
|
||||
if (!config) return;
|
||||
setConfig({
|
||||
...config,
|
||||
retention: {
|
||||
...config.retention,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAlertChannel = (channel: string) => {
|
||||
if (!config) return;
|
||||
const channels = config.alerting.channels.includes(channel)
|
||||
? config.alerting.channels.filter((c) => c !== channel)
|
||||
: [...config.alerting.channels, channel];
|
||||
|
||||
updateAlerting("channels", channels);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Dialog open onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<Dialog open onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Error</DialogTitle>
|
||||
<DialogDescription>
|
||||
Failed to load security configuration
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Security Monitoring Configuration</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure security monitoring thresholds, alerting, and data
|
||||
retention
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="thresholds" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="thresholds">Thresholds</TabsTrigger>
|
||||
<TabsTrigger value="alerting">Alerting</TabsTrigger>
|
||||
<TabsTrigger value="retention">Data Retention</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="thresholds" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Detection Thresholds</CardTitle>
|
||||
<CardDescription>
|
||||
Configure when security alerts should be triggered
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="failedLoginsPerMinute">
|
||||
Failed Logins per Minute
|
||||
</Label>
|
||||
<Input
|
||||
id="failedLoginsPerMinute"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={config.thresholds.failedLoginsPerMinute}
|
||||
onChange={(e) =>
|
||||
updateThreshold(
|
||||
"failedLoginsPerMinute",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="failedLoginsPerHour">
|
||||
Failed Logins per Hour
|
||||
</Label>
|
||||
<Input
|
||||
id="failedLoginsPerHour"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1000"
|
||||
value={config.thresholds.failedLoginsPerHour}
|
||||
onChange={(e) =>
|
||||
updateThreshold(
|
||||
"failedLoginsPerHour",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rateLimitViolationsPerMinute">
|
||||
Rate Limit Violations per Minute
|
||||
</Label>
|
||||
<Input
|
||||
id="rateLimitViolationsPerMinute"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={config.thresholds.rateLimitViolationsPerMinute}
|
||||
onChange={(e) =>
|
||||
updateThreshold(
|
||||
"rateLimitViolationsPerMinute",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cspViolationsPerMinute">
|
||||
CSP Violations per Minute
|
||||
</Label>
|
||||
<Input
|
||||
id="cspViolationsPerMinute"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={config.thresholds.cspViolationsPerMinute}
|
||||
onChange={(e) =>
|
||||
updateThreshold(
|
||||
"cspViolationsPerMinute",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="adminActionsPerHour">
|
||||
Admin Actions per Hour
|
||||
</Label>
|
||||
<Input
|
||||
id="adminActionsPerHour"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={config.thresholds.adminActionsPerHour}
|
||||
onChange={(e) =>
|
||||
updateThreshold(
|
||||
"adminActionsPerHour",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="suspiciousIPThreshold">
|
||||
Suspicious IP Threshold
|
||||
</Label>
|
||||
<Input
|
||||
id="suspiciousIPThreshold"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={config.thresholds.suspiciousIPThreshold}
|
||||
onChange={(e) =>
|
||||
updateThreshold(
|
||||
"suspiciousIPThreshold",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="alerting" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Alert Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Configure how and when alerts are sent
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="alerting-enabled"
|
||||
checked={config.alerting.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
updateAlerting("enabled", checked)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="alerting-enabled">
|
||||
Enable Security Alerting
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Alert Channels</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{["EMAIL", "WEBHOOK", "SLACK", "DISCORD", "PAGERDUTY"].map(
|
||||
(channel) => (
|
||||
<Badge
|
||||
key={channel}
|
||||
variant={
|
||||
config.alerting.channels.includes(channel)
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleAlertChannel(channel)}
|
||||
>
|
||||
{channel}
|
||||
</Badge>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="suppressDuplicateMinutes">
|
||||
Suppress Duplicates (minutes)
|
||||
</Label>
|
||||
<Input
|
||||
id="suppressDuplicateMinutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1440"
|
||||
value={config.alerting.suppressDuplicateMinutes}
|
||||
onChange={(e) =>
|
||||
updateAlerting(
|
||||
"suppressDuplicateMinutes",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="escalationTimeoutMinutes">
|
||||
Escalation Timeout (minutes)
|
||||
</Label>
|
||||
<Input
|
||||
id="escalationTimeoutMinutes"
|
||||
type="number"
|
||||
min="5"
|
||||
max="1440"
|
||||
value={config.alerting.escalationTimeoutMinutes}
|
||||
onChange={(e) =>
|
||||
updateAlerting(
|
||||
"escalationTimeoutMinutes",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="retention" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Retention</CardTitle>
|
||||
<CardDescription>
|
||||
Configure how long security data is stored
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="alertRetentionDays">
|
||||
Alert Retention (days)
|
||||
</Label>
|
||||
<Input
|
||||
id="alertRetentionDays"
|
||||
type="number"
|
||||
min="1"
|
||||
max="3650"
|
||||
value={config.retention.alertRetentionDays}
|
||||
onChange={(e) =>
|
||||
updateRetention(
|
||||
"alertRetentionDays",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="metricsRetentionDays">
|
||||
Metrics Retention (days)
|
||||
</Label>
|
||||
<Input
|
||||
id="metricsRetentionDays"
|
||||
type="number"
|
||||
min="1"
|
||||
max="3650"
|
||||
value={config.retention.metricsRetentionDays}
|
||||
onChange={(e) =>
|
||||
updateRetention(
|
||||
"metricsRetentionDays",
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>
|
||||
• Alert data includes security alerts and acknowledgments
|
||||
</p>
|
||||
<p>• Metrics data includes aggregated security statistics</p>
|
||||
<p>
|
||||
• Audit logs are retained separately according to audit
|
||||
policy
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={saveConfig} disabled={saving}>
|
||||
{saving ? "Saving..." : "Save Configuration"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
71
components/security/SecurityMetricsChart.tsx
Normal file
71
components/security/SecurityMetricsChart.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
interface SecurityMetricsChartProps {
|
||||
data: Array<{ hour: number; count: number }>;
|
||||
type?: "line" | "bar";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function SecurityMetricsChart({
|
||||
data,
|
||||
type = "line",
|
||||
title,
|
||||
}: SecurityMetricsChartProps) {
|
||||
const chartData = data.map((item) => ({
|
||||
hour: `${item.hour}:00`,
|
||||
count: item.count,
|
||||
}));
|
||||
|
||||
const ChartComponent = type === "line" ? LineChart : BarChart;
|
||||
const DataComponent =
|
||||
type === "line" ? (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke="#8884d8"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: "#8884d8", strokeWidth: 2 }}
|
||||
/>
|
||||
) : (
|
||||
<Bar dataKey="count" fill="#8884d8" />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{title && <h3 className="text-lg font-semibold">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ChartComponent data={chartData}>
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={{ stroke: "#e5e7eb" }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={{ stroke: "#e5e7eb" }}
|
||||
axisLine={{ stroke: "#e5e7eb" }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "#f9fafb",
|
||||
border: "1px solid #e5e7eb",
|
||||
borderRadius: "6px",
|
||||
}}
|
||||
/>
|
||||
{DataComponent}
|
||||
</ChartComponent>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
84
components/security/ThreatLevelIndicator.tsx
Normal file
84
components/security/ThreatLevelIndicator.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { AlertCircle, AlertTriangle, Shield, Zap } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface ThreatLevelIndicatorProps {
|
||||
level: "LOW" | "MODERATE" | "HIGH" | "CRITICAL";
|
||||
score?: number;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export function ThreatLevelIndicator({
|
||||
level,
|
||||
score,
|
||||
size = "md",
|
||||
}: ThreatLevelIndicatorProps) {
|
||||
const getConfig = (threatLevel: string) => {
|
||||
switch (threatLevel) {
|
||||
case "CRITICAL":
|
||||
return {
|
||||
color: "destructive",
|
||||
bgColor: "bg-red-500",
|
||||
icon: Zap,
|
||||
text: "Critical Threat",
|
||||
description: "Immediate action required",
|
||||
};
|
||||
case "HIGH":
|
||||
return {
|
||||
color: "destructive",
|
||||
bgColor: "bg-orange-500",
|
||||
icon: AlertCircle,
|
||||
text: "High Threat",
|
||||
description: "Urgent attention needed",
|
||||
};
|
||||
case "MODERATE":
|
||||
return {
|
||||
color: "secondary",
|
||||
bgColor: "bg-yellow-500",
|
||||
icon: AlertTriangle,
|
||||
text: "Moderate Threat",
|
||||
description: "Monitor closely",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: "outline",
|
||||
bgColor: "bg-green-500",
|
||||
icon: Shield,
|
||||
text: "Low Threat",
|
||||
description: "System is secure",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getConfig(level);
|
||||
const Icon = config.icon;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: { icon: "h-4 w-4", text: "text-sm", badge: "text-xs" },
|
||||
md: { icon: "h-5 w-5", text: "text-base", badge: "text-sm" },
|
||||
lg: { icon: "h-6 w-6", text: "text-lg", badge: "text-base" },
|
||||
};
|
||||
|
||||
const classes = sizeClasses[size];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`p-2 rounded-full ${config.bgColor}`}>
|
||||
<Icon className={`${classes.icon} text-white`} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={config.color as "default" | "secondary" | "destructive" | "outline"} className={classes.badge}>
|
||||
{config.text}
|
||||
</Badge>
|
||||
{score !== undefined && (
|
||||
<span className={`font-medium ${classes.text}`}>{score}/100</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{config.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user