Changeset 2058e5c for lan-frontend/src/App.jsx
- Timestamp:
- 01/20/26 17:28:10 (6 months ago)
- Branches:
- master
- Children:
- 505f39a
- Parents:
- 640ed89
- File:
-
- 1 edited
-
lan-frontend/src/App.jsx (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
lan-frontend/src/App.jsx
r640ed89 r2058e5c 1 import React, { useEffect, useState } from "react"; 2 import './App.css'; 1 import React from "react"; 2 import {useEffect, useMemo, useState} from "react"; 3 import "./styles/theme.css"; 3 4 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 }; 5 import TopBar from "./components/TopBar"; 6 import ComputerCard from "./components/ComputerCard"; 7 import ComputerDetailsModal from "./components/ComputerDetailsModal"; 8 import AIAssistantDrawer from "./components/AIAssistantDrawer"; 25 9 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); 10 export default function App() { 11 const [stats, setStats] = useState(null); 12 const [computers, setComputers] = useState([]); 13 const [loading, setLoading] = useState(true); 14 const [lastUpdated, setLastUpdated] = useState(null); 36 15 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); 16 const [query, setQuery] = useState(""); 17 const [status, setStatus] = useState(""); 46 18 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); 19 const [selectedComputer, setSelectedComputer] = useState(null); 20 const [aiOpen, setAiOpen] = useState(false); 21 const [aiScope, setAiScope] = useState(null); 22 const [selectedEnv, setSelectedEnv] = useState("default"); 54 23 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 }; 24 async function refresh() { 25 try { 26 setLoading(true); 27 const [s, c] = await Promise.all([ 28 fetch("/api/stats").then((r) => r.json()), 29 fetch("/api/computers", { 30 headers: {"X-Env": selectedEnv}, 31 }).then((r) => r.json()), 32 ]); 33 setStats(s); 34 setComputers(Array.isArray(c) ? c : []); 35 setLastUpdated(new Date().toISOString()); 36 } catch (e) { 37 console.error("refresh error", e); 38 } finally { 39 setLoading(false); 40 } 41 } 65 42 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 }); 43 useEffect(() => { 44 refresh(); 45 }, [selectedEnv]); 80 46 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); 47 const filtered = useMemo(() => { 48 const q = query.trim().toLowerCase(); 49 return (computers || []) 50 .filter((c) => (status ? c.status === status : true)) 51 .filter((c) => { 52 if (!q) return true; 53 const hay = [c.name, c.user, c.ip, c.os, c.status].filter(Boolean).join(" ").toLowerCase(); 54 return hay.includes(q); 55 }); 56 }, [computers, query, status]); 89 57 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); 58 return ( 59 <div className="app-wrap"> 60 <TopBar 61 onOpenAI={() => setAiOpen(true)} 62 onRefresh={refresh} 63 lastUpdated={lastUpdated} 64 onEnvChange={(env) => setSelectedEnv(env)} 65 selectedEnv={selectedEnv} 66 /> 98 67 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 }, []); 68 {/* STATS */} 69 <div className="stats-grid"> 70 <div className="stat-card"> 71 <div className="label">Total Computers</div> 72 <div className="value">{computers.length}</div> 73 <div className="sub">Monitored Systems</div> 74 </div> 109 75 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]); 76 <div className="stat-card"> 77 <div className="label">Sysmon Events (24h)</div> 78 <div className="value">{stats?.total_sysmon_events ?? "—"}</div> 79 <div className="sub">Security Events</div> 80 </div> 135 81 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> 82 <div className="stat-card"> 83 <div className="label">Network Connections (24h)</div> 84 <div className="value">{stats?.total_network_connections ?? "—"}</div> 85 <div className="sub">Captured Connections</div> 86 </div> 431 87 </div> 432 88 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> 89 {/* FILTERS */} 90 <div className="panel" style={{marginBottom: 14}}> 91 <div className="panel-head"> 92 <div className="panel-title">Filters</div> 93 <div style={{color: "var(--dim)", fontSize: 12}}> 94 {loading ? "Refreshing…" : `${filtered.length} shown`} 95 </div> 96 </div> 97 <div className="panel-body"> 98 <div className="filters"> 99 <input 100 className="input" 101 value={query} 102 onChange={(e) => setQuery(e.target.value)} 103 placeholder="Search by name / user / ip / os…" 104 /> 105 106 <select className="select" value={status} onChange={(e) => setStatus(e.target.value)}> 107 <option value="">All status</option> 108 <option value="online">Online</option> 109 <option value="idle">Idle</option> 110 <option value="offline">Offline</option> 111 </select> 112 113 <button className="btn primary" onClick={refresh}>Refresh</button> 114 </div> 115 </div> 442 116 </div> 443 117 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> 118 {/* COMPUTERS */} 119 <div className="panel"> 120 <div className="panel-head"> 121 <div className="panel-title">Connected Computers</div> 122 <div style={{display: "flex", gap: 10, alignItems: "center"}}> 123 <span className="pill">{filtered.length} computers</span> 124 <button className="btn" onClick={() => { 125 setQuery(""); 126 setStatus(""); 127 }}>Reset 128 </button> 129 </div> 130 </div> 131 <div className="panel-body"> 132 <div className="computers-grid"> 133 {filtered.map((c) => ( 134 <ComputerCard 135 key={c.name} 136 c={c} 137 onOpen={(pc) => { 138 setSelectedComputer(pc); 139 setAiScope(pc.name); 140 }} 141 /> 142 ))} 143 {filtered.length === 0 && ( 144 <div style={{color: "var(--dim)"}}>No computers match filter.</div> 145 )} 146 </div> 147 </div> 450 148 </div> 451 149 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> 150 {/* DETAILS MODAL */} 151 <ComputerDetailsModal 152 open={!!selectedComputer} 153 computer={selectedComputer} 154 onClose={() => setSelectedComputer(null)} 155 onAskAI={(name) => { 156 setAiScope(name); 157 setAiOpen(true); 158 }} 159 /> 459 160 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> 161 {/* AI DRAWER */} 162 <AIAssistantDrawer 163 open={aiOpen} 164 onClose={() => setAiOpen(false)} 165 computers={computers} 166 scope={aiScope} 167 setScope={setAiScope} 168 /> 666 169 </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 ); 170 ); 1167 171 } 1168 1169 export default App;
Note:
See TracChangeset
for help on using the changeset viewer.
