fix: harden SAC security per audit (0.4.15)

Close audit findings: anti-spoof client IP for login rate limit, JWT/CORS
enforce on startup, SSH host-key verification and sudo via stdin, optional
WinRM HTTPS, SSE httpOnly cookie auth, ingest body limit, ILIKE escaping,
and HMAC API key hashing with legacy SHA-256 fallback.
This commit is contained in:
2026-07-07 19:32:12 +10:00
parent 939fe122c5
commit 80320ae698
29 changed files with 452 additions and 160 deletions
+10 -2
View File
@@ -1,4 +1,4 @@
const TOKEN_KEY = "sac_token";
const TOKEN_KEY = "sac_token";
const ROLE_KEY = "sac_role";
export function getToken(): string | null {
@@ -31,6 +31,14 @@ export function clearToken(): void {
localStorage.removeItem(ROLE_KEY);
}
export async function logoutApi(): Promise<void> {
try {
await fetch("/api/v1/auth/logout", { method: "POST", credentials: "same-origin" });
} catch {
/* ignore network errors on logout */
}
}
export class ApiError extends Error {
status: number;
@@ -55,7 +63,7 @@ export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
const res = await fetch(path, { ...init, headers });
const res = await fetch(path, { ...init, headers, credentials: "same-origin" });
const text = await res.text();
if (res.status === 401) {
if (hadToken) {
+6 -4
View File
@@ -1,4 +1,4 @@
<template>
<template>
<aside class="sac-sidebar">
<div class="sac-sidebar-brand">
<img src="/sac-icon.png" alt="" class="sac-brand-logo" width="40" height="40" />
@@ -42,7 +42,7 @@
<script setup lang="ts">
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { clearToken, isAdmin } from "../api";
import { clearToken, isAdmin, logoutApi } from "../api";
import SidebarSystemStats from "./SidebarSystemStats.vue";
import { useSacVersion } from "../composables/useSacVersion";
import {
@@ -82,8 +82,10 @@ function isActive(item: (typeof navItems.value)[number]) {
}
function logout() {
clearToken();
router.push("/login");
void logoutApi().finally(() => {
clearToken();
router.push("/login");
});
}
</script>
@@ -1,4 +1,4 @@
import { onMounted, onUnmounted, ref } from "vue";
import { onMounted, onUnmounted, ref } from "vue";
import { getToken } from "../api";
export interface SacDashboardStreamMessage {
@@ -41,7 +41,7 @@ export function useSacLiveEventStream(onNewEvent: () => void) {
if (!token) return;
eventSource?.close();
const url = `/api/v1/stream/events?token=${encodeURIComponent(token)}`;
const url = `/api/v1/stream/events`;
eventSource = new EventSource(url);
eventSource.onopen = () => {
+2 -2
View File
@@ -1,4 +1,4 @@
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.4.11";
export const APP_VERSION = "0.4.15";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
+2 -2
View File
@@ -1,4 +1,4 @@
<template>
<template>
<div class="overview-page">
<h1>Обзор</h1>
@@ -556,7 +556,7 @@ function connectLive() {
eventSource?.close();
const url = `/api/v1/stream/events?token=${encodeURIComponent(token)}`;
const url = `/api/v1/stream/events`;
eventSource = new EventSource(url);