diff --git a/backend/app/version.py b/backend/app/version.py index 05b91fc..09905d6 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.20.3" +APP_VERSION = "0.20.4" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/frontend/src/components/RdgQwinstaModal.vue b/frontend/src/components/RdgQwinstaModal.vue new file mode 100644 index 0000000..7a24cd6 --- /dev/null +++ b/frontend/src/components/RdgQwinstaModal.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/frontend/src/composables/useRdgQwinsta.ts b/frontend/src/composables/useRdgQwinsta.ts new file mode 100644 index 0000000..e9cd1a1 --- /dev/null +++ b/frontend/src/composables/useRdgQwinsta.ts @@ -0,0 +1,186 @@ +import { onUnmounted, ref } from "vue"; +import { + fetchEventAction, + postEventLogoff, + postEventQwinsta, + type AgentCommandResponse, + type EventSummary, +} from "../api"; + +export type QwinstaSessionRow = { + sessionName: string; + userName: string; + id: number; + state: string; +}; + +function parseQwinstaSessions(stdout: string, actorUser: string): QwinstaSessionRow[] { + const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean); + const rows: QwinstaSessionRow[] = []; + for (const line of lines) { + if (/^SESSION/i.test(line) || /^---/.test(line)) continue; + const parts = line.split(/\s+/); + if (parts.length < 4) continue; + const sessionName = parts[0]; + const userName = parts[1]; + const id = Number.parseInt(parts[2], 10); + const state = parts.slice(3).join(" "); + if (!Number.isFinite(id)) continue; + const norm = (s: string) => s.replace(/^B26\\/i, "").toLowerCase(); + const matchUser = actorUser && norm(userName).includes(norm(actorUser)); + if (actorUser && !matchUser) continue; + rows.push({ sessionName, userName, id, state }); + } + if (rows.length === 0 && stdout.trim()) { + for (const line of lines) { + if (/^SESSION/i.test(line) || /^---/.test(line)) continue; + const parts = line.split(/\s+/); + if (parts.length < 4) continue; + const id = Number.parseInt(parts[2], 10); + if (!Number.isFinite(id)) continue; + rows.push({ + sessionName: parts[0], + userName: parts[1], + id, + state: parts.slice(3).join(" "), + }); + } + } + return rows; +} + +export function useRdgQwinsta() { + let qwinstaPollTimer: ReturnType | null = null; + const qwinstaLoadingId = ref(null); + const qwinstaModal = ref({ + open: false, + eventId: 0, + actorUser: "", + commandUuid: "", + target: "", + clientHostname: "", + internalIp: "", + loading: false, + error: "", + stdout: "", + sessions: [] as QwinstaSessionRow[], + logoffId: null as number | null, + }); + + function stopQwinstaPoll() { + if (qwinstaPollTimer) { + clearInterval(qwinstaPollTimer); + qwinstaPollTimer = null; + } + } + + function closeQwinstaModal() { + stopQwinstaPoll(); + qwinstaModal.value.open = false; + } + + function applyCommandResult(eventId: number, actorUser: string, cmd: AgentCommandResponse) { + if (cmd.status === "pending") return false; + qwinstaModal.value.loading = false; + if (cmd.status === "failed") { + qwinstaModal.value.error = cmd.result_stderr || cmd.result_stdout || "Команда завершилась с ошибкой"; + return true; + } + qwinstaModal.value.stdout = cmd.result_stdout || ""; + qwinstaModal.value.sessions = parseQwinstaSessions(qwinstaModal.value.stdout, actorUser); + return true; + } + + function applyCommandMeta(modal: typeof qwinstaModal.value, cmd: AgentCommandResponse) { + modal.target = cmd.target || ""; + modal.clientHostname = cmd.client_hostname || ""; + modal.internalIp = cmd.internal_ip || ""; + } + + function pollQwinstaCommand(eventId: number, commandUuid: string, actorUser: string) { + stopQwinstaPoll(); + qwinstaPollTimer = setInterval(async () => { + try { + const cmd = await fetchEventAction(eventId, commandUuid); + if (applyCommandResult(eventId, actorUser, cmd)) { + stopQwinstaPoll(); + } + } catch (e) { + qwinstaModal.value.loading = false; + qwinstaModal.value.error = e instanceof Error ? e.message : "Ошибка опроса команды"; + stopQwinstaPoll(); + } + }, 2000); + } + + async function runQwinsta(event: EventSummary) { + qwinstaLoadingId.value = event.id; + qwinstaModal.value = { + open: true, + eventId: event.id, + actorUser: event.actor_user || "", + commandUuid: "", + target: "", + clientHostname: "", + internalIp: "", + loading: true, + error: "", + stdout: "", + sessions: [], + logoffId: null, + }; + try { + const cmd = await postEventQwinsta(event.id); + applyCommandMeta(qwinstaModal.value, cmd); + qwinstaModal.value.commandUuid = cmd.command_uuid; + if (applyCommandResult(event.id, event.actor_user || "", cmd)) { + return; + } + pollQwinstaCommand(event.id, cmd.command_uuid, event.actor_user || ""); + } catch (e) { + qwinstaModal.value.loading = false; + qwinstaModal.value.error = e instanceof Error ? e.message : "Не удалось отправить qwinsta"; + } finally { + qwinstaLoadingId.value = null; + } + } + + async function runLogoff(sessionId: number) { + const modal = qwinstaModal.value; + if (!modal.eventId) return; + if (!window.confirm(`Завершить сеанс ID ${sessionId}?`)) return; + modal.logoffId = sessionId; + modal.error = ""; + try { + const cmd = await postEventLogoff(modal.eventId, sessionId); + applyCommandMeta(modal, cmd); + if (cmd.status === "failed") { + modal.error = cmd.result_stderr || cmd.result_stdout || "logoff failed"; + return; + } + modal.loading = true; + modal.stdout = ""; + modal.sessions = []; + const again = await postEventQwinsta(modal.eventId); + applyCommandMeta(modal, again); + if (applyCommandResult(modal.eventId, modal.actorUser, again)) { + return; + } + pollQwinstaCommand(modal.eventId, again.command_uuid, modal.actorUser); + } catch (e) { + modal.error = e instanceof Error ? e.message : "logoff error"; + } finally { + modal.logoffId = null; + } + } + + onUnmounted(() => stopQwinstaPoll()); + + return { + qwinstaLoadingId, + qwinstaModal, + closeQwinstaModal, + runQwinsta, + runLogoff, + }; +} diff --git a/frontend/src/version.ts b/frontend/src/version.ts index fde3c66..82fa1ed 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,4 +1,4 @@ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ export const APP_NAME = "Security Alert Center"; -export const APP_VERSION = "0.20.1"; +export const APP_VERSION = "0.20.4"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`; diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index b60e2e0..46ac85e 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -303,50 +303,11 @@ - +
@@ -368,20 +329,17 @@ import { onMounted, onUnmounted, ref } from "vue"; import { apiFetch, - fetchEventAction, getToken, - postEventLogoff, - postEventQwinsta, - type AgentCommandResponse, type DashboardSummary, type EventSummary, } from "../api"; +import RdgQwinstaModal from "../components/RdgQwinstaModal.vue"; +import { useRdgQwinsta } from "../composables/useRdgQwinsta"; import { useSacVersion } from "../composables/useSacVersion"; import { formatServerName } from "../utils/hostDisplay"; const { appName, appVersion } = useSacVersion(); - - +const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta(); const data = ref(null); @@ -397,174 +355,6 @@ let eventSource: EventSource | null = null; let refreshingRecent = false; -let qwinstaPollTimer: ReturnType | null = null; - -const qwinstaLoadingId = ref(null); - -type QwinstaSessionRow = { - sessionName: string; - userName: string; - id: number; - state: string; -}; - -const qwinstaModal = ref({ - open: false, - eventId: 0, - actorUser: "", - commandUuid: "", - target: "", - clientHostname: "", - internalIp: "", - loading: false, - error: "", - stdout: "", - sessions: [] as QwinstaSessionRow[], - logoffId: null as number | null, -}); - -function parseQwinstaSessions(stdout: string, actorUser: string): QwinstaSessionRow[] { - const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean); - const rows: QwinstaSessionRow[] = []; - for (const line of lines) { - if (/^SESSION/i.test(line) || /^---/.test(line)) continue; - const parts = line.split(/\s+/); - if (parts.length < 4) continue; - const sessionName = parts[0]; - const userName = parts[1]; - const id = Number.parseInt(parts[2], 10); - const state = parts.slice(3).join(" "); - if (!Number.isFinite(id)) continue; - const norm = (s: string) => s.replace(/^B26\\/i, "").toLowerCase(); - const matchUser = actorUser && norm(userName).includes(norm(actorUser)); - if (actorUser && !matchUser) continue; - rows.push({ sessionName, userName, id, state }); - } - if (rows.length === 0 && stdout.trim()) { - for (const line of lines) { - if (/^SESSION/i.test(line) || /^---/.test(line)) continue; - const parts = line.split(/\s+/); - if (parts.length < 4) continue; - const id = Number.parseInt(parts[2], 10); - if (!Number.isFinite(id)) continue; - rows.push({ - sessionName: parts[0], - userName: parts[1], - id, - state: parts.slice(3).join(" "), - }); - } - } - return rows; -} - -function stopQwinstaPoll() { - if (qwinstaPollTimer) { - clearInterval(qwinstaPollTimer); - qwinstaPollTimer = null; - } -} - -function closeQwinstaModal() { - stopQwinstaPoll(); - qwinstaModal.value.open = false; -} - -function applyCommandResult(eventId: number, actorUser: string, cmd: AgentCommandResponse) { - if (cmd.status === "pending") return false; - qwinstaModal.value.loading = false; - if (cmd.status === "failed") { - qwinstaModal.value.error = cmd.result_stderr || cmd.result_stdout || "Команда завершилась с ошибкой"; - return true; - } - qwinstaModal.value.stdout = cmd.result_stdout || ""; - qwinstaModal.value.sessions = parseQwinstaSessions(qwinstaModal.value.stdout, actorUser); - return true; -} - -function pollQwinstaCommand(eventId: number, commandUuid: string, actorUser: string) { - stopQwinstaPoll(); - qwinstaPollTimer = setInterval(async () => { - try { - const cmd = await fetchEventAction(eventId, commandUuid); - if (applyCommandResult(eventId, actorUser, cmd)) { - stopQwinstaPoll(); - } - } catch (e) { - qwinstaModal.value.loading = false; - qwinstaModal.value.error = e instanceof Error ? e.message : "Ошибка опроса команды"; - stopQwinstaPoll(); - } - }, 2000); -} - -function applyCommandMeta(modal: typeof qwinstaModal.value, cmd: AgentCommandResponse) { - modal.target = cmd.target || ""; - modal.clientHostname = cmd.client_hostname || ""; - modal.internalIp = cmd.internal_ip || ""; -} - -async function runQwinsta(event: EventSummary) { - qwinstaLoadingId.value = event.id; - qwinstaModal.value = { - open: true, - eventId: event.id, - actorUser: event.actor_user || "", - commandUuid: "", - target: "", - clientHostname: "", - internalIp: "", - loading: true, - error: "", - stdout: "", - sessions: [], - logoffId: null, - }; - try { - const cmd = await postEventQwinsta(event.id); - applyCommandMeta(qwinstaModal.value, cmd); - qwinstaModal.value.commandUuid = cmd.command_uuid; - if (applyCommandResult(event.id, event.actor_user || "", cmd)) { - return; - } - pollQwinstaCommand(event.id, cmd.command_uuid, event.actor_user || ""); - } catch (e) { - qwinstaModal.value.loading = false; - qwinstaModal.value.error = e instanceof Error ? e.message : "Не удалось отправить qwinsta"; - } finally { - qwinstaLoadingId.value = null; - } -} - -async function runLogoff(sessionId: number) { - const modal = qwinstaModal.value; - if (!modal.eventId) return; - if (!window.confirm(`Завершить сеанс ID ${sessionId}?`)) return; - modal.logoffId = sessionId; - modal.error = ""; - try { - const cmd = await postEventLogoff(modal.eventId, sessionId); - applyCommandMeta(modal, cmd); - if (cmd.status === "failed") { - modal.error = cmd.result_stderr || cmd.result_stdout || "logoff failed"; - return; - } - modal.loading = true; - modal.stdout = ""; - modal.sessions = []; - const again = await postEventQwinsta(modal.eventId); - applyCommandMeta(modal, again); - if (applyCommandResult(modal.eventId, modal.actorUser, again)) { - return; - } - pollQwinstaCommand(modal.eventId, again.command_uuid, modal.actorUser); - } catch (e) { - modal.error = e instanceof Error ? e.message : "logoff error"; - } finally { - modal.logoffId = null; - } -} - function formatDt(iso: string) { return new Date(iso).toLocaleString("ru-RU"); @@ -790,13 +580,8 @@ onMounted(() => { onUnmounted(() => { - - stopQwinstaPoll(); - eventSource?.close(); - eventSource = null; - }); @@ -836,49 +621,6 @@ onUnmounted(() => { padding: 0.2rem 0.55rem; } -.modal-backdrop { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.55); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1rem; -} - -.modal-card { - background: var(--card-bg, #1e2430); - border: 1px solid var(--border, #3a4556); - border-radius: 8px; - max-width: 720px; - width: 100%; - max-height: 85vh; - overflow: auto; - padding: 1rem 1.25rem; -} - -.qwinsta-meta { - margin: 0 0 0.75rem; - font-size: 0.9rem; -} - -.qwinsta-raw { - font-size: 0.8rem; - max-height: 12rem; - overflow: auto; - background: #0d1117; - padding: 0.75rem; - border-radius: 4px; -} - -.modal-actions { - margin-top: 1rem; - display: flex; - justify-content: flex-end; - gap: 0.5rem; -} - diff --git a/frontend/src/views/EventDetailView.vue b/frontend/src/views/EventDetailView.vue index 4f28493..b88a54d 100644 --- a/frontend/src/views/EventDetailView.vue +++ b/frontend/src/views/EventDetailView.vue @@ -7,6 +7,21 @@

Загрузка…