feat: RDG qwinsta/logoff via WinRM on client workstation (0.11.6)

SAC resolves internal_ip to a registered Windows host and runs qwinsta/logoff synchronously over WinRM instead of queueing commands to the gateway agent.
This commit is contained in:
PTah
2026-06-20 15:36:12 +10:00
parent 2eb06acb5b
commit 75f4c475df
13 changed files with 612 additions and 174 deletions
+3
View File
@@ -180,6 +180,9 @@ export interface AgentCommandResponse {
result_stderr?: string | null;
created_at?: string | null;
completed_at?: string | null;
target?: string | null;
client_hostname?: string | null;
internal_ip?: string | null;
}
export function postEventQwinsta(eventId: number): Promise<AgentCommandResponse> {
+1 -1
View File
@@ -1,4 +1,4 @@
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.11.5";
export const APP_VERSION = "0.11.6";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
+33 -19
View File
@@ -305,7 +305,8 @@
<div v-if="qwinstaModal.open" class="modal-backdrop" @click.self="closeQwinstaModal">
<div class="modal-card qwinsta-modal" role="dialog" aria-modal="true">
<h3>qwinsta событие #{{ qwinstaModal.eventId }}</h3>
<h3>qwinsta {{ qwinstaModal.clientHostname || qwinstaModal.target || `#${qwinstaModal.eventId}` }}</h3>
<p v-if="qwinstaModal.internalIp" class="muted qwinsta-meta">Клиентский ПК: {{ qwinstaModal.internalIp }}</p>
<p v-if="qwinstaModal.error" class="error">{{ qwinstaModal.error }}</p>
<p v-else-if="qwinstaModal.loading">Ожидание ответа агента</p>
<template v-else>
@@ -412,6 +413,9 @@ const qwinstaModal = ref({
eventId: 0,
actorUser: "",
commandUuid: "",
target: "",
clientHostname: "",
internalIp: "",
loading: false,
error: "",
stdout: "",
@@ -494,6 +498,12 @@ function pollQwinstaCommand(eventId: number, commandUuid: string, actorUser: str
}, 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 = {
@@ -501,6 +511,9 @@ async function runQwinsta(event: EventSummary) {
eventId: event.id,
actorUser: event.actor_user || "",
commandUuid: "",
target: "",
clientHostname: "",
internalIp: "",
loading: true,
error: "",
stdout: "",
@@ -509,6 +522,7 @@ async function runQwinsta(event: EventSummary) {
};
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;
@@ -530,25 +544,20 @@ async function runLogoff(sessionId: number) {
modal.error = "";
try {
const cmd = await postEventLogoff(modal.eventId, sessionId);
if (cmd.status === "pending") {
const deadline = Date.now() + 60000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 2000));
const polled = await fetchEventAction(modal.eventId, cmd.command_uuid);
if (polled.status !== "pending") {
if (polled.status === "failed") {
modal.error = polled.result_stderr || "logoff failed";
} else {
modal.loading = true;
modal.stdout = "";
modal.sessions = [];
const again = await postEventQwinsta(modal.eventId);
pollQwinstaCommand(modal.eventId, again.command_uuid, modal.actorUser);
}
break;
}
}
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 {
@@ -849,6 +858,11 @@ onUnmounted(() => {
padding: 1rem 1.25rem;
}
.qwinsta-meta {
margin: 0 0 0.75rem;
font-size: 0.9rem;
}
.qwinsta-raw {
font-size: 0.8rem;
max-height: 12rem;