feat: RDG 302/303 display path labels and RDS session break (0.3.1)
Show RDS access via RDG-Comp or Haproxy-RDG-Comp, enable qwinsta/logoff on any RDG event with internal_ip. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -187,6 +187,8 @@ export interface EventSummary {
|
||||
rdg_flap?: boolean;
|
||||
rdg_flap_pair_event_id?: number | null;
|
||||
rdg_flap_qwinsta_event_id?: number | null;
|
||||
rdg_access_path?: string | null;
|
||||
rdg_qwinsta_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentCommandResponse {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<span class="rdg-access-badge" :title="tooltip">{{ event.rdg_access_path }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { EventSummary } from "../api";
|
||||
|
||||
const props = defineProps<{
|
||||
event: EventSummary;
|
||||
}>();
|
||||
|
||||
const tooltip = computed(() => {
|
||||
const path = props.event.rdg_access_path;
|
||||
if (path === "Haproxy-RDG-Comp") {
|
||||
return "Вход на рабочий ПК через HAProxy → RD Gateway";
|
||||
}
|
||||
if (path === "RDG-Comp") {
|
||||
return "Прямой вход на рабочий ПК через RD Gateway";
|
||||
}
|
||||
return "Путь входа RDS через RD Gateway";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rdg-access-badge {
|
||||
display: inline-block;
|
||||
margin-right: 0.45rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: #7ec8e3;
|
||||
background: rgba(126, 200, 227, 0.1);
|
||||
border: 1px solid rgba(126, 200, 227, 0.4);
|
||||
border-radius: 4px;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type AgentCommandResponse,
|
||||
type EventSummary,
|
||||
} from "../api";
|
||||
import { rdgQwinstaEventId } from "../utils/rdgFlap";
|
||||
|
||||
export type QwinstaSessionRow = {
|
||||
sessionName: string;
|
||||
@@ -114,7 +115,7 @@ export function useRdgQwinsta() {
|
||||
}
|
||||
|
||||
async function runQwinsta(event: EventSummary) {
|
||||
const targetId = event.rdg_flap_qwinsta_event_id ?? event.id;
|
||||
const targetId = rdgQwinstaEventId(event) ?? event.id;
|
||||
qwinstaLoadingId.value = event.id;
|
||||
qwinstaModal.value = {
|
||||
open: true,
|
||||
|
||||
@@ -4,12 +4,31 @@ export function hasRdgFlapUi(event: EventSummary): boolean {
|
||||
return Boolean(event.rdg_flap || event.rdg_flap_qwinsta_event_id);
|
||||
}
|
||||
|
||||
export function rdgFlapQwinstaEventId(event: EventSummary): number | null {
|
||||
export function hasRdgAccessPath(event: EventSummary): boolean {
|
||||
return Boolean(event.rdg_access_path);
|
||||
}
|
||||
|
||||
export function rdgAccessPathLabel(event: EventSummary): string {
|
||||
return event.rdg_access_path || "";
|
||||
}
|
||||
|
||||
export function rdgQwinstaEventId(event: EventSummary): number | null {
|
||||
if (event.rdg_flap_qwinsta_event_id) return event.rdg_flap_qwinsta_event_id;
|
||||
return event.rdg_flap ? event.id : null;
|
||||
if (event.rdg_qwinsta_enabled) return event.id;
|
||||
if (event.rdg_flap) return event.id;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @deprecated use rdgQwinstaEventId */
|
||||
export function rdgFlapQwinstaEventId(event: EventSummary): number | null {
|
||||
return rdgQwinstaEventId(event);
|
||||
}
|
||||
|
||||
export function rdgFlapPairLabel(event: EventSummary): string {
|
||||
if (!event.rdg_flap_pair_event_id) return "";
|
||||
return event.type === "rdg.connection.success" ? "→" : "←";
|
||||
}
|
||||
|
||||
export function isRdgConnectionEvent(event: EventSummary): boolean {
|
||||
return event.type.startsWith("rdg.connection.");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||
export const APP_NAME = "Security Alert Center";
|
||||
export const APP_VERSION = "0.3.0";
|
||||
export const APP_VERSION = "0.3.1";
|
||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||
|
||||
@@ -282,6 +282,7 @@
|
||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||
|
||||
<td>
|
||||
<RdgAccessBadge v-if="e.rdg_access_path" :event="e" />
|
||||
<RdgFlapBadge v-if="hasRdgFlapUi(e)" :event="e" />
|
||||
{{ e.title }}
|
||||
</td>
|
||||
@@ -289,13 +290,14 @@
|
||||
<td class="dash-actions">
|
||||
|
||||
<button
|
||||
v-if="rdgFlapQwinstaEventId(e)"
|
||||
v-if="rdgQwinstaEventId(e)"
|
||||
type="button"
|
||||
class="secondary dash-qwinsta-btn"
|
||||
:disabled="qwinstaLoadingId === e.id"
|
||||
title="qwinsta и завершение сеанса на рабочем ПК"
|
||||
@click="runQwinsta(e)"
|
||||
>
|
||||
qwinsta
|
||||
Обрыв RDS
|
||||
</button>
|
||||
<button
|
||||
v-if="eventSupportsSessionTerminate(e)"
|
||||
@@ -346,12 +348,13 @@ import {
|
||||
type DashboardSummary,
|
||||
type EventSummary,
|
||||
} from "../api";
|
||||
import RdgAccessBadge from "../components/RdgAccessBadge.vue";
|
||||
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
|
||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||
import { useSacVersion } from "../composables/useSacVersion";
|
||||
import { formatServerName } from "../utils/hostDisplay";
|
||||
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
|
||||
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
|
||||
import { eventSupportsSessionTerminate } from "../utils/sessionActions";
|
||||
|
||||
const { appName, appVersion } = useSacVersion();
|
||||
|
||||
@@ -7,18 +7,23 @@
|
||||
<p v-else-if="loading">Загрузка…</p>
|
||||
<template v-else-if="event">
|
||||
<h1>{{ event.title }}</h1>
|
||||
<p v-if="event.rdg_access_path" class="event-rdg-access-banner">
|
||||
<RdgAccessBadge :event="event" />
|
||||
<span class="muted">Вход на рабочий ПК через RD Gateway.</span>
|
||||
</p>
|
||||
<p v-if="hasRdgFlapUi(event)" class="event-rdg-flap-banner">
|
||||
<RdgFlapBadge :event="event" :detail-link-query="backToEventsQuery" />
|
||||
<span class="muted">302→303 за несколько секунд — возможна зависшая сессия на ПК пользователя.</span>
|
||||
</p>
|
||||
<p v-if="rdgFlapQwinstaEventId(event)" class="event-rdg-actions">
|
||||
<p v-if="rdgQwinstaEventId(event)" class="event-rdg-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:disabled="qwinstaLoadingId === event.id"
|
||||
title="qwinsta и завершение сеанса на рабочем ПК"
|
||||
@click="runQwinsta(event)"
|
||||
>
|
||||
qwinsta (RDG flap)
|
||||
Обрыв RDS
|
||||
</button>
|
||||
</p>
|
||||
<RdgQwinstaModal
|
||||
@@ -93,11 +98,12 @@
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { apiFetch, type EventDetail } from "../api";
|
||||
import RdgAccessBadge from "../components/RdgAccessBadge.vue";
|
||||
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
|
||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||
import ReportBodyCard from "../components/ReportBodyCard.vue";
|
||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
|
||||
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
|
||||
import { eventsBackQueryFromDetail } from "../utils/eventsListQuery";
|
||||
import { dailyReportTypeLabel, isDailyReportType } from "../utils/reportDisplay";
|
||||
|
||||
|
||||
@@ -54,18 +54,20 @@
|
||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||
<td><code>{{ e.type }}</code></td>
|
||||
<td>
|
||||
<RdgAccessBadge v-if="e.rdg_access_path" :event="e" />
|
||||
<RdgFlapBadge v-if="hasRdgFlapUi(e)" :event="e" :detail-link-query="detailLinkQuery" />
|
||||
{{ e.title }}
|
||||
</td>
|
||||
<td class="events-actions">
|
||||
<button
|
||||
v-if="rdgFlapQwinstaEventId(e)"
|
||||
v-if="rdgQwinstaEventId(e)"
|
||||
type="button"
|
||||
class="secondary events-qwinsta-btn"
|
||||
:disabled="qwinstaLoadingId === e.id"
|
||||
title="qwinsta и завершение сеанса на рабочем ПК"
|
||||
@click="runQwinsta(e)"
|
||||
>
|
||||
qwinsta
|
||||
Обрыв RDS
|
||||
</button>
|
||||
<button
|
||||
v-if="eventSupportsSessionTerminate(e)"
|
||||
@@ -105,10 +107,11 @@ import { computed, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useSacLiveEventStream } from "../composables/useSacLiveEventStream";
|
||||
import { apiFetch, postEventTerminateSession, type EventListResponse, type EventSummary } from "../api";
|
||||
import RdgAccessBadge from "../components/RdgAccessBadge.vue";
|
||||
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
|
||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
|
||||
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
|
||||
import {
|
||||
apiParamsFromState,
|
||||
buildEventsListQuery,
|
||||
|
||||
Reference in New Issue
Block a user