| 1 | import React, { useEffect, useState } from "react";
|
|---|
| 2 | import './App.css';
|
|---|
| 3 |
|
|---|
| 4 | function App() {
|
|---|
| 5 | const [computers, setComputers] = useState([]);
|
|---|
| 6 | const [filteredComputers, setFilteredComputers] = useState([]);
|
|---|
| 7 | const [stats, setStats] = useState(null);
|
|---|
| 8 | const [selectedComputer, setSelectedComputer] = useState(null);
|
|---|
| 9 | const [computerDetails, setComputerDetails] = useState(null);
|
|---|
| 10 | const [loadingDetails, setLoadingDetails] = useState(false);
|
|---|
| 11 | const [performanceData, setPerformanceData] = useState([]);
|
|---|
| 12 | const [eventDistribution, setEventDistribution] = useState([]);
|
|---|
| 13 | const [networkTraffic, setNetworkTraffic] = useState([]);
|
|---|
| 14 | const [topProcesses, setTopProcesses] = useState([]);
|
|---|
| 15 | // Бои за графикони
|
|---|
| 16 | const CHART_COLORS = {
|
|---|
| 17 | primary: '#2563eb',
|
|---|
| 18 | secondary: '#7c3aed',
|
|---|
| 19 | accentGreen: '#10b981',
|
|---|
| 20 | accentOrange: '#f59e0b',
|
|---|
| 21 | accentRed: '#ef4444',
|
|---|
| 22 | lightBlue: '#93c5fd',
|
|---|
| 23 | lightPurple: '#c4b5fd'
|
|---|
| 24 | };
|
|---|
| 25 |
|
|---|
| 26 | // Функција за вчитување на податоци за графикони
|
|---|
| 27 | const loadChartData = () => {
|
|---|
| 28 | // Перформанс податоци (симулирани, треба да ги земеш од API)
|
|---|
| 29 | const mockPerfData = Array.from({ length: 24 }, (_, i) => ({
|
|---|
| 30 | hour: `${i}:00`,
|
|---|
| 31 | cpu: 20 + Math.random() * 30,
|
|---|
| 32 | memory: 30 + Math.random() * 40,
|
|---|
| 33 | network: Math.random() * 100
|
|---|
| 34 | }));
|
|---|
| 35 | setPerformanceData(mockPerfData);
|
|---|
| 36 |
|
|---|
| 37 | // Дистрибуција на настани
|
|---|
| 38 | const mockEventDist = [
|
|---|
| 39 | { name: 'Process Create', value: 45, color: CHART_COLORS.primary },
|
|---|
| 40 | { name: 'Network', value: 25, color: CHART_COLORS.secondary },
|
|---|
| 41 | { name: 'File Create', value: 15, color: CHART_COLORS.accentGreen },
|
|---|
| 42 | { name: 'Registry', value: 10, color: CHART_COLORS.accentOrange },
|
|---|
| 43 | { name: 'Other', value: 5, color: CHART_COLORS.accentRed }
|
|---|
| 44 | ];
|
|---|
| 45 | setEventDistribution(mockEventDist);
|
|---|
| 46 |
|
|---|
| 47 | // Мрежен сообраќај
|
|---|
| 48 | const mockNetworkData = Array.from({ length: 12 }, (_, i) => ({
|
|---|
| 49 | time: `${i * 2}:00`,
|
|---|
| 50 | inbound: 50 + Math.random() * 200,
|
|---|
| 51 | outbound: 30 + Math.random() * 150
|
|---|
| 52 | }));
|
|---|
| 53 | setNetworkTraffic(mockNetworkData);
|
|---|
| 54 |
|
|---|
| 55 | // Топ процеси
|
|---|
| 56 | const mockTopProcesses = [
|
|---|
| 57 | { name: 'chrome.exe', cpu: 25, memory: 1200 },
|
|---|
| 58 | { name: 'python.exe', cpu: 18, memory: 800 },
|
|---|
| 59 | { name: 'explorer.exe', cpu: 12, memory: 300 },
|
|---|
| 60 | { name: 'svchost.exe', cpu: 8, memory: 200 },
|
|---|
| 61 | { name: 'node.exe', cpu: 6, memory: 500 }
|
|---|
| 62 | ];
|
|---|
| 63 | setTopProcesses(mockTopProcesses);
|
|---|
| 64 | };
|
|---|
| 65 |
|
|---|
| 66 | useEffect(() => {
|
|---|
| 67 | loadChartData();
|
|---|
| 68 | // Автоматско освежување на графикони
|
|---|
| 69 | const chartInterval = setInterval(loadChartData, 30000);
|
|---|
| 70 | return () => clearInterval(chartInterval);
|
|---|
| 71 | }, []);
|
|---|
| 72 | // Filter state
|
|---|
| 73 | const [filters, setFilters] = useState({
|
|---|
| 74 | status: "all",
|
|---|
| 75 | search: "",
|
|---|
| 76 | os: "all",
|
|---|
| 77 | minCpu: 0,
|
|---|
| 78 | minRam: 0
|
|---|
| 79 | });
|
|---|
| 80 |
|
|---|
| 81 | // AI chat state
|
|---|
| 82 | const [chatQuestion, setChatQuestion] = useState("");
|
|---|
| 83 | const [chatAnswer, setChatAnswer] = useState("");
|
|---|
| 84 | const [chatLoading, setChatLoading] = useState(false);
|
|---|
| 85 | const [chatComputer, setChatComputer] = useState("");
|
|---|
| 86 | const [chatHistory, setChatHistory] = useState([]);
|
|---|
| 87 | const [isChatOpen, setIsChatOpen] = useState(true);
|
|---|
| 88 | const [showWelcome, setShowWelcome] = useState(true);
|
|---|
| 89 |
|
|---|
| 90 | // Modal tabs state
|
|---|
| 91 | const [activeTab, setActiveTab] = useState('security');
|
|---|
| 92 | const [sysmonData, setSysmonData] = useState(null);
|
|---|
| 93 | const [networkData, setNetworkData] = useState(null);
|
|---|
| 94 | const [loadingSysmon, setLoadingSysmon] = useState(false);
|
|---|
| 95 | const [loadingNetwork, setLoadingNetwork] = useState(false);
|
|---|
| 96 | const [selectedSysmonProcess, setSelectedSysmonProcess] = useState(null);
|
|---|
| 97 | const [selectedNetworkItem, setSelectedNetworkItem] = useState(null);
|
|---|
| 98 |
|
|---|
| 99 | // Load main data
|
|---|
| 100 | useEffect(() => {
|
|---|
| 101 | loadComputers();
|
|---|
| 102 | loadStats();
|
|---|
| 103 | const interval = setInterval(() => {
|
|---|
| 104 | loadComputers();
|
|---|
| 105 | loadStats();
|
|---|
| 106 | }, 30000);
|
|---|
| 107 | return () => clearInterval(interval);
|
|---|
| 108 | }, []);
|
|---|
| 109 |
|
|---|
| 110 | // Apply filters
|
|---|
| 111 | useEffect(() => {
|
|---|
| 112 | let result = computers;
|
|---|
| 113 | if (filters.status !== "all") {
|
|---|
| 114 | result = result.filter(c => c.status === filters.status);
|
|---|
| 115 | }
|
|---|
| 116 | if (filters.os !== "all") {
|
|---|
| 117 | result = result.filter(c => c.os && c.os.toLowerCase().includes(filters.os.toLowerCase()));
|
|---|
| 118 | }
|
|---|
| 119 | if (filters.minCpu > 0) {
|
|---|
| 120 | result = result.filter(c => (c.avg_cpu || 0) >= filters.minCpu);
|
|---|
| 121 | }
|
|---|
| 122 | if (filters.minRam > 0) {
|
|---|
| 123 | result = result.filter(c => (c.avg_ram || 0) >= filters.minRam);
|
|---|
| 124 | }
|
|---|
| 125 | if (filters.search.trim()) {
|
|---|
| 126 | const searchTerm = filters.search.toLowerCase();
|
|---|
| 127 | result = result.filter(c =>
|
|---|
| 128 | c.name.toLowerCase().includes(searchTerm) ||
|
|---|
| 129 | (c.user && c.user.toLowerCase().includes(searchTerm)) ||
|
|---|
| 130 | (c.ip && c.ip.includes(searchTerm))
|
|---|
| 131 | );
|
|---|
| 132 | }
|
|---|
| 133 | setFilteredComputers(result);
|
|---|
| 134 | }, [computers, filters]);
|
|---|
| 135 |
|
|---|
| 136 | const loadComputers = () => {
|
|---|
| 137 | fetch("/api/computers")
|
|---|
| 138 | .then((r) => r.json())
|
|---|
| 139 | .then(data => setComputers(data))
|
|---|
| 140 | .catch((err) => console.error("API error:", err));
|
|---|
| 141 | };
|
|---|
| 142 |
|
|---|
| 143 | const loadStats = () => {
|
|---|
| 144 | fetch("/api/stats")
|
|---|
| 145 | .then((r) => r.json())
|
|---|
| 146 | .then(setStats)
|
|---|
| 147 | .catch((err) => console.error("API error:", err));
|
|---|
| 148 | };
|
|---|
| 149 |
|
|---|
| 150 | const openComputer = (name) => {
|
|---|
| 151 | setSelectedComputer(name);
|
|---|
| 152 | setLoadingDetails(true);
|
|---|
| 153 | setComputerDetails(null);
|
|---|
| 154 | setActiveTab('security');
|
|---|
| 155 | setSysmonData(null);
|
|---|
| 156 | setNetworkData(null);
|
|---|
| 157 |
|
|---|
| 158 | fetch(`/api/computer/${encodeURIComponent(name)}`)
|
|---|
| 159 | .then((r) => r.json())
|
|---|
| 160 | .then((data) => {
|
|---|
| 161 | setComputerDetails(data);
|
|---|
| 162 | setLoadingDetails(false);
|
|---|
| 163 | })
|
|---|
| 164 | .catch((err) => {
|
|---|
| 165 | console.error("API error:", err);
|
|---|
| 166 | setLoadingDetails(false);
|
|---|
| 167 | });
|
|---|
| 168 | };
|
|---|
| 169 |
|
|---|
| 170 | // Load Sysmon processes
|
|---|
| 171 | const loadSysmonProcesses = () => {
|
|---|
| 172 | if (!selectedComputer) return;
|
|---|
| 173 | setLoadingSysmon(true);
|
|---|
| 174 | setSelectedSysmonProcess(null);
|
|---|
| 175 |
|
|---|
| 176 | // КОРИГИРАЈ ГО ОВА URL
|
|---|
| 177 | fetch(`/api/computer/${encodeURIComponent(selectedComputer)}/sysmon`)
|
|---|
| 178 | .then((r) => {
|
|---|
| 179 | if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|---|
| 180 | return r.json();
|
|---|
| 181 | })
|
|---|
| 182 | .then((data) => {
|
|---|
| 183 | console.log("Sysmon data received:", data);
|
|---|
| 184 |
|
|---|
| 185 | // Ако data.processes е празна, пробај со детален endpoint
|
|---|
| 186 | if (!data.processes || data.processes.length === 0) {
|
|---|
| 187 | return fetch(`/api/computer/${encodeURIComponent(selectedComputer)}/detailed-sysmon`)
|
|---|
| 188 | .then(r => r.json())
|
|---|
| 189 | .then(detailedData => {
|
|---|
| 190 | setSysmonData(detailedData);
|
|---|
| 191 | setLoadingSysmon(false);
|
|---|
| 192 | });
|
|---|
| 193 | }
|
|---|
| 194 |
|
|---|
| 195 | setSysmonData(data);
|
|---|
| 196 | setLoadingSysmon(false);
|
|---|
| 197 | })
|
|---|
| 198 | .catch((err) => {
|
|---|
| 199 | console.error("Sysmon API error:", err);
|
|---|
| 200 |
|
|---|
| 201 | // Fallback - пробај да земеш Sysmon events од општиот endpoint
|
|---|
| 202 | fetch(`/api/computer/${encodeURIComponent(selectedComputer)}`)
|
|---|
| 203 | .then(r => r.json())
|
|---|
| 204 | .then(fullData => {
|
|---|
| 205 | if (fullData.sysmon_events) {
|
|---|
| 206 | // Конвертирај Sysmon events во процес формат
|
|---|
| 207 | const processes = fullData.sysmon_events.map((ev, idx) => ({
|
|---|
| 208 | process_id: ev.event_id,
|
|---|
| 209 | process_name: ev.event_type,
|
|---|
| 210 | timestamp: ev.timestamp,
|
|---|
| 211 | message: ev.message,
|
|---|
| 212 | details: ev.details || {}
|
|---|
| 213 | }));
|
|---|
| 214 |
|
|---|
| 215 | setSysmonData({
|
|---|
| 216 | computer_name: selectedComputer,
|
|---|
| 217 | processes: processes,
|
|---|
| 218 | count: processes.length
|
|---|
| 219 | });
|
|---|
| 220 | }
|
|---|
| 221 | setLoadingSysmon(false);
|
|---|
| 222 | })
|
|---|
| 223 | .catch(() => {
|
|---|
| 224 | setLoadingSysmon(false);
|
|---|
| 225 | });
|
|---|
| 226 | });
|
|---|
| 227 | };
|
|---|
| 228 | // Load Network connections
|
|---|
| 229 | const loadNetworkConnections = () => {
|
|---|
| 230 | if (!selectedComputer) return;
|
|---|
| 231 | setLoadingNetwork(true);
|
|---|
| 232 | setSelectedNetworkItem(null);
|
|---|
| 233 |
|
|---|
| 234 | // КОРИГИРАЈ ГО ОВА URL
|
|---|
| 235 | fetch(`/api/computer/${encodeURIComponent(selectedComputer)}/network`)
|
|---|
| 236 | .then((r) => {
|
|---|
| 237 | if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|---|
| 238 | return r.json();
|
|---|
| 239 | })
|
|---|
| 240 | .then((data) => {
|
|---|
| 241 | console.log("Network data received:", data);
|
|---|
| 242 |
|
|---|
| 243 | setNetworkData(data);
|
|---|
| 244 | setLoadingNetwork(false);
|
|---|
| 245 | })
|
|---|
| 246 | .catch((err) => {
|
|---|
| 247 | console.error("Network API error:", err);
|
|---|
| 248 |
|
|---|
| 249 | // Fallback - пробај да земеш network од општиот endpoint
|
|---|
| 250 | fetch(`/api/computer/${encodeURIComponent(selectedComputer)}`)
|
|---|
| 251 | .then(r => r.json())
|
|---|
| 252 | .then(fullData => {
|
|---|
| 253 | if (fullData.network_connections) {
|
|---|
| 254 | const connections = fullData.network_connections.map((conn, idx) => ({
|
|---|
| 255 | pid: conn.pid,
|
|---|
| 256 | process_name: conn.process_name,
|
|---|
| 257 | local_address: conn.local_address,
|
|---|
| 258 | remote_address: conn.remote_address,
|
|---|
| 259 | status: conn.status,
|
|---|
| 260 | timestamp: conn.timestamp,
|
|---|
| 261 | // Додади ги овие полиња за да match-не со очекуваниот формат
|
|---|
| 262 | local_port: conn.local_address?.split(':')?.[1] || '',
|
|---|
| 263 | remote_port: conn.remote_address?.split(':')?.[1] || '',
|
|---|
| 264 | protocol: 'TCP'
|
|---|
| 265 | }));
|
|---|
| 266 |
|
|---|
| 267 | setNetworkData({
|
|---|
| 268 | computer_name: selectedComputer,
|
|---|
| 269 | connections: connections,
|
|---|
| 270 | count: connections.length
|
|---|
| 271 | });
|
|---|
| 272 | }
|
|---|
| 273 | setLoadingNetwork(false);
|
|---|
| 274 | })
|
|---|
| 275 | .catch(() => {
|
|---|
| 276 | setLoadingNetwork(false);
|
|---|
| 277 | });
|
|---|
| 278 | });
|
|---|
| 279 | };
|
|---|
| 280 | const formatTime = (iso) => {
|
|---|
| 281 | if (!iso) return "-";
|
|---|
| 282 | const d = new Date(iso);
|
|---|
| 283 | return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|---|
| 284 | };
|
|---|
| 285 |
|
|---|
| 286 | const formatDate = (iso) => {
|
|---|
| 287 | if (!iso) return "-";
|
|---|
| 288 | const d = new Date(iso);
|
|---|
| 289 | return d.toLocaleDateString();
|
|---|
| 290 | };
|
|---|
| 291 |
|
|---|
| 292 | const askAssistant = async () => {
|
|---|
| 293 | if (!chatQuestion.trim()) return;
|
|---|
| 294 | const question = chatQuestion;
|
|---|
| 295 | setChatLoading(true);
|
|---|
| 296 | setChatQuestion("");
|
|---|
| 297 | setShowWelcome(false);
|
|---|
| 298 |
|
|---|
| 299 | const newHistory = [...chatHistory, {
|
|---|
| 300 | type: "question",
|
|---|
| 301 | content: question,
|
|---|
| 302 | computer: chatComputer,
|
|---|
| 303 | timestamp: new Date().toISOString()
|
|---|
| 304 | }];
|
|---|
| 305 | setChatHistory(newHistory);
|
|---|
| 306 |
|
|---|
| 307 | try {
|
|---|
| 308 | const res = await fetch("/api/chat", {
|
|---|
| 309 | method: "POST",
|
|---|
| 310 | headers: { "Content-Type": "application/json" },
|
|---|
| 311 | body: JSON.stringify({
|
|---|
| 312 | question: question,
|
|---|
| 313 | computer_name: chatComputer || null,
|
|---|
| 314 | }),
|
|---|
| 315 | });
|
|---|
| 316 |
|
|---|
| 317 | const answer = await res.json();
|
|---|
| 318 | const finalAnswer = answer.answer || answer.error || "I couldn't process that request.";
|
|---|
| 319 |
|
|---|
| 320 | setChatHistory([...newHistory, {
|
|---|
| 321 | type: "answer",
|
|---|
| 322 | content: finalAnswer,
|
|---|
| 323 | timestamp: new Date().toISOString()
|
|---|
| 324 | }]);
|
|---|
| 325 | } catch (err) {
|
|---|
| 326 | setChatHistory([...newHistory, {
|
|---|
| 327 | type: "answer",
|
|---|
| 328 | content: "⚠️ Connection error. Please check the server.",
|
|---|
| 329 | timestamp: new Date().toISOString()
|
|---|
| 330 | }]);
|
|---|
| 331 | } finally {
|
|---|
| 332 | setChatLoading(false);
|
|---|
| 333 | }
|
|---|
| 334 | };
|
|---|
| 335 |
|
|---|
| 336 | const resetFilters = () => {
|
|---|
| 337 | setFilters({
|
|---|
| 338 | status: "all",
|
|---|
| 339 | search: "",
|
|---|
| 340 | os: "all",
|
|---|
| 341 | minCpu: 0,
|
|---|
| 342 | minRam: 0
|
|---|
| 343 | });
|
|---|
| 344 | };
|
|---|
| 345 |
|
|---|
| 346 | const clearChat = () => {
|
|---|
| 347 | setChatHistory([]);
|
|---|
| 348 | setChatAnswer("");
|
|---|
| 349 | setShowWelcome(true);
|
|---|
| 350 | };
|
|---|
| 351 |
|
|---|
| 352 | const closeModal = () => {
|
|---|
| 353 | setSelectedComputer(null);
|
|---|
| 354 | setComputerDetails(null);
|
|---|
| 355 | setSysmonData(null);
|
|---|
| 356 | setNetworkData(null);
|
|---|
| 357 | setSelectedSysmonProcess(null);
|
|---|
| 358 | setSelectedNetworkItem(null);
|
|---|
| 359 | };
|
|---|
| 360 |
|
|---|
| 361 | const quickQuestions = [
|
|---|
| 362 | "Show security alerts from yesterday",
|
|---|
| 363 | "Which devices have high CPU usage?",
|
|---|
| 364 | "List suspicious network connections",
|
|---|
| 365 | "Show recent Sysmon events",
|
|---|
| 366 | "Display offline devices"
|
|---|
| 367 | ];
|
|---|
| 368 |
|
|---|
| 369 | return (
|
|---|
| 370 | <div className="app-container">
|
|---|
| 371 | {/* Main Content */}
|
|---|
| 372 | <main className="main-content">
|
|---|
| 373 | {/* Top Navigation */}
|
|---|
| 374 | <nav className="top-nav">
|
|---|
| 375 | <div className="nav-left">
|
|---|
| 376 | <div className="logo">
|
|---|
| 377 | <div className="logo-icon">🛡️</div>
|
|---|
| 378 | <div className="logo-text">
|
|---|
| 379 | <h1>LAN Sentinel</h1>
|
|---|
| 380 | <span className="logo-subtitle">Enterprise Security Dashboard</span>
|
|---|
| 381 | </div>
|
|---|
| 382 | </div>
|
|---|
| 383 | </div>
|
|---|
| 384 |
|
|---|
| 385 | <div className="nav-center">
|
|---|
| 386 | <div className="search-bar">
|
|---|
| 387 | <input
|
|---|
| 388 | type="text"
|
|---|
| 389 | placeholder="Search devices..."
|
|---|
| 390 | value={filters.search}
|
|---|
| 391 | onChange={(e) => setFilters({...filters, search: e.target.value})}
|
|---|
| 392 | />
|
|---|
| 393 | <span className="search-icon">🔍</span>
|
|---|
| 394 | </div>
|
|---|
| 395 | </div>
|
|---|
| 396 |
|
|---|
| 397 | <div className="nav-right">
|
|---|
| 398 | <button
|
|---|
| 399 | className={`ai-toggle-btn ${isChatOpen ? 'active' : ''}`}
|
|---|
| 400 | onClick={() => setIsChatOpen(!isChatOpen)}
|
|---|
| 401 | >
|
|---|
| 402 | <span className="ai-icon">🤖</span>
|
|---|
| 403 | <span>AI Assistant</span>
|
|---|
| 404 | </button>
|
|---|
| 405 | <button className="refresh-btn" onClick={loadComputers}>
|
|---|
| 406 | <span className="refresh-icon">↻</span>
|
|---|
| 407 | Refresh
|
|---|
| 408 | </button>
|
|---|
| 409 | <div className="user-profile">
|
|---|
| 410 | <div className="profile-avatar">AD</div>
|
|---|
| 411 | </div>
|
|---|
| 412 | </div>
|
|---|
| 413 | </nav>
|
|---|
| 414 |
|
|---|
| 415 | {/* Stats Overview */}
|
|---|
| 416 | <section className="stats-overview">
|
|---|
| 417 | <div className="stats-header">
|
|---|
| 418 | <h2>Network Overview</h2>
|
|---|
| 419 | <div className="stats-time">
|
|---|
| 420 | Last updated: {new Date().toLocaleTimeString()}
|
|---|
| 421 | </div>
|
|---|
| 422 | </div>
|
|---|
| 423 |
|
|---|
| 424 | <div className="stats-grid">
|
|---|
| 425 | <div className="stat-card">
|
|---|
| 426 | <div className="stat-main">
|
|---|
| 427 | <span className="stat-number">{computers.length}</span>
|
|---|
| 428 | <span className="stat-label">Total Devices</span>
|
|---|
| 429 | </div>
|
|---|
| 430 | <div className="stat-icon">🖥️</div>
|
|---|
| 431 | </div>
|
|---|
| 432 |
|
|---|
| 433 | <div className="stat-card online">
|
|---|
| 434 | <div className="stat-main">
|
|---|
| 435 | <span className="stat-number">{computers.filter(c => c.status === 'online').length}</span>
|
|---|
| 436 | <span className="stat-label">Online</span>
|
|---|
| 437 | </div>
|
|---|
| 438 | <div className="stat-trend">
|
|---|
| 439 | <span className="trend-up">↑</span>
|
|---|
| 440 | <span>{computers.length > 0 ? Math.round((computers.filter(c => c.status === 'online').length/computers.length)*100) : 0}%</span>
|
|---|
| 441 | </div>
|
|---|
| 442 | </div>
|
|---|
| 443 |
|
|---|
| 444 | <div className="stat-card idle">
|
|---|
| 445 | <div className="stat-main">
|
|---|
| 446 | <span className="stat-number">{computers.filter(c => c.status === 'idle').length}</span>
|
|---|
| 447 | <span className="stat-label">Idle</span>
|
|---|
| 448 | </div>
|
|---|
| 449 | <div className="stat-icon">⏸️</div>
|
|---|
| 450 | </div>
|
|---|
| 451 |
|
|---|
| 452 | <div className="stat-card offline">
|
|---|
| 453 | <div className="stat-main">
|
|---|
| 454 | <span className="stat-number">{computers.filter(c => c.status === 'offline').length}</span>
|
|---|
| 455 | <span className="stat-label">Offline</span>
|
|---|
| 456 | </div>
|
|---|
| 457 | <div className="stat-icon">🔴</div>
|
|---|
| 458 | </div>
|
|---|
| 459 |
|
|---|
| 460 | <div className="stat-card security">
|
|---|
| 461 | <div className="stat-main">
|
|---|
| 462 | <span className="stat-number">
|
|---|
| 463 | {stats ? stats.total_sysmon_events.toLocaleString() : "0"}
|
|---|
| 464 | </span>
|
|---|
| 465 | <span className="stat-label">Sysmon Events</span>
|
|---|
| 466 | </div>
|
|---|
| 467 | <div className="stat-badge">24h</div>
|
|---|
| 468 | </div>
|
|---|
| 469 |
|
|---|
| 470 | <div className="stat-card alerts">
|
|---|
| 471 | <div className="stat-main">
|
|---|
| 472 | <span className="stat-number">
|
|---|
| 473 | {stats ? stats.security_alerts || 0 : "0"}
|
|---|
| 474 | </span>
|
|---|
| 475 | <span className="stat-label">Alerts</span>
|
|---|
| 476 | </div>
|
|---|
| 477 | <div className="stat-icon">🚨</div>
|
|---|
| 478 | </div>
|
|---|
| 479 | </div>
|
|---|
| 480 | </section>
|
|---|
| 481 |
|
|---|
| 482 | {/* Main Dashboard */}
|
|---|
| 483 | <div className="dashboard-grid">
|
|---|
| 484 | {/* Left Sidebar - Filters */}
|
|---|
| 485 | <aside className="filter-sidebar">
|
|---|
| 486 | <div className="filter-section">
|
|---|
| 487 | <h3>Filters</h3>
|
|---|
| 488 | <div className="filter-group">
|
|---|
| 489 | <label>Status</label>
|
|---|
| 490 | <div className="filter-tags">
|
|---|
| 491 | {["all", "online", "idle", "offline"].map(status => (
|
|---|
| 492 | <button
|
|---|
| 493 | key={status}
|
|---|
| 494 | className={`filter-tag ${filters.status === status ? 'active' : ''}`}
|
|---|
| 495 | onClick={() => setFilters({...filters, status})}
|
|---|
| 496 | >
|
|---|
| 497 | {status === 'all' ? 'All' : status}
|
|---|
| 498 | </button>
|
|---|
| 499 | ))}
|
|---|
| 500 | </div>
|
|---|
| 501 | </div>
|
|---|
| 502 |
|
|---|
| 503 | <div className="filter-group">
|
|---|
| 504 | <label>Operating System</label>
|
|---|
| 505 | <select
|
|---|
| 506 | value={filters.os}
|
|---|
| 507 | onChange={(e) => setFilters({...filters, os: e.target.value})}
|
|---|
| 508 | className="filter-select"
|
|---|
| 509 | >
|
|---|
| 510 | <option value="all">All OS</option>
|
|---|
| 511 | {[...new Set(computers.map(c => c.os).filter(Boolean))].map(os => (
|
|---|
| 512 | <option key={os} value={os}>{os}</option>
|
|---|
| 513 | ))}
|
|---|
| 514 | </select>
|
|---|
| 515 | </div>
|
|---|
| 516 |
|
|---|
| 517 | <div className="filter-group">
|
|---|
| 518 | <label>CPU Usage ≥ {filters.minCpu}%</label>
|
|---|
| 519 | <input
|
|---|
| 520 | type="range"
|
|---|
| 521 | min="0"
|
|---|
| 522 | max="100"
|
|---|
| 523 | value={filters.minCpu}
|
|---|
| 524 | onChange={(e) => setFilters({...filters, minCpu: parseInt(e.target.value)})}
|
|---|
| 525 | className="range-slider"
|
|---|
| 526 | />
|
|---|
| 527 | </div>
|
|---|
| 528 |
|
|---|
| 529 | <div className="filter-group">
|
|---|
| 530 | <label>RAM Usage ≥ {filters.minRam}%</label>
|
|---|
| 531 | <input
|
|---|
| 532 | type="range"
|
|---|
| 533 | min="0"
|
|---|
| 534 | max="100"
|
|---|
| 535 | value={filters.minRam}
|
|---|
| 536 | onChange={(e) => setFilters({...filters, minRam: parseInt(e.target.value)})}
|
|---|
| 537 | className="range-slider"
|
|---|
| 538 | />
|
|---|
| 539 | </div>
|
|---|
| 540 |
|
|---|
| 541 | <button onClick={resetFilters} className="reset-filters-btn">
|
|---|
| 542 | Reset Filters
|
|---|
| 543 | </button>
|
|---|
| 544 | </div>
|
|---|
| 545 |
|
|---|
| 546 | <div className="filter-section">
|
|---|
| 547 | <h3>Quick Actions</h3>
|
|---|
| 548 | <button className="action-btn" onClick={loadComputers}>
|
|---|
| 549 | <span className="action-icon">↻</span>
|
|---|
| 550 | Refresh Data
|
|---|
| 551 | </button>
|
|---|
| 552 | <button className="action-btn" onClick={() => setIsChatOpen(true)}>
|
|---|
| 553 | <span className="action-icon">🤖</span>
|
|---|
| 554 | Open AI Assistant
|
|---|
| 555 | </button>
|
|---|
| 556 | </div>
|
|---|
| 557 | </aside>
|
|---|
| 558 |
|
|---|
| 559 | {/* Main Content Area */}
|
|---|
| 560 | <div className="content-area">
|
|---|
| 561 | <div className="content-header">
|
|---|
| 562 | <div>
|
|---|
| 563 | <h2>Network Devices</h2>
|
|---|
| 564 | <p className="content-subtitle">{filteredComputers.length} devices found</p>
|
|---|
| 565 | </div>
|
|---|
| 566 | <div className="header-actions">
|
|---|
| 567 | <select className="sort-select">
|
|---|
| 568 | <option>Last Seen</option>
|
|---|
| 569 | <option>Name</option>
|
|---|
| 570 | <option>CPU Usage</option>
|
|---|
| 571 | <option>Status</option>
|
|---|
| 572 | </select>
|
|---|
| 573 | </div>
|
|---|
| 574 | </div>
|
|---|
| 575 |
|
|---|
| 576 | {/* Devices Grid */}
|
|---|
| 577 | <div className="devices-grid">
|
|---|
| 578 | {filteredComputers.map((c) => (
|
|---|
| 579 | <div
|
|---|
| 580 | key={c.name}
|
|---|
| 581 | className={`device-card ${c.status}`}
|
|---|
| 582 | onClick={() => openComputer(c.name)}
|
|---|
| 583 | >
|
|---|
| 584 | <div className="device-header">
|
|---|
| 585 | <div className="device-title">
|
|---|
| 586 | <div className="device-avatar">
|
|---|
| 587 | {c.os?.includes("Windows") ? "🪟" :
|
|---|
| 588 | c.os?.includes("Linux") ? "🐧" :
|
|---|
| 589 | c.os?.includes("Mac") ? "🍎" : "💻"}
|
|---|
| 590 | </div>
|
|---|
| 591 | <div>
|
|---|
| 592 | <h3>{c.name}</h3>
|
|---|
| 593 | <p className="device-user">{c.user || "Unknown"}</p>
|
|---|
| 594 | </div>
|
|---|
| 595 | </div>
|
|---|
| 596 | <div className={`status-indicator ${c.status}`}>
|
|---|
| 597 | <div className="status-dot"></div>
|
|---|
| 598 | {c.status}
|
|---|
| 599 | </div>
|
|---|
| 600 | </div>
|
|---|
| 601 |
|
|---|
| 602 | <div className="device-info">
|
|---|
| 603 | <div className="info-row">
|
|---|
| 604 | <span className="info-label">IP</span>
|
|---|
| 605 | <span className="info-value">{c.ip}</span>
|
|---|
| 606 | </div>
|
|---|
| 607 | <div className="info-row">
|
|---|
| 608 | <span className="info-label">OS</span>
|
|---|
| 609 | <span className="info-value">{c.os}</span>
|
|---|
| 610 | </div>
|
|---|
| 611 | <div className="info-row">
|
|---|
| 612 | <span className="info-label">Last Active</span>
|
|---|
| 613 | <span className="info-value">{formatTime(c.last_seen)}</span>
|
|---|
| 614 | </div>
|
|---|
| 615 | </div>
|
|---|
| 616 |
|
|---|
| 617 | <div className="device-metrics">
|
|---|
| 618 | <div className="metric">
|
|---|
| 619 | <div className="metric-header">
|
|---|
| 620 | <span>CPU</span>
|
|---|
| 621 | <span className="metric-value">{c.avg_cpu || 0}%</span>
|
|---|
| 622 | </div>
|
|---|
| 623 | <div className="progress-bar">
|
|---|
| 624 | <div
|
|---|
| 625 | className="progress-fill cpu"
|
|---|
| 626 | style={{ width: `${Math.min(c.avg_cpu || 0, 100)}%` }}
|
|---|
| 627 | ></div>
|
|---|
| 628 | </div>
|
|---|
| 629 | </div>
|
|---|
| 630 | <div className="metric">
|
|---|
| 631 | <div className="metric-header">
|
|---|
| 632 | <span>Memory</span>
|
|---|
| 633 | <span className="metric-value">{c.avg_ram || 0}%</span>
|
|---|
| 634 | </div>
|
|---|
| 635 | <div className="progress-bar">
|
|---|
| 636 | <div
|
|---|
| 637 | className="progress-fill memory"
|
|---|
| 638 | style={{ width: `${Math.min(c.avg_ram || 0, 100)}%` }}
|
|---|
| 639 | ></div>
|
|---|
| 640 | </div>
|
|---|
| 641 | </div>
|
|---|
| 642 | </div>
|
|---|
| 643 |
|
|---|
| 644 | <div className="device-footer">
|
|---|
| 645 | {c.sysmon_available && (
|
|---|
| 646 | <div className="sysmon-badge">
|
|---|
| 647 | <span className="sysmon-icon">🛡️</span>
|
|---|
| 648 | {c.recent_sysmon || 0} events
|
|---|
| 649 | </div>
|
|---|
| 650 | )}
|
|---|
| 651 | </div>
|
|---|
| 652 | </div>
|
|---|
| 653 | ))}
|
|---|
| 654 |
|
|---|
| 655 | {filteredComputers.length === 0 && (
|
|---|
| 656 | <div className="empty-state">
|
|---|
| 657 | <div className="empty-icon">🔍</div>
|
|---|
| 658 | <h3>No devices match your filters</h3>
|
|---|
| 659 | <button onClick={resetFilters} className="empty-action-btn">
|
|---|
| 660 | Reset All Filters
|
|---|
| 661 | </button>
|
|---|
| 662 | </div>
|
|---|
| 663 | )}
|
|---|
| 664 | </div>
|
|---|
| 665 | </div>
|
|---|
| 666 | </div>
|
|---|
| 667 | </main>
|
|---|
| 668 |
|
|---|
| 669 | {/* AI Assistant Sidebar */}
|
|---|
| 670 | <div className={`ai-sidebar ${isChatOpen ? 'open' : ''}`}>
|
|---|
| 671 | <div className="ai-header">
|
|---|
| 672 | <div className="ai-title">
|
|---|
| 673 | <div className="ai-avatar">🤖</div>
|
|---|
| 674 | <div>
|
|---|
| 675 | <h3>Security AI Assistant</h3>
|
|---|
| 676 | <p className="ai-subtitle">Powered by LAN Sentinel</p>
|
|---|
| 677 | </div>
|
|---|
| 678 | </div>
|
|---|
| 679 | <button className="close-ai" onClick={() => setIsChatOpen(false)}>
|
|---|
| 680 | ✕
|
|---|
| 681 | </button>
|
|---|
| 682 | </div>
|
|---|
| 683 |
|
|---|
| 684 | <div className="ai-content">
|
|---|
| 685 | <div className="chat-container">
|
|---|
| 686 | <div className="chat-history">
|
|---|
| 687 | {showWelcome && (
|
|---|
| 688 | <div className="welcome-message">
|
|---|
| 689 | <div className="welcome-avatar">🤖</div>
|
|---|
| 690 | <div className="welcome-content">
|
|---|
| 691 | <h4>Welcome to Security AI Assistant!</h4>
|
|---|
| 692 | <p>I can help you analyze security events, monitor network activity, and investigate incidents.</p>
|
|---|
| 693 | <div className="welcome-questions">
|
|---|
| 694 | {quickQuestions.map((q, idx) => (
|
|---|
| 695 | <button
|
|---|
| 696 | key={idx}
|
|---|
| 697 | className="quick-question"
|
|---|
| 698 | onClick={() => {
|
|---|
| 699 | setChatQuestion(q);
|
|---|
| 700 | setTimeout(() => askAssistant(), 100);
|
|---|
| 701 | }}
|
|---|
| 702 | >
|
|---|
| 703 | {q}
|
|---|
| 704 | </button>
|
|---|
| 705 | ))}
|
|---|
| 706 | </div>
|
|---|
| 707 | </div>
|
|---|
| 708 | </div>
|
|---|
| 709 | )}
|
|---|
| 710 |
|
|---|
| 711 | {chatHistory.map((msg, idx) => (
|
|---|
| 712 | <div key={idx} className={`chat-message ${msg.type}`}>
|
|---|
| 713 | <div className="message-avatar">
|
|---|
| 714 | {msg.type === 'question' ? '👤' : '🤖'}
|
|---|
| 715 | </div>
|
|---|
| 716 | <div className="message-content">
|
|---|
| 717 | <div className="message-header">
|
|---|
| 718 | <span className="message-sender">
|
|---|
| 719 | {msg.type === 'question' ? 'You' : 'Security AI'}
|
|---|
| 720 | </span>
|
|---|
| 721 | {msg.computer && (
|
|---|
| 722 | <span className="message-context">
|
|---|
| 723 | Computer: {msg.computer}
|
|---|
| 724 | </span>
|
|---|
| 725 | )}
|
|---|
| 726 | </div>
|
|---|
| 727 | <div className="message-text">
|
|---|
| 728 | {msg.content}
|
|---|
| 729 | </div>
|
|---|
| 730 | <div className="message-time">
|
|---|
| 731 | {new Date(msg.timestamp).toLocaleTimeString([], {
|
|---|
| 732 | hour: '2-digit',
|
|---|
| 733 | minute: '2-digit'
|
|---|
| 734 | })}
|
|---|
| 735 | </div>
|
|---|
| 736 | </div>
|
|---|
| 737 | </div>
|
|---|
| 738 | ))}
|
|---|
| 739 |
|
|---|
| 740 | {chatLoading && (
|
|---|
| 741 | <div className="chat-message answer">
|
|---|
| 742 | <div className="message-avatar">🤖</div>
|
|---|
| 743 | <div className="message-content">
|
|---|
| 744 | <div className="typing-indicator">
|
|---|
| 745 | <div></div>
|
|---|
| 746 | <div></div>
|
|---|
| 747 | <div></div>
|
|---|
| 748 | </div>
|
|---|
| 749 | </div>
|
|---|
| 750 | </div>
|
|---|
| 751 | )}
|
|---|
| 752 | </div>
|
|---|
| 753 |
|
|---|
| 754 | <div className="chat-input-area">
|
|---|
| 755 | <select
|
|---|
| 756 | value={chatComputer}
|
|---|
| 757 | onChange={(e) => setChatComputer(e.target.value)}
|
|---|
| 758 | className="chat-select"
|
|---|
| 759 | >
|
|---|
| 760 | <option value="">🌐 All Network</option>
|
|---|
| 761 | {computers.map((c) => (
|
|---|
| 762 | <option key={c.name} value={c.name}>
|
|---|
| 763 | {c.name}
|
|---|
| 764 | </option>
|
|---|
| 765 | ))}
|
|---|
| 766 | </select>
|
|---|
| 767 |
|
|---|
| 768 | <div className="input-wrapper">
|
|---|
| 769 | <input
|
|---|
| 770 | type="text"
|
|---|
| 771 | placeholder="Ask about security events..."
|
|---|
| 772 | value={chatQuestion}
|
|---|
| 773 | onChange={(e) => setChatQuestion(e.target.value)}
|
|---|
| 774 | onKeyDown={(e) => e.key === 'Enter' && askAssistant()}
|
|---|
| 775 | className="chat-input"
|
|---|
| 776 | />
|
|---|
| 777 | <button
|
|---|
| 778 | onClick={askAssistant}
|
|---|
| 779 | disabled={chatLoading || !chatQuestion.trim()}
|
|---|
| 780 | className="send-btn"
|
|---|
| 781 | >
|
|---|
| 782 | {chatLoading ? '...' : '➤'}
|
|---|
| 783 | </button>
|
|---|
| 784 | </div>
|
|---|
| 785 | </div>
|
|---|
| 786 | </div>
|
|---|
| 787 | </div>
|
|---|
| 788 | </div>
|
|---|
| 789 |
|
|---|
| 790 | {/* Enhanced Computer Details Modal */}
|
|---|
| 791 | {selectedComputer && (
|
|---|
| 792 | <div className="modal-overlay" onClick={closeModal}>
|
|---|
| 793 | <div className="modal large" onClick={(e) => e.stopPropagation()}>
|
|---|
| 794 | <div className="modal-header">
|
|---|
| 795 | <div className="modal-title">
|
|---|
| 796 | <h2>{selectedComputer}</h2>
|
|---|
| 797 | <div className="modal-subtitle">
|
|---|
| 798 | <span className="ip-badge">{computerDetails?.computer?.ip}</span>
|
|---|
| 799 | <span className="os-badge">{computerDetails?.computer?.os}</span>
|
|---|
| 800 | <span className={`status-badge ${computerDetails?.computer?.status}`}>
|
|---|
| 801 | {computerDetails?.computer?.status}
|
|---|
| 802 | </span>
|
|---|
| 803 | </div>
|
|---|
| 804 | </div>
|
|---|
| 805 | <div className="modal-actions">
|
|---|
| 806 | <button className="modal-btn">
|
|---|
| 807 | <span className="btn-icon">📊</span>
|
|---|
| 808 | Report
|
|---|
| 809 | </button>
|
|---|
| 810 | <button className="modal-close" onClick={closeModal}>
|
|---|
| 811 | ✕
|
|---|
| 812 | </button>
|
|---|
| 813 | </div>
|
|---|
| 814 | </div>
|
|---|
| 815 |
|
|---|
| 816 | <div className="modal-content">
|
|---|
| 817 | {loadingDetails ? (
|
|---|
| 818 | <div className="loading-state">
|
|---|
| 819 | <div className="loader"></div>
|
|---|
| 820 | <p>Loading device details...</p>
|
|---|
| 821 | </div>
|
|---|
| 822 | ) : (
|
|---|
| 823 | <>
|
|---|
| 824 | {/* Quick Stats */}
|
|---|
| 825 | <div className="quick-stats">
|
|---|
| 826 | <div className="quick-stat">
|
|---|
| 827 | <div className="stat-icon">⚡</div>
|
|---|
| 828 | <div className="stat-info">
|
|---|
| 829 | <div className="stat-value">
|
|---|
| 830 | {computerDetails?.current_metrics?.cpu_usage || 0}%
|
|---|
| 831 | </div>
|
|---|
| 832 | <div className="stat-label">CPU</div>
|
|---|
| 833 | </div>
|
|---|
| 834 | </div>
|
|---|
| 835 | <div className="quick-stat">
|
|---|
| 836 | <div className="stat-icon">💾</div>
|
|---|
| 837 | <div className="stat-info">
|
|---|
| 838 | <div className="stat-value">
|
|---|
| 839 | {computerDetails?.current_metrics?.ram_usage || 0}%
|
|---|
| 840 | </div>
|
|---|
| 841 | <div className="stat-label">RAM</div>
|
|---|
| 842 | </div>
|
|---|
| 843 | </div>
|
|---|
| 844 | <div className="quick-stat">
|
|---|
| 845 | <div className="stat-icon">📡</div>
|
|---|
| 846 | <div className="stat-info">
|
|---|
| 847 | <div className="stat-value">
|
|---|
| 848 | {computerDetails?.current_metrics?.network_usage || 0}%
|
|---|
| 849 | </div>
|
|---|
| 850 | <div className="stat-label">Network</div>
|
|---|
| 851 | </div>
|
|---|
| 852 | </div>
|
|---|
| 853 | <div className="quick-stat">
|
|---|
| 854 | <div className="stat-icon">🛡️</div>
|
|---|
| 855 | <div className="stat-info">
|
|---|
| 856 | <div className="stat-value">
|
|---|
| 857 | {computerDetails?.recent_sysmon || 0}
|
|---|
| 858 | </div>
|
|---|
| 859 | <div className="stat-label">Sysmon Events</div>
|
|---|
| 860 | </div>
|
|---|
| 861 | </div>
|
|---|
| 862 | </div>
|
|---|
| 863 |
|
|---|
| 864 | {/* Tabs */}
|
|---|
| 865 | <div className="modal-tabs">
|
|---|
| 866 | <div className="tabs-header">
|
|---|
| 867 | <button
|
|---|
| 868 | className={`tab ${activeTab === 'security' ? 'active' : ''}`}
|
|---|
| 869 | onClick={() => setActiveTab('security')}
|
|---|
| 870 | >
|
|---|
| 871 | Security Events
|
|---|
| 872 | </button>
|
|---|
| 873 | <button
|
|---|
| 874 | className={`tab ${activeTab === 'sysmon' ? 'active' : ''}`}
|
|---|
| 875 | onClick={() => {
|
|---|
| 876 | setActiveTab('sysmon');
|
|---|
| 877 | if (!sysmonData) loadSysmonProcesses();
|
|---|
| 878 | }}
|
|---|
| 879 | >
|
|---|
| 880 | Sysmon Processes
|
|---|
| 881 | </button>
|
|---|
| 882 | <button
|
|---|
| 883 | className={`tab ${activeTab === 'network' ? 'active' : ''}`}
|
|---|
| 884 | onClick={() => {
|
|---|
| 885 | setActiveTab('network');
|
|---|
| 886 | if (!networkData) loadNetworkConnections();
|
|---|
| 887 | }}
|
|---|
| 888 | >
|
|---|
| 889 | Network Connections
|
|---|
| 890 | </button>
|
|---|
| 891 | <button
|
|---|
| 892 | className={`tab ${activeTab === 'details' ? 'active' : ''}`}
|
|---|
| 893 | onClick={() => setActiveTab('details')}
|
|---|
| 894 | >
|
|---|
| 895 | System Details
|
|---|
| 896 | </button>
|
|---|
| 897 | </div>
|
|---|
| 898 |
|
|---|
| 899 | <div className="tabs-content">
|
|---|
| 900 | {/* Security Events Tab */}
|
|---|
| 901 | {activeTab === 'security' && (
|
|---|
| 902 | <div className="tab-pane active">
|
|---|
| 903 | <div className="data-table-container">
|
|---|
| 904 | <table className="data-table">
|
|---|
| 905 | <thead>
|
|---|
| 906 | <tr>
|
|---|
| 907 | <th>Time</th>
|
|---|
| 908 | <th>Event ID</th>
|
|---|
| 909 | <th>Type</th>
|
|---|
| 910 | <th>Description</th>
|
|---|
| 911 | </tr>
|
|---|
| 912 | </thead>
|
|---|
| 913 | <tbody>
|
|---|
| 914 | {(computerDetails?.sysmon_events || []).map((ev, idx) => (
|
|---|
| 915 | <tr key={idx}>
|
|---|
| 916 | <td>{formatTime(ev.timestamp)}</td>
|
|---|
| 917 | <td>
|
|---|
| 918 | <span className="event-id">#{ev.event_id}</span>
|
|---|
| 919 | </td>
|
|---|
| 920 | <td>
|
|---|
| 921 | <span className={`event-type ${ev.event_type?.toLowerCase()}`}>
|
|---|
| 922 | {ev.event_type}
|
|---|
| 923 | </span>
|
|---|
| 924 | </td>
|
|---|
| 925 | <td className="event-desc">
|
|---|
| 926 | {ev.message?.slice(0, 100) || 'No description'}
|
|---|
| 927 | </td>
|
|---|
| 928 | </tr>
|
|---|
| 929 | ))}
|
|---|
| 930 | </tbody>
|
|---|
| 931 | </table>
|
|---|
| 932 | </div>
|
|---|
| 933 | </div>
|
|---|
| 934 | )}
|
|---|
| 935 |
|
|---|
| 936 | {/* Sysmon Processes Tab */}
|
|---|
| 937 | {activeTab === 'sysmon' && (
|
|---|
| 938 | <div className="tab-pane active">
|
|---|
| 939 | <div className="pane-header">
|
|---|
| 940 | <h3>Sysmon Processes</h3>
|
|---|
| 941 | <button
|
|---|
| 942 | onClick={loadSysmonProcesses}
|
|---|
| 943 | className="refresh-btn-small"
|
|---|
| 944 | disabled={loadingSysmon}
|
|---|
| 945 | >
|
|---|
| 946 | {loadingSysmon ? 'Loading...' : '↻ Refresh'}
|
|---|
| 947 | </button>
|
|---|
| 948 | </div>
|
|---|
| 949 |
|
|---|
| 950 | {loadingSysmon ? (
|
|---|
| 951 | <div className="loading-state">
|
|---|
| 952 | <div className="loader"></div>
|
|---|
| 953 | <p>Loading Sysmon processes...</p>
|
|---|
| 954 | </div>
|
|---|
| 955 | ) : sysmonData ? (
|
|---|
| 956 | <div className="data-grid">
|
|---|
| 957 | <div className="grid-container">
|
|---|
| 958 | {sysmonData.processes && sysmonData.processes.length > 0 ? (
|
|---|
| 959 | <>
|
|---|
| 960 | <div className="grid-sidebar">
|
|---|
| 961 | <div className="process-list">
|
|---|
| 962 | {sysmonData.processes.map((proc, idx) => (
|
|---|
| 963 | <div
|
|---|
| 964 | key={idx}
|
|---|
| 965 | className={`process-item ${selectedSysmonProcess === idx ? 'active' : ''}`}
|
|---|
| 966 | onClick={() => setSelectedSysmonProcess(idx)}
|
|---|
| 967 | >
|
|---|
| 968 | <div className="process-name">
|
|---|
| 969 | <strong>{proc.process_name || 'Unknown'}</strong>
|
|---|
| 970 | </div>
|
|---|
| 971 | <div className="process-pid">
|
|---|
| 972 | PID: {proc.process_id}
|
|---|
| 973 | </div>
|
|---|
| 974 | <div className="process-time">
|
|---|
| 975 | {formatTime(proc.timestamp)}
|
|---|
| 976 | </div>
|
|---|
| 977 | </div>
|
|---|
| 978 | ))}
|
|---|
| 979 | </div>
|
|---|
| 980 | </div>
|
|---|
| 981 | <div className="grid-content">
|
|---|
| 982 | {selectedSysmonProcess !== null ? (
|
|---|
| 983 | <div className="json-viewer">
|
|---|
| 984 | <div className="json-header">
|
|---|
| 985 | <h4>Process Details (JSON)</h4>
|
|---|
| 986 | <button
|
|---|
| 987 | className="copy-json"
|
|---|
| 988 | onClick={() => navigator.clipboard.writeText(
|
|---|
| 989 | JSON.stringify(sysmonData.processes[selectedSysmonProcess], null, 2)
|
|---|
| 990 | )}
|
|---|
| 991 | >
|
|---|
| 992 | 📋 Copy JSON
|
|---|
| 993 | </button>
|
|---|
| 994 | </div>
|
|---|
| 995 | <pre className="json-content">
|
|---|
| 996 | {JSON.stringify(sysmonData.processes[selectedSysmonProcess], null, 2)}
|
|---|
| 997 | </pre>
|
|---|
| 998 | </div>
|
|---|
| 999 | ) : (
|
|---|
| 1000 | <div className="select-prompt">
|
|---|
| 1001 | <div className="prompt-icon">👈</div>
|
|---|
| 1002 | <p>Select a process from the list to view its JSON data</p>
|
|---|
| 1003 | </div>
|
|---|
| 1004 | )}
|
|---|
| 1005 | </div>
|
|---|
| 1006 | </>
|
|---|
| 1007 | ) : (
|
|---|
| 1008 | <div className="empty-data">
|
|---|
| 1009 | <div className="empty-icon">📄</div>
|
|---|
| 1010 | <p>No Sysmon processes found</p>
|
|---|
| 1011 | </div>
|
|---|
| 1012 | )}
|
|---|
| 1013 | </div>
|
|---|
| 1014 | </div>
|
|---|
| 1015 | ) : (
|
|---|
| 1016 | <div className="empty-data">
|
|---|
| 1017 | <p>Click refresh to load Sysmon processes</p>
|
|---|
| 1018 | <button onClick={loadSysmonProcesses} className="load-btn">
|
|---|
| 1019 | Load Sysmon Data
|
|---|
| 1020 | </button>
|
|---|
| 1021 | </div>
|
|---|
| 1022 | )}
|
|---|
| 1023 | </div>
|
|---|
| 1024 | )}
|
|---|
| 1025 |
|
|---|
| 1026 | {/* Network Connections Tab */}
|
|---|
| 1027 | {activeTab === 'network' && (
|
|---|
| 1028 | <div className="tab-pane active">
|
|---|
| 1029 | <div className="pane-header">
|
|---|
| 1030 | <h3>Network Connections</h3>
|
|---|
| 1031 | <button
|
|---|
| 1032 | onClick={loadNetworkConnections}
|
|---|
| 1033 | className="refresh-btn-small"
|
|---|
| 1034 | disabled={loadingNetwork}
|
|---|
| 1035 | >
|
|---|
| 1036 | {loadingNetwork ? 'Loading...' : '↻ Refresh'}
|
|---|
| 1037 | </button>
|
|---|
| 1038 | </div>
|
|---|
| 1039 |
|
|---|
| 1040 | {loadingNetwork ? (
|
|---|
| 1041 | <div className="loading-state">
|
|---|
| 1042 | <div className="loader"></div>
|
|---|
| 1043 | <p>Loading network connections...</p>
|
|---|
| 1044 | </div>
|
|---|
| 1045 | ) : networkData ? (
|
|---|
| 1046 | <div className="data-grid">
|
|---|
| 1047 | <div className="grid-container">
|
|---|
| 1048 | {networkData.connections && networkData.connections.length > 0 ? (
|
|---|
| 1049 | <>
|
|---|
| 1050 | <div className="grid-sidebar">
|
|---|
| 1051 | <div className="network-list">
|
|---|
| 1052 | {networkData.connections.map((conn, idx) => (
|
|---|
| 1053 | <div
|
|---|
| 1054 | key={idx}
|
|---|
| 1055 | className={`network-item ${selectedNetworkItem === idx ? 'active' : ''}`}
|
|---|
| 1056 | onClick={() => setSelectedNetworkItem(idx)}
|
|---|
| 1057 | >
|
|---|
| 1058 | <div className="network-address">
|
|---|
| 1059 | <strong>{conn.local_address}:{conn.local_port}</strong>
|
|---|
| 1060 | <div className="arrow">→</div>
|
|---|
| 1061 | <strong>{conn.remote_address}:{conn.remote_port}</strong>
|
|---|
| 1062 | </div>
|
|---|
| 1063 | <div className="network-protocol">
|
|---|
| 1064 | {conn.protocol} • {conn.state}
|
|---|
| 1065 | </div>
|
|---|
| 1066 | </div>
|
|---|
| 1067 | ))}
|
|---|
| 1068 | </div>
|
|---|
| 1069 | </div>
|
|---|
| 1070 | <div className="grid-content">
|
|---|
| 1071 | {selectedNetworkItem !== null ? (
|
|---|
| 1072 | <div className="json-viewer">
|
|---|
| 1073 | <div className="json-header">
|
|---|
| 1074 | <h4>Connection Details (JSON)</h4>
|
|---|
| 1075 | <button
|
|---|
| 1076 | className="copy-json"
|
|---|
| 1077 | onClick={() => navigator.clipboard.writeText(
|
|---|
| 1078 | JSON.stringify(networkData.connections[selectedNetworkItem], null, 2)
|
|---|
| 1079 | )}
|
|---|
| 1080 | >
|
|---|
| 1081 | 📋 Copy JSON
|
|---|
| 1082 | </button>
|
|---|
| 1083 | </div>
|
|---|
| 1084 | <pre className="json-content">
|
|---|
| 1085 | {JSON.stringify(networkData.connections[selectedNetworkItem], null, 2)}
|
|---|
| 1086 | </pre>
|
|---|
| 1087 | </div>
|
|---|
| 1088 | ) : (
|
|---|
| 1089 | <div className="select-prompt">
|
|---|
| 1090 | <div className="prompt-icon">👈</div>
|
|---|
| 1091 | <p>Select a connection to view its JSON data</p>
|
|---|
| 1092 | </div>
|
|---|
| 1093 | )}
|
|---|
| 1094 | </div>
|
|---|
| 1095 | </>
|
|---|
| 1096 | ) : (
|
|---|
| 1097 | <div className="empty-data">
|
|---|
| 1098 | <div className="empty-icon">🌐</div>
|
|---|
| 1099 | <p>No network connections found</p>
|
|---|
| 1100 | </div>
|
|---|
| 1101 | )}
|
|---|
| 1102 | </div>
|
|---|
| 1103 | </div>
|
|---|
| 1104 | ) : (
|
|---|
| 1105 | <div className="empty-data">
|
|---|
| 1106 | <p>Click refresh to load network connections</p>
|
|---|
| 1107 | <button onClick={loadNetworkConnections} className="load-btn">
|
|---|
| 1108 | Load Network Data
|
|---|
| 1109 | </button>
|
|---|
| 1110 | </div>
|
|---|
| 1111 | )}
|
|---|
| 1112 | </div>
|
|---|
| 1113 | )}
|
|---|
| 1114 |
|
|---|
| 1115 | {/* System Details Tab */}
|
|---|
| 1116 | {activeTab === 'details' && computerDetails && (
|
|---|
| 1117 | <div className="tab-pane active">
|
|---|
| 1118 | <div className="details-grid">
|
|---|
| 1119 | <div className="detail-group">
|
|---|
| 1120 | <h4>System Information</h4>
|
|---|
| 1121 | <div className="detail-item">
|
|---|
| 1122 | <span className="detail-label">Hostname:</span>
|
|---|
| 1123 | <span className="detail-value">{computerDetails.computer.name}</span>
|
|---|
| 1124 | </div>
|
|---|
| 1125 | <div className="detail-item">
|
|---|
| 1126 | <span className="detail-label">IP Address:</span>
|
|---|
| 1127 | <span className="detail-value">{computerDetails.computer.ip}</span>
|
|---|
| 1128 | </div>
|
|---|
| 1129 | <div className="detail-item">
|
|---|
| 1130 | <span className="detail-label">OS Version:</span>
|
|---|
| 1131 | <span className="detail-value">{computerDetails.computer.os}</span>
|
|---|
| 1132 | </div>
|
|---|
| 1133 | <div className="detail-item">
|
|---|
| 1134 | <span className="detail-label">Last Seen:</span>
|
|---|
| 1135 | <span className="detail-value">{formatTime(computerDetails.computer.last_seen)}</span>
|
|---|
| 1136 | </div>
|
|---|
| 1137 | </div>
|
|---|
| 1138 |
|
|---|
| 1139 | <div className="detail-group">
|
|---|
| 1140 | <h4>Current Metrics</h4>
|
|---|
| 1141 | <div className="detail-item">
|
|---|
| 1142 | <span className="detail-label">CPU Usage:</span>
|
|---|
| 1143 | <span className="detail-value">{computerDetails.current_metrics?.cpu_usage || 0}%</span>
|
|---|
| 1144 | </div>
|
|---|
| 1145 | <div className="detail-item">
|
|---|
| 1146 | <span className="detail-label">Memory Usage:</span>
|
|---|
| 1147 | <span className="detail-value">{computerDetails.current_metrics?.ram_usage || 0}%</span>
|
|---|
| 1148 | </div>
|
|---|
| 1149 | <div className="detail-item">
|
|---|
| 1150 | <span className="detail-label">Disk Usage:</span>
|
|---|
| 1151 | <span className="detail-value">{computerDetails.current_metrics?.disk_usage || 0}%</span>
|
|---|
| 1152 | </div>
|
|---|
| 1153 | </div>
|
|---|
| 1154 | </div>
|
|---|
| 1155 | </div>
|
|---|
| 1156 | )}
|
|---|
| 1157 | </div>
|
|---|
| 1158 | </div>
|
|---|
| 1159 | </>
|
|---|
| 1160 | )}
|
|---|
| 1161 | </div>
|
|---|
| 1162 | </div>
|
|---|
| 1163 | </div>
|
|---|
| 1164 | )}
|
|---|
| 1165 | </div>
|
|---|
| 1166 | );
|
|---|
| 1167 | }
|
|---|
| 1168 |
|
|---|
| 1169 | export default App; |
|---|