feat: git-based agent version reference and unified outdated badge (0.20.0)

SAC reads latest RDP/ssh-monitor versions from configured git repos for outdated detection, update targets, and settings test-git.
This commit is contained in:
PTah
2026-06-20 16:25:53 +10:00
parent d820d941c1
commit f411d8f070
22 changed files with 932 additions and 45 deletions
+23
View File
@@ -267,6 +267,8 @@ export interface HostListResponse {
page: number;
page_size: number;
latest_agent_versions?: Record<string, string>;
/** Эталон для «устарела»: max(git latest, min, max в fleet) */
reference_agent_versions?: Record<string, string>;
}
export interface ProblemSummary {
@@ -548,9 +550,24 @@ export interface AgentUpdateSettings {
min_rdp_version: string | null;
min_ssh_version: string | null;
win_agent_update_script: string | null;
rdp_git_repo_url: string | null;
ssh_git_repo_url: string | null;
git_branch: string;
git_latest_rdp_version: string | null;
git_latest_ssh_version: string | null;
git_fetched_at: string | null;
git_errors: Record<string, string>;
source: string;
}
export interface AgentGitTestResult {
git_latest_rdp_version: string | null;
git_latest_ssh_version: string | null;
git_fetched_at: string | null;
git_errors: Record<string, string>;
from_cache: boolean;
}
export function fetchAgentUpdateSettings(): Promise<AgentUpdateSettings> {
return apiFetch<AgentUpdateSettings>("/api/v1/settings/agent-updates");
}
@@ -564,6 +581,12 @@ export function saveAgentUpdateSettings(
});
}
export function testAgentGitRelease(): Promise<AgentGitTestResult> {
return apiFetch<AgentGitTestResult>("/api/v1/settings/agent-updates/test-git", {
method: "POST",
});
}
export interface DashboardSummary {
events_last_24h: number;
hosts_total: number;
+3
View File
@@ -13,6 +13,9 @@ export function patchHostSummaryFromDetail(target: HostSummary, detail: HostDeta
target.last_daily_report_at = detail.last_daily_report_at;
target.last_inventory_at = detail.last_inventory_at;
target.agent_status = detail.agent_status;
if (detail.version_status) {
target.version_status = detail.version_status;
}
}
export function bumpLatestAgentVersion(
+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.12.1";
export const APP_VERSION = "0.20.0";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
+13 -6
View File
@@ -202,16 +202,23 @@ function agentLabel(status: string) {
function isVersionOutdated(h: HostSummary): boolean {
if (h.version_status === "outdated") return true;
if (h.version_status === "ok") return false;
const latest = data.value?.latest_agent_versions?.[h.product];
return isAgentVersionOutdated(h.product_version, latest);
const reference =
data.value?.reference_agent_versions?.[h.product] ??
data.value?.latest_agent_versions?.[h.product];
return isAgentVersionOutdated(h.product_version, reference);
}
function versionTitle(h: HostSummary): string {
const latest = data.value?.latest_agent_versions?.[h.product];
const reference =
data.value?.reference_agent_versions?.[h.product] ??
data.value?.latest_agent_versions?.[h.product];
if (!isVersionOutdated(h)) return "";
return latest
? `Устарела относительно последней для ${h.product}: ${latest}`
: "Устаревшая версия агента";
if (!reference) return "Устаревшая версия агента";
const fromSettings = data.value?.reference_agent_versions?.[h.product];
if (fromSettings && fromSettings !== data.value?.latest_agent_versions?.[h.product]) {
return `Устарела относительно эталона ${reference} (настройки SAC)`;
}
return `Устарела относительно эталона ${reference}`;
}
function openHost(id: number) {
+78 -10
View File
@@ -126,6 +126,8 @@
Режим <code>gpo</code> обновления вручную/GPO; SAC только показывает устаревшие версии.
Режим <code>sac</code> «Запросить обновление» ставит флаг в poll; агент self-update;
через {{ agentUpdateForm.fallback_after_minutes }} мин без ответа fallback SSH/WinRM с SAC.
<strong>Эталон «устарела»</strong> берётся из <code>version.txt</code> / скриптов в git-репозиториях
(ветка <code>main</code> по умолчанию); при недоступности git fallback на max версию в fleet.
</p>
<form class="settings-form" @submit.prevent="saveAgentUpdateSettingsForm">
<label class="settings-field">
@@ -144,13 +146,34 @@
<input v-model.number="agentUpdateForm.fallback_after_minutes" type="number" min="1" max="1440" />
</label>
<label class="settings-field">
Recommended RDP-login-monitor
<input v-model="agentUpdateForm.recommended_rdp_version" type="text" placeholder="2.1.0-SAC" />
Git: RDP-login-monitor
<input
v-model="agentUpdateForm.rdp_git_repo_url"
type="text"
placeholder="https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git"
/>
</label>
<label class="settings-field">
Recommended ssh-monitor
<input v-model="agentUpdateForm.recommended_ssh_version" type="text" placeholder="2.1.0-SAC" />
Git: ssh-monitor
<input
v-model="agentUpdateForm.ssh_git_repo_url"
type="text"
placeholder="https://git.kalinamall.ru/PapaTramp/ssh-monitor.git"
/>
</label>
<label class="settings-field">
Git-ветка
<input v-model="agentUpdateForm.git_branch" type="text" placeholder="main" />
</label>
<p v-if="agentUpdateLoaded?.git_latest_rdp_version || agentUpdateLoaded?.git_latest_ssh_version" class="settings-meta">
Latest из git:
RDP <code>{{ agentUpdateLoaded?.git_latest_rdp_version ?? "—" }}</code>,
SSH <code>{{ agentUpdateLoaded?.git_latest_ssh_version ?? "—" }}</code>
<span v-if="agentUpdateLoaded?.git_fetched_at">
· обновлено {{ formatDateTime(agentUpdateLoaded.git_fetched_at) }}
</span>
</p>
<p v-if="agentGitTestErrors" class="settings-error-inline">{{ agentGitTestErrors }}</p>
<label class="settings-field">
WinRM: путь к Deploy-LoginMonitor.ps1 (fallback)
<input
@@ -163,6 +186,9 @@
Источник: <code>{{ agentUpdateLoaded.source }}</code>
</p>
<div class="settings-actions">
<button type="button" :disabled="testingAgentGit" @click="runAgentGitTest">
{{ testingAgentGit ? "Проверка git" : "Проверить git" }}
</button>
<button type="submit" :disabled="savingAgentUpdate">
{{ savingAgentUpdate ? "Сохранение…" : "Сохранить обновления агентов" }}
</button>
@@ -566,6 +592,7 @@ import {
fetchLinuxAdminSettings,
fetchAgentUpdateSettings,
saveAgentUpdateSettings,
testAgentGitRelease,
revokeMobileDevice,
deleteMobileDevice,
revokeMobileEnrollmentCode,
@@ -658,6 +685,8 @@ const savingUi = ref(false);
const savingWinAdmin = ref(false);
const savingLinuxAdmin = ref(false);
const savingAgentUpdate = ref(false);
const testingAgentGit = ref(false);
const agentGitTestErrors = ref("");
const savingTelegram = ref(false);
const savingWebhook = ref(false);
const savingEmail = ref(false);
@@ -754,8 +783,9 @@ const agentUpdateForm = reactive({
mode: "gpo" as "gpo" | "sac",
fallback_enabled: true,
fallback_after_minutes: 15,
recommended_rdp_version: "",
recommended_ssh_version: "",
rdp_git_repo_url: "",
ssh_git_repo_url: "",
git_branch: "main",
win_agent_update_script: "",
});
@@ -973,15 +1003,51 @@ async function saveLinuxAdminSettings() {
}
}
function formatDateTime(iso: string | null | undefined): string {
if (!iso) return "—";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleString("ru-RU");
}
async function loadAgentUpdateSettings() {
const data = await fetchAgentUpdateSettings();
agentUpdateLoaded.value = data;
agentUpdateForm.mode = data.mode;
agentUpdateForm.fallback_enabled = data.fallback_enabled;
agentUpdateForm.fallback_after_minutes = data.fallback_after_minutes;
agentUpdateForm.recommended_rdp_version = data.recommended_rdp_version ?? "";
agentUpdateForm.recommended_ssh_version = data.recommended_ssh_version ?? "";
agentUpdateForm.rdp_git_repo_url = data.rdp_git_repo_url ?? "";
agentUpdateForm.ssh_git_repo_url = data.ssh_git_repo_url ?? "";
agentUpdateForm.git_branch = data.git_branch ?? "main";
agentUpdateForm.win_agent_update_script = data.win_agent_update_script ?? "";
agentGitTestErrors.value = Object.values(data.git_errors ?? {}).join("; ");
}
async function runAgentGitTest() {
testingAgentGit.value = true;
agentGitTestErrors.value = "";
error.value = "";
try {
const result = await testAgentGitRelease();
if (agentUpdateLoaded.value) {
agentUpdateLoaded.value = {
...agentUpdateLoaded.value,
git_latest_rdp_version: result.git_latest_rdp_version,
git_latest_ssh_version: result.git_latest_ssh_version,
git_fetched_at: result.git_fetched_at,
git_errors: result.git_errors,
};
}
const errs = Object.values(result.git_errors ?? {});
agentGitTestErrors.value = errs.length ? errs.join("; ") : "";
success.value = result.from_cache
? "Версии из кэша (git не менялся)."
: "Версии успешно прочитаны из git.";
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка проверки git";
} finally {
testingAgentGit.value = false;
}
}
async function saveAgentUpdateSettingsForm() {
@@ -993,10 +1059,12 @@ async function saveAgentUpdateSettingsForm() {
mode: agentUpdateForm.mode,
fallback_enabled: agentUpdateForm.fallback_enabled,
fallback_after_minutes: agentUpdateForm.fallback_after_minutes,
recommended_rdp_version: agentUpdateForm.recommended_rdp_version.trim() || null,
recommended_ssh_version: agentUpdateForm.recommended_ssh_version.trim() || null,
rdp_git_repo_url: agentUpdateForm.rdp_git_repo_url.trim() || null,
ssh_git_repo_url: agentUpdateForm.ssh_git_repo_url.trim() || null,
git_branch: agentUpdateForm.git_branch.trim() || "main",
win_agent_update_script: agentUpdateForm.win_agent_update_script.trim() || null,
});
agentGitTestErrors.value = Object.values(agentUpdateLoaded.value.git_errors ?? {}).join("; ");
success.value = "Настройки обновления агентов сохранены.";
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка сохранения agent-updates";