Дві шкали серйозності: тригер відповідності фільтрував навпаки
Подія відповідності несла шкалу critical/high/medium, а поріг тригера
порівнювався шкалою алертів, де SeverityRank("critical") = 0. Тригер із
порогом «warning» пропускав середнє й відкидав найважче — тихо.
Подія тепер народжується в шкалі алертів; умова тригера приймає обидві.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e0fdfcde06
commit
3b9217de3c
5 changed files with 151 additions and 4 deletions
34
HISTORY.md
34
HISTORY.md
|
|
@ -7286,3 +7286,37 @@ FAIL modal > тло стає inert, поки вікно відкрите
|
||||||
а на `display: none` він і не був потрібен. Причина розписана в пункті 10.
|
а на `display: none` він і не був потрібен. Причина розписана в пункті 10.
|
||||||
- Попередження збірки про чанк >500 kB — було до цієї роботи, до переліку не
|
- Попередження збірки про чанк >500 kB — було до цієї роботи, до переліку не
|
||||||
входить, і код-спліт мапи це окрема задача.
|
входить, і код-спліт мапи це окрема задача.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-28 — Дві шкали серйозності: тригер відповідності фільтрував навпаки
|
||||||
|
|
||||||
|
Знайдено під час першого наскрізного налаштування ескалацій: API відмовив
|
||||||
|
створити тригер із `min_severity: critical` — «невідома серйозність». Відмова
|
||||||
|
виявилась правильною, а за нею стояла тиха вада.
|
||||||
|
|
||||||
|
**Що було.** Відповідність оперує `critical/high/medium/low`, алерти —
|
||||||
|
`info/warning/average/high/disaster`. Спільне слово рівно одне: `high`.
|
||||||
|
Подія відповідності несла свою шкалу, а поріг тригера порівнювався
|
||||||
|
лінійкою алертів (`alerting/events.go:351`), де `SeverityRank("critical")`
|
||||||
|
повертає **нуль**. Тобто тригер із порогом «warning» пропускав середні
|
||||||
|
порушення й **відкидав найважчі** — саме ті шість критичних SNMP-community,
|
||||||
|
заради яких механізм і вмикають. Мовчки: правило створене, увімкнене,
|
||||||
|
прогін іде, порушень 18, алертів нуль.
|
||||||
|
|
||||||
|
**Що зроблено.**
|
||||||
|
|
||||||
|
* `store.ComplianceSeverityToAlert` — переклад на шкалу алертів
|
||||||
|
(`critical→disaster`, `high→high`, `medium→average`, `low→warning`).
|
||||||
|
* `httpapi/compliance.go` віддає подію вже в шкалі алертів: далі її читають
|
||||||
|
маршрути, пороги каналів і драбини ескалації — усе, що знає одну шкалу.
|
||||||
|
* `store.NormalizeEventSeverity` — умова тригера приймає **обидві** шкали.
|
||||||
|
Людина щойно дивилась на сторінку відповідності, де написано «critical»;
|
||||||
|
вимагати подумки перекласти це в «disaster» — роздавати відмови за власну
|
||||||
|
незручність.
|
||||||
|
* Тести (`alerts_events_severity_test.go`) тримають той бік, що ламався:
|
||||||
|
найважче не може не пройти найлегший поріг, і переклад не переставляє
|
||||||
|
порядок.
|
||||||
|
|
||||||
|
**Чого тести НЕ покривають:** саму подорож події від прогону відповідності
|
||||||
|
до каналу — це перевірено вручну на стенді, не в CI.
|
||||||
|
|
|
||||||
|
|
@ -348,7 +348,13 @@ func matchesComplianceRule(cond store.Condition, ev ComplianceEvent) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cond.MinSeverity != "" && store.SeverityRank(ev.Severity) < store.SeverityRank(cond.MinSeverity) {
|
// Обидві сторони зводяться до шкали алертів: подія може прийти зі
|
||||||
|
// шкалою відповідності (critical/medium), і поріг людина теж могла
|
||||||
|
// написати нею. Без зведення «critical» отримував ранг нуль і
|
||||||
|
// відкидався порогом «warning» — тобто фільтр працював навпаки.
|
||||||
|
if cond.MinSeverity != "" &&
|
||||||
|
store.SeverityRank(store.NormalizeEventSeverity(ev.Severity)) <
|
||||||
|
store.SeverityRank(store.NormalizeEventSeverity(cond.MinSeverity)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,12 @@ func (s *Server) handleRunCompliance(w http.ResponseWriter, r *http.Request, p *
|
||||||
evs := make([]alerting.ComplianceEvent, 0, len(stat.Findings))
|
evs := make([]alerting.ComplianceEvent, 0, len(stat.Findings))
|
||||||
for _, f := range stat.Findings {
|
for _, f := range stat.Findings {
|
||||||
evs = append(evs, alerting.ComplianceEvent{
|
evs = append(evs, alerting.ComplianceEvent{
|
||||||
RuleID: f.RuleID, RuleName: f.RuleName, Severity: f.Severity,
|
RuleID: f.RuleID, RuleName: f.RuleName,
|
||||||
|
// Алерт народжується зі шкалою АЛЕРТІВ, а не
|
||||||
|
// відповідності: далі його читають маршрути, пороги
|
||||||
|
// каналів і драбини ескалації — усе, що знає лише
|
||||||
|
// info/warning/average/high/disaster.
|
||||||
|
Severity: store.ComplianceSeverityToAlert(f.Severity),
|
||||||
DeviceID: f.DeviceID, Passed: f.Passed,
|
DeviceID: f.DeviceID, Passed: f.Passed,
|
||||||
Line: f.Line, LineNumber: f.LineNumber,
|
Line: f.Line, LineNumber: f.LineNumber,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,16 @@ func ValidateRuleCondition(source string, raw []byte) error {
|
||||||
if c.Event != "" && c.Event != "violation" {
|
if c.Event != "" && c.Event != "violation" {
|
||||||
return fmt.Errorf("%w: для відповідності є лише подія violation", ErrInvalid)
|
return fmt.Errorf("%w: для відповідності є лише подія violation", ErrInvalid)
|
||||||
}
|
}
|
||||||
if c.MinSeverity != "" && !validSeverity[c.MinSeverity] {
|
// Приймаємо обидві шкали. Людина щойно дивилась на сторінку
|
||||||
return fmt.Errorf("%w: невідома серйозність %q", ErrInvalid, c.MinSeverity)
|
// відповідності, де написано «critical», і саме це слово вона
|
||||||
|
// сюди й напише; вимагати подумки перекласти його в «disaster»
|
||||||
|
// означає роздавати відмови за власну незручність.
|
||||||
|
if c.MinSeverity != "" &&
|
||||||
|
!validSeverity[c.MinSeverity] && !validComplianceSeverity[c.MinSeverity] {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: невідома серйозність %q — буває critical, high, medium (як у правилі "+
|
||||||
|
"відповідності) або info, warning, average, high, disaster (як в алерті)",
|
||||||
|
ErrInvalid, c.MinSeverity)
|
||||||
}
|
}
|
||||||
case "trap":
|
case "trap":
|
||||||
return validateTrapCondition(c)
|
return validateTrapCondition(c)
|
||||||
|
|
@ -193,6 +201,47 @@ var validSeverity = map[string]bool{
|
||||||
"info": true, "warning": true, "average": true, "high": true, "disaster": true,
|
"info": true, "warning": true, "average": true, "high": true, "disaster": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Шкала правил ВІДПОВІДНОСТІ. Інша, і це не недогляд: відповідність
|
||||||
|
// говорить мовою аудиту, алерти — мовою чергового. Спільне слово одне —
|
||||||
|
// «high».
|
||||||
|
var validComplianceSeverity = map[string]bool{
|
||||||
|
"critical": true, "high": true, "medium": true, "low": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComplianceSeverityToAlert переводить серйозність правила відповідності
|
||||||
|
// у шкалу алертів.
|
||||||
|
//
|
||||||
|
// Без цього переведення виходила тиха й ЗВОРОТНА помилка. Подія несла
|
||||||
|
// серйозність відповідності, а порівнювалась через SeverityRank, який
|
||||||
|
// знає лише шкалу алертів і на «critical» повертає нуль. Тобто тригер
|
||||||
|
// із порогом «warning» пропускав середні порушення й ВІДКИДАВ
|
||||||
|
// найважчі — рівно ті, через які цей механізм і вмикають.
|
||||||
|
//
|
||||||
|
// Напрямок очевидний: найгірше в одній шкалі стає найгіршим у другій.
|
||||||
|
func ComplianceSeverityToAlert(s string) string {
|
||||||
|
switch s {
|
||||||
|
case "critical":
|
||||||
|
return "disaster"
|
||||||
|
case "high":
|
||||||
|
return "high"
|
||||||
|
case "medium":
|
||||||
|
return "average"
|
||||||
|
case "low":
|
||||||
|
return "warning"
|
||||||
|
}
|
||||||
|
// Невідоме не піднімаємо й не занижуємо мовчки: «warning» означає
|
||||||
|
// «покажи, але не буди», і для незнайомого слова це найчесніше.
|
||||||
|
return "warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeEventSeverity зводить будь-яку з двох шкал до шкали алертів.
|
||||||
|
func NormalizeEventSeverity(s string) string {
|
||||||
|
if validSeverity[s] {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return ComplianceSeverityToAlert(s)
|
||||||
|
}
|
||||||
|
|
||||||
// SeverityRank — порядок серйозності, той самий, що в alr.severity.
|
// SeverityRank — порядок серйозності, той самий, що в alr.severity.
|
||||||
func SeverityRank(s string) int {
|
func SeverityRank(s string) int {
|
||||||
switch s {
|
switch s {
|
||||||
|
|
|
||||||
53
server/internal/store/alerts_events_severity_test.go
Normal file
53
server/internal/store/alerts_events_severity_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Дві шкали в одному продукті — і поріг тригера порівнював їх однією
|
||||||
|
// лінійкою. Тест тримає саме той бік, який ламався: НАЙВАЖЧЕ порушення
|
||||||
|
// відповідності не має відкидатись порогом, який пропускає легші.
|
||||||
|
//
|
||||||
|
// До виправлення SeverityRank("critical") давав нуль, тож правило з
|
||||||
|
// порогом «warning» пропускало high і мовчки викидало critical — тобто
|
||||||
|
// фільтр працював навпаки, і саме на тих шести знахідках, через які
|
||||||
|
// механізм і вмикають.
|
||||||
|
func TestComplianceSeverityDoesNotInvertThreshold(t *testing.T) {
|
||||||
|
pass := func(ev, min string) bool {
|
||||||
|
return SeverityRank(NormalizeEventSeverity(ev)) >=
|
||||||
|
SeverityRank(NormalizeEventSeverity(min))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range []struct {
|
||||||
|
ev, min string
|
||||||
|
want bool
|
||||||
|
why string
|
||||||
|
}{
|
||||||
|
{"critical", "warning", true, "найважче не може не пройти найлегший поріг"},
|
||||||
|
{"critical", "high", true, "критичне важче за high"},
|
||||||
|
{"high", "warning", true, ""},
|
||||||
|
{"medium", "warning", true, "medium — це average, воно вище за warning"},
|
||||||
|
{"medium", "high", false, "середнє не має будити за порогом high"},
|
||||||
|
{"critical", "disaster", true, "critical і є disaster"},
|
||||||
|
{"high", "disaster", false, ""},
|
||||||
|
// Поріг теж міг бути написаний шкалою відповідності.
|
||||||
|
{"disaster", "critical", true, ""},
|
||||||
|
{"average", "medium", true, ""},
|
||||||
|
} {
|
||||||
|
if got := pass(c.ev, c.min); got != c.want {
|
||||||
|
t.Errorf("подія %q проти порогу %q: маємо %v, очікували %v %s",
|
||||||
|
c.ev, c.min, got, c.want, c.why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Порядок має зберігатись: переклад не може переставити місцями те, що
|
||||||
|
// в початковій шкалі стояло по-різному.
|
||||||
|
func TestComplianceSeverityKeepsOrder(t *testing.T) {
|
||||||
|
order := []string{"low", "medium", "high", "critical"}
|
||||||
|
for i := 1; i < len(order); i++ {
|
||||||
|
lo := SeverityRank(ComplianceSeverityToAlert(order[i-1]))
|
||||||
|
hi := SeverityRank(ComplianceSeverityToAlert(order[i]))
|
||||||
|
if lo >= hi {
|
||||||
|
t.Fatalf("%s (%d) не нижче за %s (%d)", order[i-1], lo, order[i], hi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue