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:
@@ -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")
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
@@ -11,6 +11,7 @@ from app.database import get_db
|
||||
from app.models import Event, Host, Problem
|
||||
from app.schemas.list_models import EventSummary
|
||||
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
|
||||
|
||||
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]:
|
||||
"""Последние события по времени приёма ingest (received_at)."""
|
||||
rows = db.scalars(
|
||||
hidden = get_hidden_event_types(db)
|
||||
type_filter = visibility_type_filter(hidden)
|
||||
stmt = (
|
||||
select(Event)
|
||||
.join(Host)
|
||||
.options(joinedload(Event.host))
|
||||
.order_by(Event.received_at.desc())
|
||||
.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]
|
||||
|
||||
|
||||
@@ -61,10 +67,13 @@ def dashboard_summary(
|
||||
_user: str = Depends(get_current_user),
|
||||
) -> DashboardSummary:
|
||||
since = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
hidden = get_hidden_event_types(db)
|
||||
type_filter = visibility_type_filter(hidden)
|
||||
|
||||
events_24h = db.scalar(
|
||||
select(func.count()).select_from(Event).where(Event.received_at >= since)
|
||||
) or 0
|
||||
events_24h_stmt = select(func.count()).select_from(Event).where(Event.received_at >= since)
|
||||
if type_filter is not None:
|
||||
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
|
||||
settings = get_settings()
|
||||
hosts_stale = count_stale_hosts(db, settings.sac_heartbeat_stale_minutes)
|
||||
@@ -100,12 +109,16 @@ def dashboard_summary(
|
||||
or 0
|
||||
)
|
||||
|
||||
top_host_rows = db.execute(
|
||||
top_host_stmt = (
|
||||
select(Host.id, Host.hostname, Host.display_name, func.count())
|
||||
.select_from(Event)
|
||||
.join(Host, Event.host_id == Host.id)
|
||||
.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())
|
||||
.limit(10)
|
||||
).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
|
||||
]
|
||||
|
||||
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(
|
||||
select(Event.type, func.count())
|
||||
.where(Event.received_at >= since)
|
||||
.group_by(Event.type)
|
||||
.order_by(func.count().desc())
|
||||
.limit(10)
|
||||
top_type_stmt.group_by(Event.type).order_by(func.count().desc()).limit(10)
|
||||
).all()
|
||||
top_event_types = [TopTypeItem(type=row[0], count=row[1]) for row in top_type_rows]
|
||||
|
||||
severity_rows = db.execute(
|
||||
select(Event.severity, func.count())
|
||||
.where(Event.received_at >= since)
|
||||
.group_by(Event.severity)
|
||||
).all()
|
||||
severity_stmt = select(Event.severity, func.count()).where(Event.received_at >= since)
|
||||
if type_filter is not None:
|
||||
severity_stmt = severity_stmt.where(type_filter)
|
||||
severity_rows = db.execute(severity_stmt.group_by(Event.severity)).all()
|
||||
severity_24h = {row[0]: row[1] for row in severity_rows}
|
||||
|
||||
recent_events = fetch_recent_events(db, limit=8)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logging
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
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.ingest import ingest_event
|
||||
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.schema_validate import validate_event_payload
|
||||
from app.services.notify_dispatch import (
|
||||
@@ -134,8 +135,13 @@ def list_events(
|
||||
db: Session = Depends(get_db),
|
||||
_user: str = Depends(get_current_user),
|
||||
) -> EventListResponse:
|
||||
hidden = get_hidden_event_types(db)
|
||||
type_filter = visibility_type_filter(hidden)
|
||||
stmt = select(Event).join(Host).options(joinedload(Event.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:
|
||||
stmt = stmt.where(Event.severity == severity)
|
||||
@@ -261,11 +267,14 @@ def get_event(
|
||||
db: Session = Depends(get_db),
|
||||
_user: str = Depends(get_current_user),
|
||||
) -> EventDetail:
|
||||
hidden = get_hidden_event_types(db)
|
||||
event = db.scalar(
|
||||
select(Event).where(Event.id == event_db_id).options(joinedload(Event.host))
|
||||
)
|
||||
if event is None:
|
||||
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)
|
||||
return EventDetail(
|
||||
**base.model_dump(),
|
||||
|
||||
@@ -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 sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -56,10 +56,19 @@ from app.services.host_health import (
|
||||
max_daily_report_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"])
|
||||
|
||||
|
||||
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)
|
||||
def list_hosts(
|
||||
page: int = Query(1, ge=1),
|
||||
@@ -108,12 +117,11 @@ def list_hosts(
|
||||
latest_map,
|
||||
git_versions=git_release.versions,
|
||||
)
|
||||
hidden = get_hidden_event_types(db)
|
||||
|
||||
items: list[HostSummary] = []
|
||||
for host in rows:
|
||||
event_count = db.scalar(
|
||||
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)
|
||||
items.append(
|
||||
HostSummary(
|
||||
@@ -161,9 +169,8 @@ def _host_detail_from_model(
|
||||
) -> HostDetail:
|
||||
hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE)
|
||||
report_map = max_daily_report_time_by_host(db)
|
||||
event_count = db.scalar(
|
||||
select(func.count()).select_from(Event).where(Event.host_id == host.id)
|
||||
)
|
||||
hidden = get_hidden_event_types(db)
|
||||
event_count = _count_visible_events(db, host.id, hidden)
|
||||
last_hb = hb_map.get(host.id)
|
||||
latest_map = latest_agent_versions_by_product(db)
|
||||
update_cfg = get_effective_agent_update_config(db)
|
||||
|
||||
@@ -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.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())
|
||||
|
||||
).all()
|
||||
hidden = get_hidden_event_types(db)
|
||||
event_rows = [event for event in event_rows if event.type not in hidden]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from dataclasses import replace
|
||||
from dataclasses import replace
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -37,6 +37,7 @@ from app.services.event_severity_overrides import (
|
||||
list_severity_override_items,
|
||||
replace_severity_overrides,
|
||||
)
|
||||
from app.services.event_type_visibility import replace_event_visibility
|
||||
from app.services.ui_settings import (
|
||||
get_effective_ui_settings,
|
||||
upsert_ui_settings,
|
||||
@@ -375,6 +376,7 @@ class EventSeverityOverrideRow(BaseModel):
|
||||
event_type: str
|
||||
default_severity: str
|
||||
override_severity: str | None = None
|
||||
show_in_events: bool = True
|
||||
|
||||
|
||||
class EventSeverityOverridesResponse(BaseModel):
|
||||
@@ -384,8 +386,25 @@ class EventSeverityOverridesResponse(BaseModel):
|
||||
|
||||
class EventSeverityOverridesUpdate(BaseModel):
|
||||
overrides: dict[str, str | None] = Field(
|
||||
default_factory=dict,
|
||||
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)
|
||||
@@ -395,14 +414,7 @@ def get_severity_overrides(
|
||||
) -> EventSeverityOverridesResponse:
|
||||
items = list_severity_override_items(db)
|
||||
return EventSeverityOverridesResponse(
|
||||
items=[
|
||||
EventSeverityOverrideRow(
|
||||
event_type=i.event_type,
|
||||
default_severity=i.default_severity,
|
||||
override_severity=i.override_severity,
|
||||
)
|
||||
for i in items
|
||||
],
|
||||
items=_severity_override_rows(items),
|
||||
source=SOURCE_DB,
|
||||
)
|
||||
|
||||
@@ -414,19 +426,19 @@ def update_severity_overrides(
|
||||
_user=Depends(require_admin),
|
||||
) -> EventSeverityOverridesResponse:
|
||||
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()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
return EventSeverityOverridesResponse(
|
||||
items=[
|
||||
EventSeverityOverrideRow(
|
||||
event_type=i.event_type,
|
||||
default_severity=i.default_severity,
|
||||
override_severity=i.override_severity,
|
||||
)
|
||||
for i in items
|
||||
],
|
||||
items=_severity_override_rows(items),
|
||||
source=SOURCE_DB,
|
||||
)
|
||||
|
||||
|
||||
@@ -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.event import Event
|
||||
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.ui_settings import UiSettings
|
||||
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.mobile_settings import MobileSettings
|
||||
from app.models.mobile_enrollment_code import MobileEnrollmentCode
|
||||
@@ -27,6 +28,7 @@ __all__ = [
|
||||
"ProblemEvent",
|
||||
"User",
|
||||
"EventSeverityOverride",
|
||||
"EventTypeVisibility",
|
||||
"LoginAttempt",
|
||||
"UiSettings",
|
||||
"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
|
||||
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.constants.event_types import DEFAULT_EVENT_SEVERITIES
|
||||
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
|
||||
|
||||
SOURCE_DB = "db"
|
||||
@@ -19,6 +20,7 @@ class EventSeverityOverrideItem:
|
||||
event_type: str
|
||||
default_severity: str
|
||||
override_severity: str | None
|
||||
show_in_events: bool = True
|
||||
|
||||
|
||||
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]:
|
||||
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] = []
|
||||
for event_type in sorted(types):
|
||||
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,
|
||||
default_severity=default,
|
||||
override_severity=override_map.get(event_type),
|
||||
show_in_events=visibility_map.get(event_type, True),
|
||||
)
|
||||
)
|
||||
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,5 +1,5 @@
|
||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||
|
||||
APP_NAME = "Security Alert Center"
|
||||
APP_VERSION = "0.20.22"
|
||||
APP_VERSION = "0.20.23"
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
{
|
||||
"name": "sac-ui",
|
||||
"private": true,
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||
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}`;
|
||||
|
||||
@@ -397,6 +397,7 @@
|
||||
<p class="settings-hint settings-hint-top">
|
||||
Переопределение значимости для новых событий после ingest. Исходное severity агента сохраняется в
|
||||
<code>details.severity_agent</code>. Пустое значение — как прислал агент (см. «По умолчанию»).
|
||||
Снимите «Показывать в событиях», чтобы скрыть тип из списков SAC (например <code>agent.heartbeat</code>).
|
||||
</p>
|
||||
<div class="settings-severity-toolbar">
|
||||
<input v-model="severityFilter" type="search" placeholder="Фильтр по type…" />
|
||||
@@ -411,6 +412,7 @@
|
||||
<th>Тип события</th>
|
||||
<th>По умолчанию</th>
|
||||
<th>Effective</th>
|
||||
<th>Показывать в событиях</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -426,6 +428,12 @@
|
||||
<option value="critical">critical</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="settings-severity-show-cell">
|
||||
<label>
|
||||
<input v-model="row.show_in_events" type="checkbox" />
|
||||
показывать
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -678,10 +686,12 @@ interface SeverityOverrideRowApi {
|
||||
event_type: string;
|
||||
default_severity: string;
|
||||
override_severity: string | null;
|
||||
show_in_events: boolean;
|
||||
}
|
||||
|
||||
interface SeverityOverrideRowUi extends SeverityOverrideRowApi {
|
||||
selected: string;
|
||||
override_show_in_events: boolean;
|
||||
}
|
||||
|
||||
const loading = ref(true);
|
||||
@@ -868,6 +878,7 @@ function mapSeverityRows(items: SeverityOverrideRowApi[]): SeverityOverrideRowUi
|
||||
return items.map((row) => ({
|
||||
...row,
|
||||
selected: row.override_severity ?? "",
|
||||
override_show_in_events: row.show_in_events,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -884,25 +895,30 @@ async function saveSeverityOverrides() {
|
||||
success.value = "";
|
||||
try {
|
||||
const overrides: Record<string, string | null> = {};
|
||||
const visibility: Record<string, boolean | null> = {};
|
||||
for (const row of severityRows.value) {
|
||||
const prev = row.override_severity ?? "";
|
||||
const next = row.selected.trim();
|
||||
if (next === prev) continue;
|
||||
overrides[row.event_type] = next || null;
|
||||
const prevSeverity = row.override_severity ?? "";
|
||||
const nextSeverity = row.selected.trim();
|
||||
if (nextSeverity !== prevSeverity) {
|
||||
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) {
|
||||
error.value = "Нет изменений severity для сохранения";
|
||||
if (Object.keys(overrides).length === 0 && Object.keys(visibility).length === 0) {
|
||||
error.value = "Нет изменений для сохранения";
|
||||
return;
|
||||
}
|
||||
const data = await apiFetch<{ items: SeverityOverrideRowApi[] }>(
|
||||
"/api/v1/settings/notifications/severity-overrides",
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ overrides }),
|
||||
body: JSON.stringify({ overrides, visibility }),
|
||||
},
|
||||
);
|
||||
severityRows.value = mapSeverityRows(data.items);
|
||||
success.value = "Severity по типам событий сохранён.";
|
||||
success.value = "Severity и видимость событий сохранены.";
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Ошибка сохранения severity";
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user