feat: Windows admin in Settings UI and WinRM host test (0.10.1)
Store domain admin in ui_settings (DB overrides env); qwinsta/logoff use effective config.
Host card: POST /hosts/{id}/actions/winrm-test via pywinrm.
This commit is contained in:
@@ -412,6 +412,40 @@ export function testMobileDevicePush(deviceId: number): Promise<{ message: strin
|
||||
return apiFetch(`/api/v1/mobile/devices/${deviceId}/test-push`, { method: "POST" });
|
||||
}
|
||||
|
||||
export interface WinAdminSettings {
|
||||
configured: boolean;
|
||||
user: string | null;
|
||||
password_hint: string | null;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export function fetchWinAdminSettings(): Promise<WinAdminSettings> {
|
||||
return apiFetch<WinAdminSettings>("/api/v1/settings/win-admin");
|
||||
}
|
||||
|
||||
export function updateWinAdminSettings(payload: {
|
||||
user?: string | null;
|
||||
password?: string | null;
|
||||
}): Promise<WinAdminSettings> {
|
||||
return apiFetch<WinAdminSettings>("/api/v1/settings/win-admin", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export interface HostWinRmTestResult {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
target: string;
|
||||
hostname: string | null;
|
||||
}
|
||||
|
||||
export function testHostWinRm(hostId: number): Promise<HostWinRmTestResult> {
|
||||
return apiFetch<HostWinRmTestResult>(`/api/v1/hosts/${hostId}/actions/winrm-test`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export interface DashboardSummary {
|
||||
events_last_24h: number;
|
||||
hosts_total: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||
export const APP_NAME = "Security Alert Center";
|
||||
export const APP_VERSION = "0.10.0";
|
||||
export const APP_VERSION = "0.10.1";
|
||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||
|
||||
@@ -47,6 +47,17 @@
|
||||
<dd>{{ host.event_count ?? 0 }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div v-if="isWindowsHost" class="host-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:disabled="testingWinRm"
|
||||
@click="runWinRmTest"
|
||||
>
|
||||
{{ testingWinRm ? "Проверка…" : "Проверить WinRM (admin)" }}
|
||||
</button>
|
||||
<p v-if="winRmTestMessage" :class="winRmTestOk ? 'success' : 'error'">{{ winRmTestMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="inventory" class="host-inventory card">
|
||||
@@ -165,6 +176,7 @@ import {
|
||||
apiFetch,
|
||||
fetchHost,
|
||||
fetchRecentEvents,
|
||||
testHostWinRm,
|
||||
type EventListResponse,
|
||||
type HostDetail,
|
||||
} from "../api";
|
||||
@@ -180,6 +192,9 @@ const error = ref("");
|
||||
const eventsError = ref("");
|
||||
const eventsPage = ref(1);
|
||||
const eventsPageSize = 50;
|
||||
const testingWinRm = ref(false);
|
||||
const winRmTestMessage = ref("");
|
||||
const winRmTestOk = ref(false);
|
||||
let refreshingLive = false;
|
||||
|
||||
useSacLiveEventStream(() => {
|
||||
@@ -188,6 +203,13 @@ useSacLiveEventStream(() => {
|
||||
|
||||
const hostId = computed(() => parseInt(props.id, 10));
|
||||
|
||||
const isWindowsHost = computed(() => {
|
||||
const h = host.value;
|
||||
if (!h) return false;
|
||||
if ((h.os_family || "").toLowerCase() === "windows") return true;
|
||||
return h.product === "rdp-login-monitor";
|
||||
});
|
||||
|
||||
const inventory = computed(() => host.value?.inventory ?? null);
|
||||
|
||||
const windowsFields = computed(() => {
|
||||
@@ -266,6 +288,7 @@ function agentLabel(status: string) {
|
||||
async function loadHost() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
winRmTestMessage.value = "";
|
||||
try {
|
||||
host.value = await fetchHost(hostId.value);
|
||||
} catch (e) {
|
||||
@@ -275,6 +298,21 @@ async function loadHost() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runWinRmTest() {
|
||||
testingWinRm.value = true;
|
||||
winRmTestMessage.value = "";
|
||||
try {
|
||||
const result = await testHostWinRm(hostId.value);
|
||||
winRmTestOk.value = result.ok;
|
||||
winRmTestMessage.value = result.message;
|
||||
} catch (e) {
|
||||
winRmTestOk.value = false;
|
||||
winRmTestMessage.value = e instanceof Error ? e.message : "Ошибка проверки WinRM";
|
||||
} finally {
|
||||
testingWinRm.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFromLatestEvent() {
|
||||
if (refreshingLive) return;
|
||||
refreshingLive = true;
|
||||
|
||||
@@ -44,6 +44,44 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card settings-card settings-policy">
|
||||
<h2>Windows (qwinsta / logoff)</h2>
|
||||
<p class="settings-hint settings-hint-top">
|
||||
Доменный admin для удалённых команд на Windows-хостах (qwinsta, logoff через агент).
|
||||
Формат логина: <code>ДОМЕН\пользователь</code> (например <code>B26\papatramp</code>).
|
||||
Пока запись не создана в UI — используются <code>SAC_WIN_ADMIN_*</code> из <code>sac-api.env</code>.
|
||||
</p>
|
||||
<form class="settings-form" @submit.prevent="saveWinAdminSettings">
|
||||
<label class="settings-field">
|
||||
Доменный admin
|
||||
<input
|
||||
v-model="winAdminForm.user"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="B26\papatramp"
|
||||
/>
|
||||
</label>
|
||||
<label class="settings-field">
|
||||
Пароль
|
||||
<input
|
||||
v-model="winAdminForm.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="winAdminPasswordPlaceholder"
|
||||
/>
|
||||
</label>
|
||||
<p v-if="winAdminLoaded" class="settings-meta">
|
||||
Настроен: {{ winAdminLoaded.configured ? "да" : "нет" }}
|
||||
· источник: <code>{{ winAdminLoaded.source }}</code>
|
||||
</p>
|
||||
<div class="settings-actions">
|
||||
<button type="submit" :disabled="savingWinAdmin">
|
||||
{{ savingWinAdmin ? "Сохранение…" : "Сохранить Windows admin" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card settings-card settings-policy">
|
||||
<h2>Правило оповещений</h2>
|
||||
<p class="settings-hint settings-hint-top">
|
||||
@@ -436,11 +474,13 @@ import {
|
||||
fetchMobileEnrollmentCodes,
|
||||
fetchMobileSettings,
|
||||
fetchUsers,
|
||||
fetchWinAdminSettings,
|
||||
revokeMobileDevice,
|
||||
deleteMobileDevice,
|
||||
revokeMobileEnrollmentCode,
|
||||
testMobileDevicePush,
|
||||
updateMobileSettings,
|
||||
updateWinAdminSettings,
|
||||
type MobileDevice,
|
||||
type MobileEnrollmentCode,
|
||||
type MobileSettings,
|
||||
@@ -495,6 +535,13 @@ interface UiSettings {
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface WinAdminSettings {
|
||||
configured: boolean;
|
||||
user: string | null;
|
||||
password_hint: string | null;
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface SeverityOverrideRowApi {
|
||||
event_type: string;
|
||||
default_severity: string;
|
||||
@@ -508,6 +555,7 @@ interface SeverityOverrideRowUi extends SeverityOverrideRowApi {
|
||||
const loading = ref(true);
|
||||
const savingPolicy = ref(false);
|
||||
const savingUi = ref(false);
|
||||
const savingWinAdmin = ref(false);
|
||||
const savingTelegram = ref(false);
|
||||
const savingWebhook = ref(false);
|
||||
const savingEmail = ref(false);
|
||||
@@ -525,6 +573,7 @@ const webhookLoaded = ref<WebhookSettings | null>(null);
|
||||
const emailLoaded = ref<EmailSettings | null>(null);
|
||||
const policyLoaded = ref<NotificationPolicy | null>(null);
|
||||
const uiLoaded = ref<UiSettings | null>(null);
|
||||
const winAdminLoaded = ref<WinAdminSettings | null>(null);
|
||||
const severityRows = ref<SeverityOverrideRowUi[]>([]);
|
||||
const severityFilter = ref("");
|
||||
const mobileLoaded = ref<MobileSettings | null>(null);
|
||||
@@ -587,6 +636,11 @@ const uiForm = reactive({
|
||||
show_sidebar_system_stats: true,
|
||||
});
|
||||
|
||||
const winAdminForm = reactive({
|
||||
user: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const telegramForm = reactive({
|
||||
enabled: false,
|
||||
bot_token: "",
|
||||
@@ -637,6 +691,11 @@ const smtpPasswordPlaceholder = computed(() =>
|
||||
const mailToPlaceholder = computed(() =>
|
||||
emailLoaded.value?.mail_to_hint ? `оставить ${emailLoaded.value.mail_to_hint}` : "admin@example.com",
|
||||
);
|
||||
const winAdminPasswordPlaceholder = computed(() =>
|
||||
winAdminLoaded.value?.password_hint
|
||||
? `оставить ${winAdminLoaded.value.password_hint}`
|
||||
: "обязателен при первой настройке",
|
||||
);
|
||||
|
||||
const filteredSeverityRows = computed(() => {
|
||||
const q = severityFilter.value.trim().toLowerCase();
|
||||
@@ -715,6 +774,44 @@ async function saveUiSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWinAdminSettings() {
|
||||
const data = await fetchWinAdminSettings();
|
||||
winAdminLoaded.value = data;
|
||||
winAdminForm.user = data.user ?? "";
|
||||
winAdminForm.password = "";
|
||||
}
|
||||
|
||||
async function saveWinAdminSettings() {
|
||||
savingWinAdmin.value = true;
|
||||
error.value = "";
|
||||
success.value = "";
|
||||
try {
|
||||
const payload: { user?: string; password?: string } = {};
|
||||
if (winAdminForm.user.trim()) {
|
||||
payload.user = winAdminForm.user.trim();
|
||||
}
|
||||
if (winAdminForm.password.trim()) {
|
||||
payload.password = winAdminForm.password;
|
||||
}
|
||||
if (!payload.user && !payload.password) {
|
||||
error.value = "Укажите логин и/или новый пароль Windows admin";
|
||||
return;
|
||||
}
|
||||
if (!winAdminLoaded.value?.configured && (!payload.user || !payload.password)) {
|
||||
error.value = "Для первой настройки укажите логин и пароль";
|
||||
return;
|
||||
}
|
||||
winAdminLoaded.value = await updateWinAdminSettings(payload);
|
||||
winAdminForm.user = winAdminLoaded.value.user ?? winAdminForm.user;
|
||||
winAdminForm.password = "";
|
||||
success.value = "Учётные данные Windows admin сохранены.";
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Ошибка сохранения Windows admin";
|
||||
} finally {
|
||||
savingWinAdmin.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMobileSection() {
|
||||
mobileLoaded.value = await fetchMobileSettings();
|
||||
mobileForm.devices_allowed = mobileLoaded.value.devices_allowed;
|
||||
@@ -860,6 +957,7 @@ async function load() {
|
||||
emailForm.smtp_starttls = data.email.smtp_starttls;
|
||||
emailForm.smtp_ssl = data.email.smtp_ssl;
|
||||
await loadUiSettings();
|
||||
await loadWinAdminSettings();
|
||||
await loadSeverityOverrides();
|
||||
await loadMobileSection();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user