| 1 | import React, { useEffect, useMemo, useState } from "react";
|
|---|
| 2 |
|
|---|
| 3 | function severityForEventId(eventId) {
|
|---|
| 4 | const id = parseInt(eventId, 10);
|
|---|
| 5 | const high = new Set([1, 3, 8, 10]);
|
|---|
| 6 | const med = new Set([5, 6, 7, 11, 22]);
|
|---|
| 7 | if (high.has(id)) return "high";
|
|---|
| 8 | if (med.has(id)) return "medium";
|
|---|
| 9 | return "low";
|
|---|
| 10 | }
|
|---|
| 11 |
|
|---|
| 12 | export default function ComputerDetailsModal({ open, computer, onClose, onAskAI }) {
|
|---|
| 13 | const [tab, setTab] = useState("overview");
|
|---|
| 14 | const [loading, setLoading] = useState(false);
|
|---|
| 15 | const [details, setDetails] = useState(null);
|
|---|
| 16 | const [search, setSearch] = useState("");
|
|---|
| 17 |
|
|---|
| 18 | // JSON viewer state
|
|---|
| 19 | const [jsonOpen, setJsonOpen] = useState(false);
|
|---|
| 20 | const [jsonTitle, setJsonTitle] = useState("");
|
|---|
| 21 | const [jsonData, setJsonData] = useState(null);
|
|---|
| 22 |
|
|---|
| 23 | useEffect(() => {
|
|---|
| 24 | if (!open || !computer?.name) return;
|
|---|
| 25 | let cancelled = false;
|
|---|
| 26 |
|
|---|
| 27 | async function load() {
|
|---|
| 28 | try {
|
|---|
| 29 | setLoading(true);
|
|---|
| 30 | const r = await fetch(`/api/computer/${encodeURIComponent(computer.name)}`);
|
|---|
| 31 | const d = await r.json();
|
|---|
| 32 | if (!cancelled) setDetails(d);
|
|---|
| 33 | } catch (e) {
|
|---|
| 34 | console.error("details error", e);
|
|---|
| 35 | } finally {
|
|---|
| 36 | if (!cancelled) setLoading(false);
|
|---|
| 37 | }
|
|---|
| 38 | }
|
|---|
| 39 |
|
|---|
| 40 | load();
|
|---|
| 41 | return () => {
|
|---|
| 42 | cancelled = true;
|
|---|
| 43 | };
|
|---|
| 44 | }, [open, computer?.name]);
|
|---|
| 45 |
|
|---|
| 46 | const filteredSysmon = useMemo(() => {
|
|---|
| 47 | const list = details?.sysmon_events || [];
|
|---|
| 48 | const q = search.trim().toLowerCase();
|
|---|
| 49 | if (!q) return list;
|
|---|
| 50 | return list.filter((ev) => {
|
|---|
| 51 | const msg = (ev.message || "").toLowerCase();
|
|---|
| 52 | const type = (ev.event_type || "").toLowerCase();
|
|---|
| 53 | const id = String(ev.event_id ?? "");
|
|---|
| 54 | return msg.includes(q) || type.includes(q) || id.includes(q);
|
|---|
| 55 | });
|
|---|
| 56 | }, [details, search]);
|
|---|
| 57 |
|
|---|
| 58 | function openJson(ev) {
|
|---|
| 59 | // server ти враќа: { event_id, event_type, message, timestamp, details }
|
|---|
| 60 | setJsonTitle(`Sysmon Event ${ev?.event_id ?? ""} • ${ev?.event_type ?? ""}`);
|
|---|
| 61 | setJsonData(ev?.details ?? ev); // ако нема details, покажи цел event
|
|---|
| 62 | setJsonOpen(true);
|
|---|
| 63 | }
|
|---|
| 64 |
|
|---|
| 65 | function closeJson() {
|
|---|
| 66 | setJsonOpen(false);
|
|---|
| 67 | setJsonTitle("");
|
|---|
| 68 | setJsonData(null);
|
|---|
| 69 | }
|
|---|
| 70 |
|
|---|
| 71 | if (!open) return null;
|
|---|
| 72 |
|
|---|
| 73 | return (
|
|---|
| 74 | <>
|
|---|
| 75 | <div className="backdrop" onMouseDown={onClose}>
|
|---|
| 76 | <div className="modal" onMouseDown={(e) => e.stopPropagation()}>
|
|---|
| 77 | <div className="modal-head">
|
|---|
| 78 | <div>
|
|---|
| 79 | <div className="modal-title">{computer.name}</div>
|
|---|
| 80 | <div className="modal-sub">
|
|---|
| 81 | {computer.ip || "—"} • {computer.user || "—"} • {computer.status || "—"}
|
|---|
| 82 | </div>
|
|---|
| 83 | </div>
|
|---|
| 84 |
|
|---|
| 85 | <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
|
|---|
| 86 | <div className="tabs">
|
|---|
| 87 | <button className={`tab ${tab === "overview" ? "active" : ""}`} onClick={() => setTab("overview")}>
|
|---|
| 88 | Overview
|
|---|
| 89 | </button>
|
|---|
| 90 | <button className={`tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}>
|
|---|
| 91 | Sysmon
|
|---|
| 92 | </button>
|
|---|
| 93 | <button className={`tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}>
|
|---|
| 94 | Processes
|
|---|
| 95 | </button>
|
|---|
| 96 | <button className={`tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}>
|
|---|
| 97 | Network
|
|---|
| 98 | </button>
|
|---|
| 99 | </div>
|
|---|
| 100 | <button className="btn" onClick={onClose}>
|
|---|
| 101 | Close
|
|---|
| 102 | </button>
|
|---|
| 103 | </div>
|
|---|
| 104 | </div>
|
|---|
| 105 |
|
|---|
| 106 | <div style={{ padding: 16 }}>
|
|---|
| 107 | {loading && <div style={{ color: "#94a3b8" }}>Loading…</div>}
|
|---|
| 108 |
|
|---|
| 109 | {!loading && details && tab === "overview" && (
|
|---|
| 110 | <div className="panel">
|
|---|
| 111 | <div className="panel-head">
|
|---|
| 112 | <div className="panel-title">System Overview</div>
|
|---|
| 113 | <button className="btn primary" onClick={() => onAskAI(computer.name)}>
|
|---|
| 114 | Ask AI about this PC
|
|---|
| 115 | </button>
|
|---|
| 116 | </div>
|
|---|
| 117 | <div className="panel-body" style={{ display: "grid", gap: 10 }}>
|
|---|
| 118 | <div>
|
|---|
| 119 | <b>User:</b> {details.computer?.user}
|
|---|
| 120 | </div>
|
|---|
| 121 | <div>
|
|---|
| 122 | <b>IP:</b> {details.computer?.ip}
|
|---|
| 123 | </div>
|
|---|
| 124 | <div>
|
|---|
| 125 | <b>OS:</b> {details.computer?.os}
|
|---|
| 126 | </div>
|
|---|
| 127 | <div>
|
|---|
| 128 | <b>First seen:</b> {details.computer?.first_seen}
|
|---|
| 129 | </div>
|
|---|
| 130 | <div>
|
|---|
| 131 | <b>Last seen:</b> {details.computer?.last_seen}
|
|---|
| 132 | </div>
|
|---|
| 133 | <div>
|
|---|
| 134 | <b>Total logs:</b> {details.computer?.total_logs}
|
|---|
| 135 | </div>
|
|---|
| 136 | </div>
|
|---|
| 137 | </div>
|
|---|
| 138 | )}
|
|---|
| 139 |
|
|---|
| 140 | {!loading && details && tab === "sysmon" && (
|
|---|
| 141 | <div className="panel">
|
|---|
| 142 | <div className="panel-head" style={{ alignItems: "center" }}>
|
|---|
| 143 | <div className="panel-title">Sysmon Events</div>
|
|---|
| 144 | <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
|
|---|
| 145 | <span className="pill">{filteredSysmon.length} events</span>
|
|---|
| 146 | <input
|
|---|
| 147 | className="input"
|
|---|
| 148 | style={{ maxWidth: 380 }}
|
|---|
| 149 | placeholder="Search by id / type / message…"
|
|---|
| 150 | value={search}
|
|---|
| 151 | onChange={(e) => setSearch(e.target.value)}
|
|---|
| 152 | />
|
|---|
| 153 | </div>
|
|---|
| 154 | </div>
|
|---|
| 155 |
|
|---|
| 156 | <div className="panel-body">
|
|---|
| 157 | <div className="table-wrap">
|
|---|
| 158 | <table>
|
|---|
| 159 | <thead>
|
|---|
| 160 | <tr>
|
|---|
| 161 | <th style={{ width: 90 }}>Time</th>
|
|---|
| 162 | <th style={{ width: 70 }}>ID</th>
|
|---|
| 163 | <th style={{ width: 170 }}>Type</th>
|
|---|
| 164 | <th style={{ width: 110 }}>Severity</th>
|
|---|
| 165 | <th>Message</th>
|
|---|
| 166 | <th style={{ width: 90 }}>View</th>
|
|---|
| 167 | </tr>
|
|---|
| 168 | </thead>
|
|---|
| 169 | <tbody>
|
|---|
| 170 | {filteredSysmon.slice(0, 200).map((ev, idx) => {
|
|---|
| 171 | const sev = severityForEventId(ev.event_id);
|
|---|
| 172 | return (
|
|---|
| 173 | <tr key={idx}>
|
|---|
| 174 | <td>{ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString() : "—"}</td>
|
|---|
| 175 | <td>
|
|---|
| 176 | <b>{ev.event_id}</b>
|
|---|
| 177 | </td>
|
|---|
| 178 | <td>{ev.event_type}</td>
|
|---|
| 179 | <td>
|
|---|
| 180 | <span className={`badge ${sev}`}>{sev.toUpperCase()}</span>
|
|---|
| 181 | </td>
|
|---|
| 182 | <td
|
|---|
| 183 | style={{
|
|---|
| 184 | fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|---|
| 185 | fontSize: 12,
|
|---|
| 186 | }}
|
|---|
| 187 | >
|
|---|
| 188 | {(ev.message || "").slice(0, 140)}
|
|---|
| 189 | </td>
|
|---|
| 190 | <td>
|
|---|
| 191 | <button className="btn" onClick={() => openJson(ev)}>
|
|---|
| 192 | View
|
|---|
| 193 | </button>
|
|---|
| 194 | </td>
|
|---|
| 195 | </tr>
|
|---|
| 196 | );
|
|---|
| 197 | })}
|
|---|
| 198 |
|
|---|
| 199 | {filteredSysmon.length === 0 && (
|
|---|
| 200 | <tr>
|
|---|
| 201 | <td colSpan="6" style={{ color: "#94a3b8" }}>
|
|---|
| 202 | No sysmon events.
|
|---|
| 203 | </td>
|
|---|
| 204 | </tr>
|
|---|
| 205 | )}
|
|---|
| 206 | </tbody>
|
|---|
| 207 | </table>
|
|---|
| 208 | </div>
|
|---|
| 209 |
|
|---|
| 210 | {filteredSysmon.length > 200 && (
|
|---|
| 211 | <div style={{ marginTop: 10, color: "#94a3b8", fontSize: 12 }}>
|
|---|
| 212 | Showing first 200 (for speed).
|
|---|
| 213 | </div>
|
|---|
| 214 | )}
|
|---|
| 215 | </div>
|
|---|
| 216 | </div>
|
|---|
| 217 | )}
|
|---|
| 218 |
|
|---|
| 219 | {!loading && details && tab === "processes" && (
|
|---|
| 220 | <div className="panel">
|
|---|
| 221 | <div className="panel-head">
|
|---|
| 222 | <div className="panel-title">Recent Processes</div>
|
|---|
| 223 | </div>
|
|---|
| 224 | <div className="panel-body">
|
|---|
| 225 | <div className="table-wrap">
|
|---|
| 226 | <table>
|
|---|
| 227 | <thead>
|
|---|
| 228 | <tr>
|
|---|
| 229 | <th>Time</th>
|
|---|
| 230 | <th>PID</th>
|
|---|
| 231 | <th>Name</th>
|
|---|
| 232 | <th>User</th>
|
|---|
| 233 | <th>CPU%</th>
|
|---|
| 234 | <th>RAM MB</th>
|
|---|
| 235 | <th>Cmdline</th>
|
|---|
| 236 | </tr>
|
|---|
| 237 | </thead>
|
|---|
| 238 | <tbody>
|
|---|
| 239 | {(details.recent_processes || []).slice(0, 150).map((p, idx) => (
|
|---|
| 240 | <tr key={idx}>
|
|---|
| 241 | <td>{p.timestamp ? new Date(p.timestamp).toLocaleTimeString() : "—"}</td>
|
|---|
| 242 | <td>
|
|---|
| 243 | <b>{p.pid}</b>
|
|---|
| 244 | </td>
|
|---|
| 245 | <td>{p.name}</td>
|
|---|
| 246 | <td>{p.username || "—"}</td>
|
|---|
| 247 | <td>{p.cpu_percent ?? 0}</td>
|
|---|
| 248 | <td>{p.memory_mb ?? 0}</td>
|
|---|
| 249 | <td
|
|---|
| 250 | style={{
|
|---|
| 251 | fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|---|
| 252 | fontSize: 12,
|
|---|
| 253 | }}
|
|---|
| 254 | >
|
|---|
| 255 | {(p.cmdline || "").slice(0, 120)}
|
|---|
| 256 | </td>
|
|---|
| 257 | </tr>
|
|---|
| 258 | ))}
|
|---|
| 259 | {(details.recent_processes || []).length === 0 && (
|
|---|
| 260 | <tr>
|
|---|
| 261 | <td colSpan="7" style={{ color: "#94a3b8" }}>
|
|---|
| 262 | No process data.
|
|---|
| 263 | </td>
|
|---|
| 264 | </tr>
|
|---|
| 265 | )}
|
|---|
| 266 | </tbody>
|
|---|
| 267 | </table>
|
|---|
| 268 | </div>
|
|---|
| 269 | </div>
|
|---|
| 270 | </div>
|
|---|
| 271 | )}
|
|---|
| 272 |
|
|---|
| 273 | {!loading && details && tab === "network" && (
|
|---|
| 274 | <div className="panel">
|
|---|
| 275 | <div className="panel-head">
|
|---|
| 276 | <div className="panel-title">Network Connections</div>
|
|---|
| 277 | </div>
|
|---|
| 278 | <div className="panel-body">
|
|---|
| 279 | <div className="table-wrap">
|
|---|
| 280 | <table>
|
|---|
| 281 | <thead>
|
|---|
| 282 | <tr>
|
|---|
| 283 | <th>Time</th>
|
|---|
| 284 | <th>PID</th>
|
|---|
| 285 | <th>Process</th>
|
|---|
| 286 | <th>Local</th>
|
|---|
| 287 | <th>Remote</th>
|
|---|
| 288 | <th>Status</th>
|
|---|
| 289 | </tr>
|
|---|
| 290 | </thead>
|
|---|
| 291 | <tbody>
|
|---|
| 292 | {(details.network_connections || []).slice(0, 150).map((n, idx) => (
|
|---|
| 293 | <tr key={idx}>
|
|---|
| 294 | <td>{n.timestamp ? new Date(n.timestamp).toLocaleTimeString() : "—"}</td>
|
|---|
| 295 | <td>
|
|---|
| 296 | <b>{n.pid}</b>
|
|---|
| 297 | </td>
|
|---|
| 298 | <td>{n.process_name || "—"}</td>
|
|---|
| 299 | <td
|
|---|
| 300 | style={{
|
|---|
| 301 | fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|---|
| 302 | fontSize: 12,
|
|---|
| 303 | }}
|
|---|
| 304 | >
|
|---|
| 305 | {n.local_address || "—"}
|
|---|
| 306 | </td>
|
|---|
| 307 | <td
|
|---|
| 308 | style={{
|
|---|
| 309 | fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|---|
| 310 | fontSize: 12,
|
|---|
| 311 | }}
|
|---|
| 312 | >
|
|---|
| 313 | {n.remote_address || "—"}
|
|---|
| 314 | </td>
|
|---|
| 315 | <td>{n.status || "—"}</td>
|
|---|
| 316 | </tr>
|
|---|
| 317 | ))}
|
|---|
| 318 | {(details.network_connections || []).length === 0 && (
|
|---|
| 319 | <tr>
|
|---|
| 320 | <td colSpan="6" style={{ color: "#94a3b8" }}>
|
|---|
| 321 | No network data.
|
|---|
| 322 | </td>
|
|---|
| 323 | </tr>
|
|---|
| 324 | )}
|
|---|
| 325 | </tbody>
|
|---|
| 326 | </table>
|
|---|
| 327 | </div>
|
|---|
| 328 | </div>
|
|---|
| 329 | </div>
|
|---|
| 330 | )}
|
|---|
| 331 |
|
|---|
| 332 | {!loading && !details && <div style={{ color: "#94a3b8" }}>No details.</div>}
|
|---|
| 333 | </div>
|
|---|
| 334 | </div>
|
|---|
| 335 | </div>
|
|---|
| 336 |
|
|---|
| 337 | {/* JSON VIEWER MODAL */}
|
|---|
| 338 | {jsonOpen && (
|
|---|
| 339 | <div className="backdrop" onMouseDown={closeJson}>
|
|---|
| 340 | <div className="modal" onMouseDown={(e) => e.stopPropagation()} style={{ width: "min(900px, 96vw)" }}>
|
|---|
| 341 | <div className="modal-head">
|
|---|
| 342 | <div>
|
|---|
| 343 | <div className="modal-title">View JSON</div>
|
|---|
| 344 | <div className="modal-sub">{jsonTitle}</div>
|
|---|
| 345 | </div>
|
|---|
| 346 | <div style={{ display: "flex", gap: 10 }}>
|
|---|
| 347 | <button
|
|---|
| 348 | className="btn"
|
|---|
| 349 | onClick={() => {
|
|---|
| 350 | const txt = JSON.stringify(jsonData ?? {}, null, 2);
|
|---|
| 351 | navigator.clipboard.writeText(txt);
|
|---|
| 352 | }}
|
|---|
| 353 | >
|
|---|
| 354 | Copy
|
|---|
| 355 | </button>
|
|---|
| 356 | <button className="btn" onClick={closeJson}>
|
|---|
| 357 | Close
|
|---|
| 358 | </button>
|
|---|
| 359 | </div>
|
|---|
| 360 | </div>
|
|---|
| 361 | <div style={{ padding: 16 }}>
|
|---|
| 362 | <pre
|
|---|
| 363 | style={{
|
|---|
| 364 | margin: 0,
|
|---|
| 365 | maxHeight: "70vh",
|
|---|
| 366 | overflow: "auto",
|
|---|
| 367 | padding: 14,
|
|---|
| 368 | borderRadius: 12,
|
|---|
| 369 | border: "1px solid rgba(148,163,184,0.25)",
|
|---|
| 370 | background: "rgba(2,6,23,0.35)",
|
|---|
| 371 | color: "rgba(226,232,240,0.95)",
|
|---|
| 372 | fontSize: 12,
|
|---|
| 373 | lineHeight: 1.5,
|
|---|
| 374 | }}
|
|---|
| 375 | >
|
|---|
| 376 | {JSON.stringify(jsonData ?? {}, null, 2)}
|
|---|
| 377 | </pre>
|
|---|
| 378 | </div>
|
|---|
| 379 | </div>
|
|---|
| 380 | </div>
|
|---|
| 381 | )}
|
|---|
| 382 | </>
|
|---|
| 383 | );
|
|---|
| 384 | }
|
|---|