fix: prevent duplicate host_silence problems per host (0.4.8)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,6 @@ from sqlalchemy.orm import Session
|
|||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.models import Host, Problem
|
from app.models import Host, Problem
|
||||||
from app.services.problem_rules import RuleMatch, build_fingerprint, host_silence_match_for_host
|
from app.services.problem_rules import RuleMatch, build_fingerprint, host_silence_match_for_host
|
||||||
from app.services.problems import _correlation_cutoff
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -44,15 +43,13 @@ def open_or_refresh_host_silence_problem(
|
|||||||
match.rule_id,
|
match.rule_id,
|
||||||
match.fingerprint_suffix,
|
match.fingerprint_suffix,
|
||||||
)
|
)
|
||||||
cutoff = _correlation_cutoff(ref)
|
|
||||||
|
|
||||||
open_problem = db.scalar(
|
open_problem = db.scalar(
|
||||||
select(Problem)
|
select(Problem)
|
||||||
.where(
|
.where(
|
||||||
Problem.status == "open",
|
Problem.status == "open",
|
||||||
Problem.fingerprint == fingerprint,
|
Problem.rule_id == match.rule_id,
|
||||||
Problem.host_id == host_id,
|
Problem.host_id == host_id,
|
||||||
Problem.last_seen_at >= cutoff,
|
|
||||||
)
|
)
|
||||||
.order_by(Problem.last_seen_at.desc())
|
.order_by(Problem.last_seen_at.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@@ -60,6 +57,7 @@ def open_or_refresh_host_silence_problem(
|
|||||||
if open_problem:
|
if open_problem:
|
||||||
open_problem.summary = match.summary
|
open_problem.summary = match.summary
|
||||||
open_problem.title = match.title
|
open_problem.title = match.title
|
||||||
|
open_problem.fingerprint = fingerprint
|
||||||
open_problem.last_seen_at = ref
|
open_problem.last_seen_at = ref
|
||||||
open_problem.updated_at = ref
|
open_problem.updated_at = ref
|
||||||
db.flush()
|
db.flush()
|
||||||
|
|||||||
@@ -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.4.7"
|
APP_VERSION = "0.4.8"
|
||||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||||
|
|||||||
@@ -82,6 +82,43 @@ def test_scan_does_not_duplicate_open_problem(db_session, scan_settings):
|
|||||||
assert len(problems) == 1
|
assert len(problems) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_scan_does_not_duplicate_after_correlation_window(db_session, scan_settings, monkeypatch):
|
||||||
|
"""host_silence — одна open-проблема на хост, даже если прошло > correlation window."""
|
||||||
|
monkeypatch.setenv("SAC_PROBLEM_CORRELATION_WINDOW_MINUTES", "60")
|
||||||
|
get_settings.cache_clear()
|
||||||
|
|
||||||
|
hb = _ingest(
|
||||||
|
db_session,
|
||||||
|
type="agent.heartbeat",
|
||||||
|
category="agent",
|
||||||
|
severity="info",
|
||||||
|
title="hb",
|
||||||
|
summary="heartbeat",
|
||||||
|
)
|
||||||
|
hb.received_at = datetime.now(timezone.utc) - timedelta(hours=2)
|
||||||
|
db_session.flush()
|
||||||
|
|
||||||
|
t0 = datetime.now(timezone.utc)
|
||||||
|
first = run_host_silence_scan(db_session, now=t0)
|
||||||
|
assert first[0].created is True
|
||||||
|
first[0].problem.last_seen_at = t0 - timedelta(hours=2)
|
||||||
|
db_session.flush()
|
||||||
|
|
||||||
|
later = run_host_silence_scan(db_session, now=t0 + timedelta(hours=3))
|
||||||
|
assert len(later) == 1
|
||||||
|
assert later[0].created is False
|
||||||
|
assert later[0].problem.id == first[0].problem.id
|
||||||
|
|
||||||
|
problems = db_session.scalars(
|
||||||
|
select(Problem).where(
|
||||||
|
Problem.host_id == hb.host_id,
|
||||||
|
Problem.rule_id == RULE_HOST_SILENCE,
|
||||||
|
Problem.status == "open",
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
assert len(problems) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_scan_skips_host_without_heartbeat(db_session, scan_settings):
|
def test_scan_skips_host_without_heartbeat(db_session, scan_settings):
|
||||||
_ingest(
|
_ingest(
|
||||||
db_session,
|
db_session,
|
||||||
|
|||||||
@@ -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.4.7";
|
export const APP_VERSION = "0.4.8";
|
||||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user