П'ять паралельних задач. Найцінніше в них — не можливості, а знайдене.
0069 БІЛІНГ. Аудит 0009 показав, що перевірка ліміту не спрацювала б
жодного разу: isPlanLimit шукала слово «ліміт», а тригер писав
"device limit reached" англійською. Перше ж досягнення стелі дало б
клієнту 500 замість пояснення. Плюс три діри: тригер лише на INSERT
(стеля в 15 обходилась за чотири дії через архів), max_maps/max_agents/
max_users не перевіряло ніщо — тобто рівно те, чим відрізняються плани,
і license_keys була закрита політикою tenant_isolation з 0011, хоча
tenant_id там NULLABLE навмисно: головний сценарій self-hosted був
недосяжний.
Після закінчення ліцензії не вимикається нічого — замерзає лише ріст.
Моніторинг, що перестав моніторити через несплачений рахунок, це
аварія в мережі клієнта, спричинена нами.
0070 SLA. Джерелом обрано ts.icmp_1h, а не device_status_history:
остання не вміє сказати «ми не знали» — перехід пишеться лише при
зміні стану, тож доба мовчання зонда виглядає як доба роботи. Час
розкладено на чотири частини, і «немає даних» не додається ні до чого;
замість вибору між двома брехнями звіт каже, яку частку періоду він
бачив. Закритий період тримає тригер, а не домовленість у Go.
0071 ВІДПОВІДНІСТЬ. 20 правил, кожне прив'язане до родини: об'єднаний
вираз, що покриває Cisco й не покриває MikroTik, дав би «0 порушень» і
сховав сліпу пляму. Вендор не входить у перелік, доки для нього немає
зразка конфігу в тесті. TestBuiltinRulesAreNotAlwaysGreen вимагає, щоб
у кожного правила був конфіг, де воно спрацювало, І де ні.
ПІСОЧНИЦЯ УСТАНОВНИКА — та сама установка в ізольованому проєкті
compose. Знайшла дві справжні вади з трьох спроб:
* healthcheck бази ходив unix-сокетом, а споживачі по TCP. При
первинній ініціалізації Postgres слухає лише сокет — compose
вважав базу здоровою, migrate отримував connection refused. На
створеній базі цієї фази немає, тож вада чекала на першого клієнта;
* у білому переліку модулів API не було traps і filecfg — зонд із
приймачем трапів неможливо було зареєструвати взагалі.
ТЕСТИ СТОРІНОК: 137 → 252. Мережевий шар, права доступу, незворотні
дії, фільтри з адресного рядка. Підмінюється лише fetch і WebSocket —
api/client.ts працює справжній.
729 lines
22 KiB
Go
729 lines
22 KiB
Go
package store
|
||
|
||
import (
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
"testing"
|
||
|
||
schema "github.com/netpulse/netpulse/server/migrations"
|
||
)
|
||
|
||
// Перевірка вбудованих правил відповідності з міграції 0071.
|
||
//
|
||
// НАВІЩО ЦЕЙ ФАЙЛ ІСНУЄ. Правило «не має бути», чий зразок не збігається
|
||
// НІКОЛИ, повідомляє «порушень немає» — тобто виглядає ідеально й
|
||
// нічого не перевіряє. Відрізнити його від справного правила за
|
||
// результатом прогону неможливо в принципі: обидва дають нуль
|
||
// порушень. Єдиний спосіб — показати конфіг, на якому зразок мусить
|
||
// спрацювати, і конфіг, на якому не мусить.
|
||
//
|
||
// ЗВІДКИ БЕРУТЬСЯ ЗРАЗКИ. З самої міграції, через schema.Files. Копії
|
||
// виразів у тесті немає навмисно: два описи одного й того самого
|
||
// розійдуться на першій же правці, і тест почав би підтверджувати
|
||
// власну копію замість того, що поїде клієнтові. Та сама причина, з
|
||
// якої db/profiles/build.py породжує SQL, а не дублює його.
|
||
//
|
||
// ЩО ТЕСТ ГАРАНТУЄ, а що ні. Гарантує: кожен зразок хоч на одному
|
||
// справжньому конфізі спрацював і хоч на одному не спрацював, і для
|
||
// КОЖНОГО вендора з переліку правила є конфіг цього вендора з
|
||
// очікуваним висновком. Не гарантує: що зразок покриває всі прошивки
|
||
// цієї родини — конфіги збирались із документації й типових
|
||
// вивантажень, а не з усього парку.
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Читання правил із міграції
|
||
// ---------------------------------------------------------------------
|
||
|
||
const builtinMigration = "0071_builtin_compliance_prototypes.sql"
|
||
|
||
// Рядок довідника має жорстку форму саме заради цього розбору:
|
||
//
|
||
// ('ключ', 'серйозність', 'вид', 'тип конфігу', NULL | string_to_array('Вендор,Вендор', ','),
|
||
// $rx$зразок$rx$,
|
||
//
|
||
// Доларові лапки навколо зразка — не оздоба: у виразі живуть зворотні
|
||
// скісні риски й апострофи, і саме вони роблять межі зразка
|
||
// однозначними для цього виразу.
|
||
var builtinRuleRe = regexp.MustCompile(
|
||
`(?s)\('([a-z0-9-]+)',\s*'([a-z]+)',\s*'([a-z_]+)',\s*'([a-z]+)',\s*` +
|
||
`(?:NULL|string_to_array\('([^']*)', ','\)),\s*\$rx\$(.*?)\$rx\$`)
|
||
|
||
type builtinRule struct {
|
||
Key string
|
||
Severity string
|
||
Kind string
|
||
ConfigType string
|
||
Vendors []string
|
||
Pattern string
|
||
}
|
||
|
||
func loadBuiltinRules(t *testing.T) map[string]builtinRule {
|
||
t.Helper()
|
||
|
||
body, err := schema.Files.ReadFile(builtinMigration)
|
||
if err != nil {
|
||
t.Fatalf("міграція %s не читається: %v", builtinMigration, err)
|
||
}
|
||
|
||
out := map[string]builtinRule{}
|
||
for _, m := range builtinRuleRe.FindAllStringSubmatch(string(body), -1) {
|
||
r := builtinRule{
|
||
Key: m[1], Severity: m[2], Kind: m[3], ConfigType: m[4], Pattern: m[6],
|
||
}
|
||
if m[5] != "" {
|
||
r.Vendors = strings.Split(m[5], ",")
|
||
}
|
||
if _, dup := out[r.Key]; dup {
|
||
t.Fatalf("ключ %q у довіднику двічі", r.Key)
|
||
}
|
||
out[r.Key] = r
|
||
}
|
||
if len(out) == 0 {
|
||
t.Fatalf("з %s не вийнято жодного правила — розбір розійшовся з форматом файлу",
|
||
builtinMigration)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Зразки конфігів
|
||
// ---------------------------------------------------------------------
|
||
|
||
// Конфіг і вендор, під яким цей хост стоїть в inv.devices.
|
||
//
|
||
// Вендор тут не прикраса: селектор вбудованого правила — це
|
||
// {"vendors":[...]}, який selectorSQL перетворює на `d.vendor = ANY(...)`,
|
||
// тобто рівно на порівняння з цим рядком. Пари «правило + вендор», для
|
||
// якої немає конфігу, тест не пропускає — інакше вендора можна було б
|
||
// дописати в правило, не перевіривши на ньому нічого.
|
||
type cfgSample struct {
|
||
Vendor string
|
||
Body string
|
||
}
|
||
|
||
var samples = map[string]cfgSample{
|
||
|
||
// --- Cisco IOS ---------------------------------------------------
|
||
|
||
"cisco-bad": {"Cisco", `Building configuration...
|
||
|
||
Current configuration : 4021 bytes
|
||
!
|
||
version 15.2
|
||
service timestamps debug datetime msec
|
||
no service password-encryption
|
||
!
|
||
hostname kyiv-acc-sw01
|
||
!
|
||
enable password cisco123
|
||
!
|
||
username admin privilege 15 password 0 admin123
|
||
!
|
||
interface Vlan1
|
||
ip address 10.10.0.11 255.255.255.0
|
||
!
|
||
snmp-server community public RO
|
||
snmp-server community private RW
|
||
!
|
||
line con 0
|
||
password cisco
|
||
line vty 0 4
|
||
transport input telnet ssh
|
||
login local
|
||
line vty 5 15
|
||
transport input all
|
||
!
|
||
end
|
||
`},
|
||
|
||
"cisco-good": {"Cisco", `Building configuration...
|
||
!
|
||
version 15.2
|
||
service password-encryption
|
||
!
|
||
hostname kyiv-core-sw01
|
||
!
|
||
enable secret 5 $1$mERr$X1s0Kk9yq2mA3cCf1uJvS/
|
||
!
|
||
username noc privilege 15 secret 5 $1$hM3z$8QeJm0Wl2sN6tR4vB7xYc.
|
||
!
|
||
aaa new-model
|
||
!
|
||
ip access-list standard MGMT-ACL
|
||
permit 10.20.30.0 0.0.0.255
|
||
!
|
||
snmp-server community NP-ro-2024 RO MGMT-ACL
|
||
!
|
||
logging host 10.20.30.15
|
||
logging trap informational
|
||
!
|
||
ntp server 10.20.30.10 prefer
|
||
ntp server 10.20.30.11
|
||
!
|
||
line con 0
|
||
exec-timeout 5 0
|
||
line vty 0 15
|
||
access-class MGMT-ACL in
|
||
transport input ssh
|
||
login local
|
||
!
|
||
end
|
||
`},
|
||
|
||
// --- Eltex MES ---------------------------------------------------
|
||
//
|
||
// Свідомо змішаний: telnet відкритий, решта в порядку. Саме такий
|
||
// конфіг і ловить помилку «зразок написаний під Cisco, а вендор у
|
||
// переліку є» — на бездоганному конфізі її не видно.
|
||
|
||
"eltex-mixed": {"Eltex", `!
|
||
version 4.0.14
|
||
!
|
||
hostname odesa-acc-sw03
|
||
!
|
||
username admin password encrypted 5f4dcc3b5aa765d61d8327deb882cf99 privilege 15
|
||
!
|
||
ip telnet server
|
||
!
|
||
snmp-server community NP-ro-2024 ro
|
||
!
|
||
logging host 10.20.30.15
|
||
!
|
||
sntp unicast client enable
|
||
sntp server 10.20.30.10
|
||
!
|
||
line telnet
|
||
exec-timeout 10
|
||
line ssh
|
||
exec-timeout 10
|
||
!
|
||
management access-list MGMT
|
||
permit ip-source 10.20.30.0 mask 255.255.255.0
|
||
management access-class MGMT
|
||
!
|
||
end
|
||
`},
|
||
|
||
// --- ZTE ZXR10 ---------------------------------------------------
|
||
|
||
"zte-mixed": {"ZTE", `!
|
||
!<Configuration Data Version 2.0>
|
||
!
|
||
hostname kharkiv-agg-zte01
|
||
!
|
||
username zteadmin password 0 zte123 privilege 15
|
||
!
|
||
snmp-server community public view AllView ro
|
||
snmp-server enable
|
||
!
|
||
ntp server 10.20.30.10
|
||
!
|
||
logging server 10.20.30.15
|
||
!
|
||
line telnet idle-timeout 10
|
||
!
|
||
line vty 0 4
|
||
transport input telnet ssh
|
||
!
|
||
end
|
||
`},
|
||
|
||
// --- Huawei VRP --------------------------------------------------
|
||
|
||
"huawei-bad": {"Huawei", `!Last configuration was updated at 2026-03-11 09:12:44+02:00
|
||
#
|
||
sysname lviv-agg-sw02
|
||
#
|
||
telnet server enable
|
||
#
|
||
snmp-agent
|
||
snmp-agent community read public
|
||
snmp-agent community write private
|
||
snmp-agent sys-info version v2c
|
||
#
|
||
aaa
|
||
local-user admin password simple Huawei@123
|
||
local-user admin service-type telnet ssh
|
||
#
|
||
user-interface vty 0 4
|
||
authentication-mode password
|
||
set authentication password simple Admin@123
|
||
protocol inbound all
|
||
#
|
||
return
|
||
`},
|
||
|
||
"huawei-good": {"Huawei", `#
|
||
sysname lviv-core-sw01
|
||
#
|
||
undo telnet server enable
|
||
undo telnet ipv6 server enable
|
||
#
|
||
snmp-agent
|
||
snmp-agent community read cipher %^%#NP2024ro%^%#
|
||
snmp-agent sys-info version v3
|
||
#
|
||
info-center enable
|
||
info-center loghost 10.20.30.15
|
||
#
|
||
ntp-service unicast-server 10.20.30.10
|
||
ntp-service unicast-server 10.20.30.11
|
||
#
|
||
aaa
|
||
local-user noc password irreversible-cipher $1c$KJHGfdsa
|
||
local-user noc service-type ssh
|
||
#
|
||
user-interface vty 0 4
|
||
acl 2000 inbound
|
||
authentication-mode aaa
|
||
protocol inbound ssh
|
||
#
|
||
return
|
||
`},
|
||
|
||
// --- H3C Comware -------------------------------------------------
|
||
|
||
"h3c-bad": {"H3C", `#
|
||
version 7.1.070, Release 6127P02
|
||
#
|
||
sysname dnipro-acc-h3c01
|
||
#
|
||
telnet server enable
|
||
#
|
||
snmp-agent
|
||
snmp-agent community read public
|
||
#
|
||
local-user admin class manage
|
||
password simple H3C@1234
|
||
service-type telnet ssh
|
||
#
|
||
line vty 0 15
|
||
authentication-mode scheme
|
||
protocol inbound all
|
||
#
|
||
return
|
||
`},
|
||
|
||
"h3c-good": {"H3C", `#
|
||
sysname dnipro-core-h3c01
|
||
#
|
||
undo telnet server enable
|
||
#
|
||
info-center loghost 10.20.30.15
|
||
#
|
||
ntp-service unicast-server 10.20.30.10
|
||
#
|
||
snmp-agent community read cipher $c$3$Nq8kL2Rt
|
||
#
|
||
local-user noc class manage
|
||
password hash $h$6$OqRsTuVw
|
||
service-type ssh
|
||
#
|
||
line vty 0 15
|
||
acl 2000 inbound
|
||
protocol inbound ssh
|
||
#
|
||
return
|
||
`},
|
||
|
||
// --- D-Link ------------------------------------------------------
|
||
|
||
"dlink-bad": {"D-Link", `#-------------------------------------------------------------------
|
||
# DGS-1210-28 Gigabit Ethernet Switch
|
||
# Configuration
|
||
#-------------------------------------------------------------------
|
||
# BASIC
|
||
config serial_port baud_rate 115200 auto_logout 10_minutes
|
||
enable telnet 23
|
||
enable web 80
|
||
# ACCOUNT
|
||
create account admin admin
|
||
# SNMP
|
||
enable snmp
|
||
create snmp community public view CommunityView read_only
|
||
create snmp community private view CommunityView read_write
|
||
# TIME AND SNTP
|
||
disable sntp
|
||
# SYSLOG
|
||
disable syslog
|
||
`},
|
||
|
||
"dlink-good": {"D-Link", `#-------------------------------------------------------------------
|
||
# DGS-3420-28SC Gigabit Ethernet Switch
|
||
# Configuration
|
||
#-------------------------------------------------------------------
|
||
# BASIC
|
||
disable telnet
|
||
enable ssh
|
||
config ssh authmode password enable
|
||
# SNMP
|
||
enable snmp
|
||
create snmp community NP-ro-2024 view CommunityView read_only
|
||
# TIME AND SNTP
|
||
config sntp primary 10.20.30.10 secondary 10.20.30.11 poll-interval 720
|
||
enable sntp
|
||
# SYSLOG
|
||
create syslog host 1 ipaddress 10.20.30.15 severity informational facility local0 udp_port 514 state enable
|
||
enable syslog
|
||
# ACCESS
|
||
create trusted_host network 10.20.30.0/24
|
||
`},
|
||
|
||
// --- MikroTik RouterOS -------------------------------------------
|
||
|
||
"mikrotik-bad": {"MikroTik", `# mar/11/2026 09:12:44 by RouterOS 7.14.3
|
||
# software id = ABCD-1234
|
||
#
|
||
/interface bridge
|
||
add name=bridge1
|
||
/ip service
|
||
set telnet disabled=no port=23
|
||
set ftp disabled=no
|
||
set www disabled=no
|
||
set ssh disabled=no
|
||
set winbox disabled=no
|
||
/snmp community
|
||
set [ find default=yes ] name=public addresses=0.0.0.0/0
|
||
/snmp
|
||
set enabled=yes
|
||
/system clock
|
||
set time-zone-name=Europe/Kyiv
|
||
/system logging
|
||
add topics=info
|
||
/user
|
||
add name=noc password=Str0ngPass group=full
|
||
`},
|
||
|
||
"mikrotik-good": {"MikroTik", `# mar/11/2026 09:12:44 by RouterOS 7.14.3
|
||
#
|
||
/interface bridge
|
||
add name=bridge1
|
||
/ip service
|
||
set telnet disabled=yes
|
||
set ftp disabled=yes
|
||
set www disabled=yes
|
||
set ssh address=10.20.30.0/24 disabled=no
|
||
set winbox address=10.20.30.0/24 disabled=no
|
||
/snmp community
|
||
set [ find default=yes ] name=NP-ro-2024 addresses=10.20.30.0/24
|
||
/snmp
|
||
set enabled=yes trap-version=2
|
||
/system ntp client
|
||
set enabled=yes servers=10.20.30.10,10.20.30.11
|
||
/system logging action
|
||
add name=remote target=remote remote=10.20.30.15
|
||
/system logging
|
||
add action=remote topics=info,error,warning
|
||
/system clock
|
||
set time-zone-name=Europe/Kyiv
|
||
`},
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Очікування
|
||
// ---------------------------------------------------------------------
|
||
|
||
// true — правило пройдено (порушення немає), false — знайдено порушення.
|
||
//
|
||
// Заповнено для КОЖНОЇ пари «правило + зразок вендора з його переліку».
|
||
// Пропуск — це не «нам байдуже», а помилка тесту: саме через пропущену
|
||
// пару вендор потрапляє в правило неперевіреним.
|
||
var expect = map[string]map[string]bool{
|
||
|
||
// Єдине правило без прив'язки до вендора — тому перелічені всі
|
||
// зразки набору.
|
||
"snmp-default-community": {
|
||
"cisco-bad": false, "cisco-good": true,
|
||
"eltex-mixed": true, "zte-mixed": false,
|
||
"huawei-bad": false, "huawei-good": true,
|
||
"h3c-bad": false, "h3c-good": true,
|
||
"dlink-bad": false, "dlink-good": true,
|
||
"mikrotik-bad": false, "mikrotik-good": true,
|
||
},
|
||
|
||
"no-telnet-ios": {
|
||
// transport input telnet / transport input all
|
||
"cisco-bad": false, "cisco-good": true,
|
||
// ip telnet server — форма Eltex, якої в синтаксисі Cisco немає
|
||
"eltex-mixed": false,
|
||
"zte-mixed": false,
|
||
},
|
||
"no-telnet-vrp": {
|
||
"huawei-bad": false, "huawei-good": true,
|
||
"h3c-bad": false, "h3c-good": true,
|
||
},
|
||
"no-telnet-dlink": {
|
||
"dlink-bad": false, "dlink-good": true,
|
||
},
|
||
"no-telnet-routeros": {
|
||
"mikrotik-bad": false, "mikrotik-good": true,
|
||
},
|
||
|
||
"plaintext-password-ios": {
|
||
// no service password-encryption + enable password + password 0
|
||
"cisco-bad": false, "cisco-good": true,
|
||
// password encrypted <md5> — незворотна схема, не порушення
|
||
"eltex-mixed": true,
|
||
// password 0 <відкрито>
|
||
"zte-mixed": false,
|
||
},
|
||
"plaintext-password-vrp": {
|
||
"huawei-bad": false, "huawei-good": true,
|
||
"h3c-bad": false, "h3c-good": true,
|
||
},
|
||
"plaintext-password-routeros": {
|
||
"mikrotik-bad": false, "mikrotik-good": true,
|
||
},
|
||
|
||
"syslog-target-ios": {
|
||
"cisco-bad": false, "cisco-good": true,
|
||
"eltex-mixed": true,
|
||
// logging server <адреса> — форма ZTE
|
||
"zte-mixed": true,
|
||
},
|
||
"syslog-target-vrp": {
|
||
"huawei-bad": false, "huawei-good": true,
|
||
"h3c-bad": false, "h3c-good": true,
|
||
},
|
||
"syslog-target-dlink": {
|
||
"dlink-bad": false, "dlink-good": true,
|
||
},
|
||
"syslog-target-routeros": {
|
||
// /system logging без action — це журнал у пам'ять
|
||
"mikrotik-bad": false, "mikrotik-good": true,
|
||
},
|
||
|
||
"ntp-source-ios": {
|
||
"cisco-bad": false, "cisco-good": true,
|
||
// sntp server — форма Eltex
|
||
"eltex-mixed": true,
|
||
"zte-mixed": true,
|
||
},
|
||
"ntp-source-vrp": {
|
||
"huawei-bad": false, "huawei-good": true,
|
||
"h3c-bad": false, "h3c-good": true,
|
||
},
|
||
"ntp-source-dlink": {
|
||
// disable sntp
|
||
"dlink-bad": false, "dlink-good": true,
|
||
},
|
||
"ntp-source-routeros": {
|
||
"mikrotik-bad": false, "mikrotik-good": true,
|
||
},
|
||
|
||
"mgmt-acl-ios": {
|
||
"cisco-bad": false, "cisco-good": true,
|
||
// management access-class — форма Eltex
|
||
"eltex-mixed": true,
|
||
// на ZTE список доступу до vty не заведений
|
||
"zte-mixed": false,
|
||
},
|
||
"mgmt-acl-vrp": {
|
||
"huawei-bad": false, "huawei-good": true,
|
||
"h3c-bad": false, "h3c-good": true,
|
||
},
|
||
"mgmt-acl-dlink": {
|
||
"dlink-bad": false, "dlink-good": true,
|
||
},
|
||
"mgmt-acl-routeros": {
|
||
"mikrotik-bad": false, "mikrotik-good": true,
|
||
},
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Самі перевірки
|
||
// ---------------------------------------------------------------------
|
||
|
||
// Склад набору звіряється поіменно: правило, яке з'явилось у міграції й
|
||
// не має очікувань, інакше проїхало б повз тест непоміченим — тобто
|
||
// найдорожчим способом.
|
||
func TestBuiltinRulesAllCovered(t *testing.T) {
|
||
rules := loadBuiltinRules(t)
|
||
|
||
var missing []string
|
||
for key := range rules {
|
||
if _, ok := expect[key]; !ok {
|
||
missing = append(missing, key)
|
||
}
|
||
}
|
||
sort.Strings(missing)
|
||
if len(missing) > 0 {
|
||
t.Errorf("правила без очікувань у тесті: %s", strings.Join(missing, ", "))
|
||
}
|
||
|
||
var stale []string
|
||
for key := range expect {
|
||
if _, ok := rules[key]; !ok {
|
||
stale = append(stale, key)
|
||
}
|
||
}
|
||
sort.Strings(stale)
|
||
if len(stale) > 0 {
|
||
t.Errorf("очікування для правил, яких у міграції немає: %s", strings.Join(stale, ", "))
|
||
}
|
||
}
|
||
|
||
// Кожне правило звіряється зі зразками конфігів усіх своїх вендорів.
|
||
func TestBuiltinRulesAgainstRealConfigs(t *testing.T) {
|
||
rules := loadBuiltinRules(t)
|
||
|
||
for key, br := range rules {
|
||
want, ok := expect[key]
|
||
if !ok {
|
||
continue // про це вже сказав TestBuiltinRulesAllCovered
|
||
}
|
||
|
||
re, err := regexp.Compile(br.Pattern)
|
||
if err != nil {
|
||
t.Errorf("%s: зразок не компілюється: %v", key, err)
|
||
continue
|
||
}
|
||
rule := ComplianceRule{Kind: br.Kind, Pattern: br.Pattern}
|
||
|
||
for name, s := range samples {
|
||
// Правило застосовне до цього конфігу, якщо вендорів у
|
||
// нього немає взагалі або цей вендор у переліку.
|
||
if !applies(br.Vendors, s.Vendor) {
|
||
continue
|
||
}
|
||
exp, has := want[name]
|
||
if !has {
|
||
t.Errorf("%s: для вендора %s (зразок %s) немає очікування — "+
|
||
"вендор у правилі є, а перевіреного конфігу немає",
|
||
key, s.Vendor, name)
|
||
continue
|
||
}
|
||
|
||
got, line, num := checkCompliance(rule, re, s.Body)
|
||
if got != exp {
|
||
t.Errorf("%s на %s: очікували %s, отримали %s (рядок %d: %q)",
|
||
key, name, verdict(exp), verdict(got), num, line)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Найважливіша перевірка файлу.
|
||
//
|
||
// Зразок, який не збігається НІКОЛИ, дає «порушень немає» на всьому
|
||
// парку — тобто ідеальний вигляд і нуль користі. Зразок, який
|
||
// збігається ЗАВЖДИ, дає суцільне червоне, після якого на розділ
|
||
// перестають дивитись. Обидва стани перевіряються однаково: у наборі
|
||
// зразків має бути хоч один конфіг, де правило спрацювало, і хоч один,
|
||
// де ні.
|
||
func TestBuiltinRulesAreNotAlwaysGreen(t *testing.T) {
|
||
rules := loadBuiltinRules(t)
|
||
|
||
for key, br := range rules {
|
||
re, err := regexp.Compile(br.Pattern)
|
||
if err != nil {
|
||
continue // про це вже сказав попередній тест
|
||
}
|
||
rule := ComplianceRule{Kind: br.Kind, Pattern: br.Pattern}
|
||
|
||
var passed, failed int
|
||
for _, s := range samples {
|
||
if !applies(br.Vendors, s.Vendor) {
|
||
continue
|
||
}
|
||
if ok, _, _ := checkCompliance(rule, re, s.Body); ok {
|
||
passed++
|
||
} else {
|
||
failed++
|
||
}
|
||
}
|
||
if failed == 0 {
|
||
t.Errorf("%s: зразок не спрацював ЖОДНОГО разу на %d конфігах своїх вендорів — "+
|
||
"таке правило показує «порушень немає» завжди", key, passed)
|
||
}
|
||
if passed == 0 {
|
||
t.Errorf("%s: зразок спрацював на ВСІХ %d конфігах своїх вендорів — "+
|
||
"правило, яке ніколи не буває зеленим, перестають читати", key, failed)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Кожен вендор, названий у правилах, мусить мати конфіг у наборі.
|
||
//
|
||
// Без цієї перевірки вендора можна дописати в перелік правила, не
|
||
// показавши жодного його конфігу, — і правило мовчки поширилось би на
|
||
// синтаксис, якого ніхто не бачив.
|
||
func TestBuiltinRuleVendorsHaveSamples(t *testing.T) {
|
||
rules := loadBuiltinRules(t)
|
||
|
||
have := map[string]bool{}
|
||
for _, s := range samples {
|
||
have[s.Vendor] = true
|
||
}
|
||
|
||
seen := map[string]bool{}
|
||
for key, br := range rules {
|
||
for _, v := range br.Vendors {
|
||
if !have[v] && !seen[key+v] {
|
||
seen[key+v] = true
|
||
t.Errorf("%s: вендор %q названий у правилі, а конфігу цього вендора в наборі немає",
|
||
key, v)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Тип конфігу заданий явно в кожному правилі (0056), і це саме
|
||
// 'running': жодне з правил набору не описує конфіг-файли сервера.
|
||
func TestBuiltinRulesDeclareConfigType(t *testing.T) {
|
||
for key, br := range loadBuiltinRules(t) {
|
||
if br.ConfigType != "running" {
|
||
t.Errorf("%s: тип конфігу %q — набір писався під конфіги заліза", key, br.ConfigType)
|
||
}
|
||
if !complianceKinds[br.Kind] {
|
||
t.Errorf("%s: вид правила %q рушій не знає", key, br.Kind)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Прототипи з цієї ж міграції мають бути вимкнені.
|
||
//
|
||
// Правило проєкту: оновлення не вмикає нічого, що заводить чеки.
|
||
// Ввімкнений прототип у вбудованому шаблоні заводить snmp.walk і
|
||
// snmp.get на кожному хості, якому шаблон причеплений, — мовчки, під
|
||
// час накату. Перевіряти це очима на рев'ю вже двічі не вийшло.
|
||
func TestBuiltinPrototypesShipDisabled(t *testing.T) {
|
||
body, err := schema.Files.ReadFile(builtinMigration)
|
||
if err != nil {
|
||
t.Fatalf("міграція %s не читається: %v", builtinMigration, err)
|
||
}
|
||
text := string(body)
|
||
|
||
// Хвіст рядка INSERT у tpl.item_prototypes: discovery_sec,
|
||
// interval_sec, max_rows, enabled.
|
||
tail := regexp.MustCompile(`(?m)^\s*\d+,\s*\d+,\s*\d+,\s*(true|false)\)`)
|
||
hits := tail.FindAllStringSubmatch(text, -1)
|
||
if len(hits) != 2 {
|
||
t.Fatalf("очікували 2 прототипи в міграції, знайшли %d", len(hits))
|
||
}
|
||
for _, h := range hits {
|
||
if h[1] != "false" {
|
||
t.Errorf("прототип заведено ввімкненим: %q", strings.TrimSpace(h[0]))
|
||
}
|
||
}
|
||
}
|
||
|
||
func applies(vendors []string, vendor string) bool {
|
||
if len(vendors) == 0 {
|
||
return true
|
||
}
|
||
for _, v := range vendors {
|
||
if v == vendor {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func verdict(passed bool) string {
|
||
if passed {
|
||
return "«порушень немає»"
|
||
}
|
||
return "«порушення»"
|
||
}
|