feat: hide event types from SAC UI via Settings checkbox (0.20.23)

Per-type «Показывать в событиях» in severity settings; filters lists, dashboard, hosts, and problems.
This commit is contained in:
2026-06-21 20:13:40 +10:00
parent 87ac1e09b0
commit bf7c83c408
15 changed files with 313 additions and 60 deletions
@@ -0,0 +1,34 @@
"""event type visibility settings
Revision ID: 023_event_type_visibility
Revises: 022_host_remote_action
Create Date: 2026-06-21
"""
from alembic import op
import sqlalchemy as sa
revision = "023_event_type_visibility"
down_revision = "022_host_remote_action"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"event_type_visibility",
sa.Column("event_type", sa.String(length=128), nullable=False),
sa.Column("show_in_events", sa.Boolean(), nullable=False),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("event_type"),
)
def downgrade() -> None:
op.drop_table("event_type_visibility")
+29 -18
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel from pydantic import BaseModel
@@ -11,6 +11,7 @@ from app.database import get_db
from app.models import Event, Host, Problem from app.models import Event, Host, Problem
from app.schemas.list_models import EventSummary from app.schemas.list_models import EventSummary
from app.services.event_summary import event_to_summary from app.services.event_summary import event_to_summary
from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter
from app.services.host_health import DAILY_REPORT_TYPES, HEARTBEAT_TYPE, count_stale_hosts from app.services.host_health import DAILY_REPORT_TYPES, HEARTBEAT_TYPE, count_stale_hosts
router = APIRouter(prefix="/dashboards", tags=["dashboards"]) router = APIRouter(prefix="/dashboards", tags=["dashboards"])
@@ -18,13 +19,18 @@ router = APIRouter(prefix="/dashboards", tags=["dashboards"])
def fetch_recent_events(db: Session, *, limit: int = 8) -> list[EventSummary]: def fetch_recent_events(db: Session, *, limit: int = 8) -> list[EventSummary]:
"""Последние события по времени приёма ingest (received_at).""" """Последние события по времени приёма ingest (received_at)."""
rows = db.scalars( hidden = get_hidden_event_types(db)
type_filter = visibility_type_filter(hidden)
stmt = (
select(Event) select(Event)
.join(Host) .join(Host)
.options(joinedload(Event.host)) .options(joinedload(Event.host))
.order_by(Event.received_at.desc()) .order_by(Event.received_at.desc())
.limit(limit) .limit(limit)
).all() )
if type_filter is not None:
stmt = stmt.where(type_filter)
rows = db.scalars(stmt).all()
return [event_to_summary(e, db) for e in rows] return [event_to_summary(e, db) for e in rows]
@@ -61,10 +67,13 @@ def dashboard_summary(
_user: str = Depends(get_current_user), _user: str = Depends(get_current_user),
) -> DashboardSummary: ) -> DashboardSummary:
since = datetime.now(timezone.utc) - timedelta(hours=24) since = datetime.now(timezone.utc) - timedelta(hours=24)
hidden = get_hidden_event_types(db)
type_filter = visibility_type_filter(hidden)
events_24h = db.scalar( events_24h_stmt = select(func.count()).select_from(Event).where(Event.received_at >= since)
select(func.count()).select_from(Event).where(Event.received_at >= since) if type_filter is not None:
) or 0 events_24h_stmt = events_24h_stmt.where(type_filter)
events_24h = db.scalar(events_24h_stmt) or 0
hosts_total = db.scalar(select(func.count()).select_from(Host)) or 0 hosts_total = db.scalar(select(func.count()).select_from(Host)) or 0
settings = get_settings() settings = get_settings()
hosts_stale = count_stale_hosts(db, settings.sac_heartbeat_stale_minutes) hosts_stale = count_stale_hosts(db, settings.sac_heartbeat_stale_minutes)
@@ -100,12 +109,16 @@ def dashboard_summary(
or 0 or 0
) )
top_host_rows = db.execute( top_host_stmt = (
select(Host.id, Host.hostname, Host.display_name, func.count()) select(Host.id, Host.hostname, Host.display_name, func.count())
.select_from(Event) .select_from(Event)
.join(Host, Event.host_id == Host.id) .join(Host, Event.host_id == Host.id)
.where(Event.received_at >= since) .where(Event.received_at >= since)
.group_by(Host.id, Host.hostname, Host.display_name) )
if type_filter is not None:
top_host_stmt = top_host_stmt.where(type_filter)
top_host_rows = db.execute(
top_host_stmt.group_by(Host.id, Host.hostname, Host.display_name)
.order_by(func.count().desc()) .order_by(func.count().desc())
.limit(10) .limit(10)
).all() ).all()
@@ -113,20 +126,18 @@ def dashboard_summary(
TopHostItem(host_id=row[0], hostname=row[1], display_name=row[2], count=row[3]) for row in top_host_rows TopHostItem(host_id=row[0], hostname=row[1], display_name=row[2], count=row[3]) for row in top_host_rows
] ]
top_type_stmt = select(Event.type, func.count()).where(Event.received_at >= since)
if type_filter is not None:
top_type_stmt = top_type_stmt.where(type_filter)
top_type_rows = db.execute( top_type_rows = db.execute(
select(Event.type, func.count()) top_type_stmt.group_by(Event.type).order_by(func.count().desc()).limit(10)
.where(Event.received_at >= since)
.group_by(Event.type)
.order_by(func.count().desc())
.limit(10)
).all() ).all()
top_event_types = [TopTypeItem(type=row[0], count=row[1]) for row in top_type_rows] top_event_types = [TopTypeItem(type=row[0], count=row[1]) for row in top_type_rows]
severity_rows = db.execute( severity_stmt = select(Event.severity, func.count()).where(Event.received_at >= since)
select(Event.severity, func.count()) if type_filter is not None:
.where(Event.received_at >= since) severity_stmt = severity_stmt.where(type_filter)
.group_by(Event.severity) severity_rows = db.execute(severity_stmt.group_by(Event.severity)).all()
).all()
severity_24h = {row[0]: row[1] for row in severity_rows} severity_24h = {row[0]: row[1] for row in severity_rows}
recent_events = fetch_recent_events(db, limit=8) recent_events = fetch_recent_events(db, limit=8)
+10 -1
View File
@@ -1,4 +1,4 @@
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any
@@ -22,6 +22,7 @@ from app.services.agent_commands import (
from app.services.rdg_winrm_actions import execute_logoff_via_winrm, execute_qwinsta_via_winrm from app.services.rdg_winrm_actions import execute_logoff_via_winrm, execute_qwinsta_via_winrm
from app.services.ingest import ingest_event from app.services.ingest import ingest_event
from app.services.event_summary import event_to_summary from app.services.event_summary import event_to_summary
from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter
from app.services.problems import maybe_create_problem from app.services.problems import maybe_create_problem
from app.services.schema_validate import validate_event_payload from app.services.schema_validate import validate_event_payload
from app.services.notify_dispatch import ( from app.services.notify_dispatch import (
@@ -134,8 +135,13 @@ def list_events(
db: Session = Depends(get_db), db: Session = Depends(get_db),
_user: str = Depends(get_current_user), _user: str = Depends(get_current_user),
) -> EventListResponse: ) -> EventListResponse:
hidden = get_hidden_event_types(db)
type_filter = visibility_type_filter(hidden)
stmt = select(Event).join(Host).options(joinedload(Event.host)) stmt = select(Event).join(Host).options(joinedload(Event.host))
count_stmt = select(func.count()).select_from(Event).join(Host) count_stmt = select(func.count()).select_from(Event).join(Host)
if type_filter is not None:
stmt = stmt.where(type_filter)
count_stmt = count_stmt.where(type_filter)
if severity: if severity:
stmt = stmt.where(Event.severity == severity) stmt = stmt.where(Event.severity == severity)
@@ -261,11 +267,14 @@ def get_event(
db: Session = Depends(get_db), db: Session = Depends(get_db),
_user: str = Depends(get_current_user), _user: str = Depends(get_current_user),
) -> EventDetail: ) -> EventDetail:
hidden = get_hidden_event_types(db)
event = db.scalar( event = db.scalar(
select(Event).where(Event.id == event_db_id).options(joinedload(Event.host)) select(Event).where(Event.id == event_db_id).options(joinedload(Event.host))
) )
if event is None: if event is None:
raise HTTPException(status_code=404, detail="Event not found") raise HTTPException(status_code=404, detail="Event not found")
if event.type in hidden:
raise HTTPException(status_code=404, detail="Event not found")
base = event_to_summary(event, db) base = event_to_summary(event, db)
return EventDetail( return EventDetail(
**base.model_dump(), **base.model_dump(),
+14 -7
View File
@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -56,10 +56,19 @@ from app.services.host_health import (
max_daily_report_time_by_host, max_daily_report_time_by_host,
max_event_time_by_host, max_event_time_by_host,
) )
from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter
router = APIRouter(prefix="/hosts", tags=["hosts"]) router = APIRouter(prefix="/hosts", tags=["hosts"])
def _count_visible_events(db: Session, host_id: int, hidden: frozenset[str]) -> int:
stmt = select(func.count()).select_from(Event).where(Event.host_id == host_id)
type_filter = visibility_type_filter(hidden)
if type_filter is not None:
stmt = stmt.where(type_filter)
return int(db.scalar(stmt) or 0)
@router.get("", response_model=HostListResponse) @router.get("", response_model=HostListResponse)
def list_hosts( def list_hosts(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
@@ -108,12 +117,11 @@ def list_hosts(
latest_map, latest_map,
git_versions=git_release.versions, git_versions=git_release.versions,
) )
hidden = get_hidden_event_types(db)
items: list[HostSummary] = [] items: list[HostSummary] = []
for host in rows: for host in rows:
event_count = db.scalar( event_count = _count_visible_events(db, host.id, hidden)
select(func.count()).select_from(Event).where(Event.host_id == host.id)
)
last_hb = hb_map.get(host.id) last_hb = hb_map.get(host.id)
items.append( items.append(
HostSummary( HostSummary(
@@ -161,9 +169,8 @@ def _host_detail_from_model(
) -> HostDetail: ) -> HostDetail:
hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE) hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE)
report_map = max_daily_report_time_by_host(db) report_map = max_daily_report_time_by_host(db)
event_count = db.scalar( hidden = get_hidden_event_types(db)
select(func.count()).select_from(Event).where(Event.host_id == host.id) event_count = _count_visible_events(db, host.id, hidden)
)
last_hb = hb_map.get(host.id) last_hb = hb_map.get(host.id)
latest_map = latest_agent_versions_by_product(db) latest_map = latest_agent_versions_by_product(db)
update_cfg = get_effective_agent_update_config(db) update_cfg = get_effective_agent_update_config(db)
+4 -1
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@@ -17,6 +17,7 @@ from app.auth.jwt_auth import get_current_user
from app.database import get_db from app.database import get_db
from app.models import Event, Host, Problem, ProblemEvent from app.models import Event, Host, Problem, ProblemEvent
from app.services.event_type_visibility import get_hidden_event_types
@@ -269,6 +270,8 @@ def get_problem(
.order_by(Event.occurred_at.desc()) .order_by(Event.occurred_at.desc())
).all() ).all()
hidden = get_hidden_event_types(db)
event_rows = [event for event in event_rows if event.type not in hidden]
+30 -18
View File
@@ -1,4 +1,4 @@
from dataclasses import replace from dataclasses import replace
from datetime import datetime from datetime import datetime
@@ -37,6 +37,7 @@ from app.services.event_severity_overrides import (
list_severity_override_items, list_severity_override_items,
replace_severity_overrides, replace_severity_overrides,
) )
from app.services.event_type_visibility import replace_event_visibility
from app.services.ui_settings import ( from app.services.ui_settings import (
get_effective_ui_settings, get_effective_ui_settings,
upsert_ui_settings, upsert_ui_settings,
@@ -375,6 +376,7 @@ class EventSeverityOverrideRow(BaseModel):
event_type: str event_type: str
default_severity: str default_severity: str
override_severity: str | None = None override_severity: str | None = None
show_in_events: bool = True
class EventSeverityOverridesResponse(BaseModel): class EventSeverityOverridesResponse(BaseModel):
@@ -384,8 +386,25 @@ class EventSeverityOverridesResponse(BaseModel):
class EventSeverityOverridesUpdate(BaseModel): class EventSeverityOverridesUpdate(BaseModel):
overrides: dict[str, str | None] = Field( overrides: dict[str, str | None] = Field(
default_factory=dict,
description="event_type → severity; null или пустая строка сбрасывает override", description="event_type → severity; null или пустая строка сбрасывает override",
) )
visibility: dict[str, bool | None] = Field(
default_factory=dict,
description="event_type → show_in_events; null или true сбрасывает скрытие (показывать)",
)
def _severity_override_rows(items) -> list[EventSeverityOverrideRow]:
return [
EventSeverityOverrideRow(
event_type=i.event_type,
default_severity=i.default_severity,
override_severity=i.override_severity,
show_in_events=i.show_in_events,
)
for i in items
]
@router.get("/notifications/severity-overrides", response_model=EventSeverityOverridesResponse) @router.get("/notifications/severity-overrides", response_model=EventSeverityOverridesResponse)
@@ -395,14 +414,7 @@ def get_severity_overrides(
) -> EventSeverityOverridesResponse: ) -> EventSeverityOverridesResponse:
items = list_severity_override_items(db) items = list_severity_override_items(db)
return EventSeverityOverridesResponse( return EventSeverityOverridesResponse(
items=[ items=_severity_override_rows(items),
EventSeverityOverrideRow(
event_type=i.event_type,
default_severity=i.default_severity,
override_severity=i.override_severity,
)
for i in items
],
source=SOURCE_DB, source=SOURCE_DB,
) )
@@ -414,19 +426,19 @@ def update_severity_overrides(
_user=Depends(require_admin), _user=Depends(require_admin),
) -> EventSeverityOverridesResponse: ) -> EventSeverityOverridesResponse:
try: try:
items = replace_severity_overrides(db, body.overrides) items = list_severity_override_items(db)
if body.overrides:
items = replace_severity_overrides(db, body.overrides)
if body.visibility:
replace_event_visibility(db, body.visibility)
items = list_severity_override_items(db)
if not body.overrides and not body.visibility:
raise HTTPException(status_code=422, detail="Нет изменений для сохранения")
db.commit() db.commit()
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc raise HTTPException(status_code=422, detail=str(exc)) from exc
return EventSeverityOverridesResponse( return EventSeverityOverridesResponse(
items=[ items=_severity_override_rows(items),
EventSeverityOverrideRow(
event_type=i.event_type,
default_severity=i.default_severity,
override_severity=i.override_severity,
)
for i in items
],
source=SOURCE_DB, source=SOURCE_DB,
) )
+3 -1
View File
@@ -1,4 +1,4 @@
from app.models.agent_command import AgentCommand from app.models.agent_command import AgentCommand
from app.models.api_key import ApiKey from app.models.api_key import ApiKey
from app.models.event import Event from app.models.event import Event
from app.models.host import Host from app.models.host import Host
@@ -9,6 +9,7 @@ from app.models.problem import Problem, ProblemEvent
from app.models.login_attempt import LoginAttempt from app.models.login_attempt import LoginAttempt
from app.models.ui_settings import UiSettings from app.models.ui_settings import UiSettings
from app.models.event_severity_override import EventSeverityOverride from app.models.event_severity_override import EventSeverityOverride
from app.models.event_type_visibility import EventTypeVisibility
from app.models.user import User from app.models.user import User
from app.models.mobile_settings import MobileSettings from app.models.mobile_settings import MobileSettings
from app.models.mobile_enrollment_code import MobileEnrollmentCode from app.models.mobile_enrollment_code import MobileEnrollmentCode
@@ -27,6 +28,7 @@ __all__ = [
"ProblemEvent", "ProblemEvent",
"User", "User",
"EventSeverityOverride", "EventSeverityOverride",
"EventTypeVisibility",
"LoginAttempt", "LoginAttempt",
"UiSettings", "UiSettings",
"MobileSettings", "MobileSettings",
@@ -0,0 +1,21 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class EventTypeVisibility(Base):
"""Per event-type UI visibility (default: show in events lists)."""
__tablename__ = "event_type_visibility"
event_type: Mapped[str] = mapped_column(String(128), primary_key=True)
show_in_events: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
@@ -1,4 +1,4 @@
"""Per event-type severity overrides (admin Settings UI).""" """Per event-type severity overrides (admin Settings UI)."""
from __future__ import annotations from __future__ import annotations
@@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
from app.constants.event_types import DEFAULT_EVENT_SEVERITIES from app.constants.event_types import DEFAULT_EVENT_SEVERITIES
from app.models.event_severity_override import EventSeverityOverride from app.models.event_severity_override import EventSeverityOverride
from app.services.event_type_visibility import get_visibility_map
from app.services.notification_settings import VALID_SEVERITIES from app.services.notification_settings import VALID_SEVERITIES
SOURCE_DB = "db" SOURCE_DB = "db"
@@ -19,6 +20,7 @@ class EventSeverityOverrideItem:
event_type: str event_type: str
default_severity: str default_severity: str
override_severity: str | None override_severity: str | None
show_in_events: bool = True
def get_override_map(db: Session) -> dict[str, str]: def get_override_map(db: Session) -> dict[str, str]:
@@ -48,7 +50,8 @@ def apply_severity_override(payload: dict, db: Session) -> dict:
def list_severity_override_items(db: Session) -> list[EventSeverityOverrideItem]: def list_severity_override_items(db: Session) -> list[EventSeverityOverrideItem]:
override_map = get_override_map(db) override_map = get_override_map(db)
types = set(DEFAULT_EVENT_SEVERITIES) | set(override_map) visibility_map = get_visibility_map(db)
types = set(DEFAULT_EVENT_SEVERITIES) | set(override_map) | set(visibility_map)
items: list[EventSeverityOverrideItem] = [] items: list[EventSeverityOverrideItem] = []
for event_type in sorted(types): for event_type in sorted(types):
default = DEFAULT_EVENT_SEVERITIES.get(event_type, "info") default = DEFAULT_EVENT_SEVERITIES.get(event_type, "info")
@@ -57,6 +60,7 @@ def list_severity_override_items(db: Session) -> list[EventSeverityOverrideItem]
event_type=event_type, event_type=event_type,
default_severity=default, default_severity=default,
override_severity=override_map.get(event_type), override_severity=override_map.get(event_type),
show_in_events=visibility_map.get(event_type, True),
) )
) )
return items return items
@@ -0,0 +1,58 @@
"""Hide selected event types from SAC UI/API event lists."""
from __future__ import annotations
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from sqlalchemy.sql import ColumnElement
from app.models.event import Event
from app.models.event_type_visibility import EventTypeVisibility
def get_hidden_event_types(db: Session) -> frozenset[str]:
rows = db.scalars(
select(EventTypeVisibility.event_type).where(EventTypeVisibility.show_in_events.is_(False))
).all()
return frozenset(rows)
def get_visibility_map(db: Session) -> dict[str, bool]:
rows = db.scalars(select(EventTypeVisibility)).all()
return {row.event_type: row.show_in_events for row in rows}
def show_in_events_for(event_type: str, *, hidden: frozenset[str]) -> bool:
return event_type not in hidden
def visibility_type_filter(hidden: frozenset[str]) -> ColumnElement[bool] | None:
if not hidden:
return None
return Event.type.not_in(tuple(sorted(hidden)))
def replace_event_visibility(db: Session, visibility: dict[str, bool | None]) -> dict[str, bool]:
stored = get_visibility_map(db)
for event_type, show in visibility.items():
normalized_type = event_type.strip()
if not normalized_type:
raise ValueError("event_type is required")
if show is None or show is True:
db.execute(
delete(EventTypeVisibility).where(EventTypeVisibility.event_type == normalized_type)
)
stored.pop(normalized_type, None)
continue
if show is False:
row = db.get(EventTypeVisibility, normalized_type)
if row is None:
row = EventTypeVisibility(event_type=normalized_type, show_in_events=False)
db.add(row)
else:
row.show_in_events = False
stored[normalized_type] = False
else:
raise ValueError("show_in_events must be true, false, or null")
db.flush()
return stored
+1 -1
View File
@@ -1,5 +1,5 @@
"""Единый источник версии SAC (API, health, логи, OpenAPI).""" """Единый источник версии SAC (API, health, логи, OpenAPI)."""
APP_NAME = "Security Alert Center" APP_NAME = "Security Alert Center"
APP_VERSION = "0.20.22" APP_VERSION = "0.20.23"
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
@@ -0,0 +1,76 @@
"""Tests for hiding event types from SAC UI."""
import uuid
from app.models.event_type_visibility import EventTypeVisibility
VALID_EVENT = {
"schema_version": "1.0",
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"occurred_at": "2026-05-27T10:00:00+03:00",
"source": {"product": "rdp-login-monitor", "product_version": "1.2.3-SAC"},
"host": {"hostname": "test-host", "os_family": "windows"},
"category": "auth",
"type": "agent.heartbeat",
"severity": "info",
"title": "Heartbeat",
"summary": "pytest",
}
LOGIN_EVENT = {
**VALID_EVENT,
"type": "rdp.login.success",
"title": "RDP login",
"summary": "login pytest",
}
def _ingest(client, auth_headers, payload):
event_id = str(uuid.uuid4())
r = client.post("/api/v1/events", json={**payload, "event_id": event_id}, headers=auth_headers)
assert r.status_code == 201
return event_id
def test_hidden_event_type_excluded_from_list(client, auth_headers, jwt_headers, db_session):
db_session.add(EventTypeVisibility(event_type="agent.heartbeat", show_in_events=False))
db_session.commit()
hb_id = _ingest(client, auth_headers, VALID_EVENT)
login_id = _ingest(client, auth_headers, LOGIN_EVENT)
listed = client.get("/api/v1/events", headers=jwt_headers)
assert listed.status_code == 200
ids = {item["event_id"] for item in listed.json()["items"]}
assert hb_id not in ids
assert login_id in ids
def test_hidden_event_type_returns_404_on_detail(client, auth_headers, jwt_headers, db_session):
db_session.add(EventTypeVisibility(event_type="agent.heartbeat", show_in_events=False))
db_session.commit()
_ingest(client, auth_headers, VALID_EVENT)
listed = client.get("/api/v1/events", headers=jwt_headers, params={"type": "agent.heartbeat"})
assert listed.json()["total"] == 0
def test_visibility_settings_api(client, jwt_headers, db_session):
put = client.put(
"/api/v1/settings/notifications/severity-overrides",
headers=jwt_headers,
json={"visibility": {"agent.heartbeat": False}},
)
assert put.status_code == 200
items = {i["event_type"]: i for i in put.json()["items"]}
assert items["agent.heartbeat"]["show_in_events"] is False
assert items["rdp.login.success"]["show_in_events"] is True
reset = client.put(
"/api/v1/settings/notifications/severity-overrides",
headers=jwt_headers,
json={"visibility": {"agent.heartbeat": None}},
)
assert reset.status_code == 200
items = {i["event_type"]: i for i in reset.json()["items"]}
assert items["agent.heartbeat"]["show_in_events"] is True
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "sac-ui", "name": "sac-ui",
"private": true, "private": true,
"version": "0.11.0", "version": "0.11.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+1 -1
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_NAME = "Security Alert Center";
export const APP_VERSION = "0.20.22"; export const APP_VERSION = "0.20.23";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
+24 -8
View File
@@ -397,6 +397,7 @@
<p class="settings-hint settings-hint-top"> <p class="settings-hint settings-hint-top">
Переопределение значимости для новых событий после ingest. Исходное severity агента сохраняется в Переопределение значимости для новых событий после ingest. Исходное severity агента сохраняется в
<code>details.severity_agent</code>. Пустое значение как прислал агент (см. «По умолчанию»). <code>details.severity_agent</code>. Пустое значение как прислал агент (см. «По умолчанию»).
Снимите «Показывать в событиях», чтобы скрыть тип из списков SAC (например <code>agent.heartbeat</code>).
</p> </p>
<div class="settings-severity-toolbar"> <div class="settings-severity-toolbar">
<input v-model="severityFilter" type="search" placeholder="Фильтр по type…" /> <input v-model="severityFilter" type="search" placeholder="Фильтр по type…" />
@@ -411,6 +412,7 @@
<th>Тип события</th> <th>Тип события</th>
<th>По умолчанию</th> <th>По умолчанию</th>
<th>Effective</th> <th>Effective</th>
<th>Показывать в событиях</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -426,6 +428,12 @@
<option value="critical">critical</option> <option value="critical">critical</option>
</select> </select>
</td> </td>
<td class="settings-severity-show-cell">
<label>
<input v-model="row.show_in_events" type="checkbox" />
показывать
</label>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -678,10 +686,12 @@ interface SeverityOverrideRowApi {
event_type: string; event_type: string;
default_severity: string; default_severity: string;
override_severity: string | null; override_severity: string | null;
show_in_events: boolean;
} }
interface SeverityOverrideRowUi extends SeverityOverrideRowApi { interface SeverityOverrideRowUi extends SeverityOverrideRowApi {
selected: string; selected: string;
override_show_in_events: boolean;
} }
const loading = ref(true); const loading = ref(true);
@@ -868,6 +878,7 @@ function mapSeverityRows(items: SeverityOverrideRowApi[]): SeverityOverrideRowUi
return items.map((row) => ({ return items.map((row) => ({
...row, ...row,
selected: row.override_severity ?? "", selected: row.override_severity ?? "",
override_show_in_events: row.show_in_events,
})); }));
} }
@@ -884,25 +895,30 @@ async function saveSeverityOverrides() {
success.value = ""; success.value = "";
try { try {
const overrides: Record<string, string | null> = {}; const overrides: Record<string, string | null> = {};
const visibility: Record<string, boolean | null> = {};
for (const row of severityRows.value) { for (const row of severityRows.value) {
const prev = row.override_severity ?? ""; const prevSeverity = row.override_severity ?? "";
const next = row.selected.trim(); const nextSeverity = row.selected.trim();
if (next === prev) continue; if (nextSeverity !== prevSeverity) {
overrides[row.event_type] = next || null; overrides[row.event_type] = nextSeverity || null;
}
if (row.show_in_events !== (row.override_show_in_events ?? true)) {
visibility[row.event_type] = row.show_in_events ? null : false;
}
} }
if (Object.keys(overrides).length === 0) { if (Object.keys(overrides).length === 0 && Object.keys(visibility).length === 0) {
error.value = "Нет изменений severity для сохранения"; error.value = "Нет изменений для сохранения";
return; return;
} }
const data = await apiFetch<{ items: SeverityOverrideRowApi[] }>( const data = await apiFetch<{ items: SeverityOverrideRowApi[] }>(
"/api/v1/settings/notifications/severity-overrides", "/api/v1/settings/notifications/severity-overrides",
{ {
method: "PUT", method: "PUT",
body: JSON.stringify({ overrides }), body: JSON.stringify({ overrides, visibility }),
}, },
); );
severityRows.value = mapSeverityRows(data.items); severityRows.value = mapSeverityRows(data.items);
success.value = "Severity по типам событий сохранён."; success.value = "Severity и видимость событий сохранены.";
} catch (e) { } catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка сохранения severity"; error.value = e instanceof Error ? e.message : "Ошибка сохранения severity";
} finally { } finally {