П'ять паралельних задач. Найцінніше в них — не можливості, а знайдене.
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 працює справжній.
412 lines
25 KiB
Go
412 lines
25 KiB
Go
package store
|
||
|
||
import "strings"
|
||
|
||
// Словник дій журналу аудиту.
|
||
//
|
||
// Навіщо він узагалі. Ключ `ncm.command_run.delete` читається лише тим,
|
||
// хто його писав; людина, яка шукає, хто стер результати прогону,
|
||
// шукає «видалення». Але сам ключ прибирати не можна: за ним фільтрують,
|
||
// його шлють у підтримку, за ним шукають у логах сервера. Тому в
|
||
// інтерфейсі є обидва — фраза великим, ключ поруч дрібним.
|
||
//
|
||
// Словник живе тут, а не в браузері, з однієї причини: перелік дій — це
|
||
// властивість того, що продукт ЗАПИСУЄ, а не того, як це показано.
|
||
// Наступний, хто додасть виклик WriteAudit, дописує рядок сюди, поруч
|
||
// із рештою, — і сторінка, вивантаження й будь-що майбутнє отримують
|
||
// однакову назву. Копія словника в TypeScript розійшлася б із цією на
|
||
// першому ж новому рядку, і розбіжність було б видно лише тому, хто
|
||
// відкриє обидва файли поруч.
|
||
//
|
||
// Ключа, якого тут немає, це не ламає: подія показується самим лише
|
||
// ключем. Журнал старший за словник — у ньому лежать дії збірок, яких
|
||
// уже немає, і мовчки ховати їх було б гірше, ніж показати як є.
|
||
//
|
||
// ЧОМУ КЛЮЧІ — КОНСТАНТИ, А НЕ РЯДКИ ПО МІСЦЯХ ВИКЛИКУ
|
||
//
|
||
// Прохання «не забудь дописати рядок сюди» цей файл уже програв: за
|
||
// півроку в журнал почали писати дзеркало Git, ролі й прив'язку хоста
|
||
// до зонда — жодна з цих дій до словника не потрапила, і адміністратор
|
||
// бачив у переліку сирі ключі. Помітити це неможливо ніяк, крім як
|
||
// відкрити журнал і впізнати відсутню назву.
|
||
//
|
||
// Тому ключ тепер має рівно одне місце оголошення — константу нижче, —
|
||
// а поруч стоїть тест (audit_actions_source_test.go), який читає ВЕСЬ
|
||
// server/internal, знаходить кожне присвоєння AuditEntry.Action і падає
|
||
// на двох речах: ключ не оголошено константою тут або в константи немає
|
||
// назви у словнику. Тобто наступна нова дія не має способу потрапити в
|
||
// журнал безіменною: збірка не пройде.
|
||
|
||
// Ключі дій. Значення — те, що лягає в core.audit_log.action; змінювати
|
||
// їх заднім числом не можна, бо в журналі вже лежать рядки зі старим
|
||
// значенням, а журнал не переписують.
|
||
const (
|
||
AuditActionDeviceBulkUpdate = "inv.device.bulk_update"
|
||
AuditActionDeviceBulkDelete = "inv.device.bulk_delete"
|
||
AuditActionDeviceBulkPurge = "inv.device.bulk_purge"
|
||
AuditActionDeviceBulkRestore = "inv.device.bulk_restore"
|
||
AuditActionDeviceSelfAgent = "inv.device.self_agent"
|
||
AuditActionDeviceSelfAgentUn = "inv.device.self_agent_clear"
|
||
|
||
AuditActionCommandRunCreate = "ncm.command_run.create"
|
||
AuditActionCommandRunCancel = "ncm.command_run.cancel"
|
||
AuditActionCommandRunDelete = "ncm.command_run.delete"
|
||
AuditActionCommandRunReport = "ncm.command_run.report"
|
||
|
||
AuditActionRollbackCreate = "ncm.rollback.create"
|
||
AuditActionRollbackApprove = "ncm.rollback.approve"
|
||
AuditActionRollbackReject = "ncm.rollback.reject"
|
||
AuditActionRollbackPolicy = "ncm.rollback_policy.update"
|
||
|
||
AuditActionConfigDelete = "ncm.config.delete"
|
||
AuditActionRetentionUpdate = "ncm.retention.update"
|
||
AuditActionMirrorUpdate = "ncm.mirror.update"
|
||
AuditActionMirrorPush = "ncm.mirror.push"
|
||
|
||
AuditActionRoleCreate = "core.role.create"
|
||
AuditActionRoleUpdate = "core.role.update"
|
||
AuditActionRoleDelete = "core.role.delete"
|
||
|
||
AuditActionTelegramLink = "core.telegram.link"
|
||
AuditActionTelegramUnlink = "core.telegram.unlink"
|
||
|
||
AuditActionRetentionSettings = "core.retention.update"
|
||
|
||
AuditActionSLATargetSave = "core.sla_target.save"
|
||
AuditActionSLATargetDelete = "core.sla_target.delete"
|
||
AuditActionSLAPeriodClose = "core.sla_period.close"
|
||
|
||
AuditActionBillingPlan = "bill.plan.update"
|
||
AuditActionLicenseApply = "bill.license.apply"
|
||
AuditActionLicenseClear = "bill.license.clear"
|
||
)
|
||
|
||
// Ключі типів об'єктів — те, НАД ЧИМ зроблено дію.
|
||
const (
|
||
AuditObjectDevice = "inv.device"
|
||
AuditObjectCommandRun = "ncm.command_run"
|
||
AuditObjectRollback = "ncm.rollback"
|
||
AuditObjectRollbackPolicy = "ncm.rollback_policy"
|
||
AuditObjectConfig = "ncm.config"
|
||
AuditObjectBackupDefaults = "ncm.backup_defaults"
|
||
AuditObjectMirror = "ncm.mirror"
|
||
AuditObjectRole = "core.role"
|
||
AuditObjectTelegram = "core.telegram_account"
|
||
AuditObjectRetention = "core.retention_settings"
|
||
AuditObjectSLATarget = "core.sla_target"
|
||
AuditObjectBillingPlan = "bill.plan"
|
||
AuditObjectLicense = "bill.license"
|
||
)
|
||
|
||
// AuditActionInfo — те, що словник знає про дію.
|
||
type AuditActionInfo struct {
|
||
Key string `json:"key"`
|
||
Label string `json:"label"`
|
||
Group string `json:"group"`
|
||
// Destructive — після цієї дії об'єкта більше немає. Не «важлива»
|
||
// й не «небезпечна»: важливість суб'єктивна, а «щось зникло» —
|
||
// факт, і саме за ним журнал переглядають найчастіше.
|
||
Destructive bool `json:"destructive,omitempty"`
|
||
}
|
||
|
||
// auditActions — усе, що продукт уміє записувати СЬОГОДНІ.
|
||
//
|
||
// Перелік і є вся правда про покриття: аудит пишуть масові дії над
|
||
// хостами, виконання команд, руйнівні дії над архівом конфігів,
|
||
// налаштування дзеркала Git і зміни складу ролей. Решта продукту в
|
||
// журнал не пише нічого — див. AuditBlindSpots.
|
||
var auditActions = []AuditActionInfo{
|
||
{
|
||
Key: AuditActionDeviceBulkUpdate, Group: "Інвентар",
|
||
Label: "Масова правка хостів",
|
||
},
|
||
{
|
||
Key: AuditActionDeviceBulkDelete, Group: "Інвентар",
|
||
// Назва навмисно широка. Досі під цим ключем писались ОБИДВА
|
||
// режими видалення — і архівний, і повний (режим лежав у
|
||
// meta.mode), тож у журналі за ним стоять і ті, й ті рядки.
|
||
// Звузити назву до «архівування» означало б перейменувати
|
||
// заднім числом чужі події, яких ніхто вже не перевірить.
|
||
Label: "Масове видалення хостів", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionDeviceBulkPurge, Group: "Інвентар",
|
||
Label: "Повне видалення хостів разом з історією", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionDeviceBulkRestore, Group: "Інвентар",
|
||
Label: "Відновлення хостів з архіву",
|
||
},
|
||
{
|
||
Key: AuditActionDeviceSelfAgent, Group: "Інвентар",
|
||
Label: "Прив'язка хоста до машини зонда",
|
||
},
|
||
{
|
||
Key: AuditActionDeviceSelfAgentUn, Group: "Інвентар",
|
||
Label: "Зняття прив'язки хоста до машини зонда",
|
||
},
|
||
{
|
||
Key: AuditActionCommandRunCreate, Group: "Команди",
|
||
Label: "Запуск команд на обладнанні",
|
||
},
|
||
{
|
||
Key: AuditActionCommandRunCancel, Group: "Команди",
|
||
Label: "Зупинка прогону команд",
|
||
},
|
||
{
|
||
Key: AuditActionCommandRunDelete, Group: "Команди",
|
||
Label: "Видалення прогону разом із виводом", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionCommandRunReport, Group: "Команди",
|
||
Label: "Вивантаження звіту про прогін",
|
||
},
|
||
{
|
||
Key: AuditActionRollbackCreate, Group: "Конфігурації",
|
||
// Створення наміру, а не сама заливка. Різниця важлива: намір
|
||
// може так і не поїхати на пристрій — його відхилять або він
|
||
// застаріє. Те, що на залізо справді писали, видно в самому
|
||
// відкаті (стани applying → verifying → applied), і дублювати
|
||
// це рядком у журналі означало б мати два джерела правди про
|
||
// подію, яку найгірше знати неточно.
|
||
Label: "Намір відкотити конфіг на пристрої",
|
||
},
|
||
{
|
||
Key: AuditActionRollbackApprove, Group: "Конфігурації",
|
||
// Найважливіший рядок розділу. Відкат за політикою погоджує
|
||
// ДРУГА людина, і питання «хто дозволив залити старий конфіг на
|
||
// магістральний вузол» має мати відповідь з іменем і часом —
|
||
// незалежно від того, чи вцілів сам намір у базі.
|
||
Label: "Погодження відкату конфігу",
|
||
},
|
||
{
|
||
Key: AuditActionRollbackReject, Group: "Конфігурації",
|
||
Label: "Відмова у відкаті конфігу",
|
||
},
|
||
{
|
||
Key: AuditActionRollbackPolicy, Group: "Конфігурації",
|
||
// Зміна політики нічого не ламає в мить збереження — вона
|
||
// змінює правила для всього, що станеться далі. Саме тому
|
||
// рядок тут: «вимогу другої людини вимкнули за годину до
|
||
// аварії» інакше не з'ясувати ніяк.
|
||
Label: "Зміна правил погодження відкату",
|
||
},
|
||
{
|
||
Key: AuditActionConfigDelete, Group: "Конфігурації",
|
||
Label: "Видалення збережених версій конфігів", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionRetentionUpdate, Group: "Конфігурації",
|
||
Label: "Зміна політики очистки конфігів",
|
||
},
|
||
{
|
||
Key: AuditActionMirrorUpdate, Group: "Конфігурації",
|
||
// Одна назва на три дії, і це не спрощення: налаштування
|
||
// дзеркала, видача ключа розгортання й вимкнення дзеркала
|
||
// міняють той самий об'єкт і той самий рядок налаштувань.
|
||
// Що саме змінилось, видно в meta (url, auth, deploy_key), а
|
||
// три окремі рядки у фільтрі означали б три способи спитати
|
||
// одне питання.
|
||
Label: "Налаштування дзеркала Git",
|
||
},
|
||
{
|
||
Key: AuditActionMirrorPush, Group: "Конфігурації",
|
||
Label: "Примусовий пуш архіву на дзеркало",
|
||
},
|
||
{
|
||
Key: AuditActionRoleCreate, Group: "Адміністрування",
|
||
Label: "Створення ролі",
|
||
},
|
||
{
|
||
Key: AuditActionRoleUpdate, Group: "Адміністрування",
|
||
Label: "Зміна прав ролі",
|
||
},
|
||
{
|
||
Key: AuditActionRoleDelete, Group: "Адміністрування",
|
||
Label: "Видалення ролі", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionTelegramLink, Group: "Адміністрування",
|
||
// Прив'язка не видає нових прав, але дає новий СПОСІБ ними
|
||
// скористатися — з телефона, без входу в систему. Питання «чому
|
||
// алерт підтверджено о третій ночі акаунтом, який тоді нікуди
|
||
// не заходив» без цього рядка відповіді не має.
|
||
Label: "Прив'язка Telegram до облікового запису",
|
||
},
|
||
{
|
||
Key: AuditActionTelegramUnlink, Group: "Адміністрування",
|
||
Label: "Зняття прив'язки Telegram",
|
||
},
|
||
{
|
||
Key: AuditActionRetentionSettings, Group: "Адміністрування",
|
||
// Destructive не ставимо, і це не недогляд. Сама зміна нічого
|
||
// не стирає — стирає її наслідок, політика, яка вночі знесе
|
||
// чанки. Позначка «об'єкта більше немає» тут була б неправдою
|
||
// про мить події. Правду про наслідок несе meta: для кожного
|
||
// виду даних там стоїть shortened, тобто «строк скоротили», і
|
||
// саме за цим полем шукатимуть того, після кого зникла історія.
|
||
Label: "Зміна строків зберігання даних",
|
||
},
|
||
{
|
||
Key: AuditActionSLATargetSave, Group: "Адміністрування",
|
||
Label: "Зміна цілі SLA",
|
||
},
|
||
{
|
||
Key: AuditActionSLATargetDelete, Group: "Адміністрування",
|
||
// Тут Destructive стоїть, на відміну від строків зберігання, і
|
||
// різниця саме в миті: видалення цілі каскадом зносить УСІ її
|
||
// закриті періоди негайно, у цій же транзакції. Скільки саме —
|
||
// у meta.periods_deleted, бо через рік це буде єдине місце, де
|
||
// видно, що торішній звіт колись існував.
|
||
Label: "Видалення цілі SLA", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionSLAPeriodClose, Group: "Адміністрування",
|
||
// Закриття не руйнівне: воно, навпаки, робить число незмінним.
|
||
// А от meta.forced і meta.revision — те, за чим шукатимуть
|
||
// відповідь на «чому в мене два роздруки з різними числами».
|
||
Label: "Закриття періоду SLA",
|
||
},
|
||
{
|
||
Key: AuditActionBillingPlan, Group: "Тариф і ліцензія",
|
||
// Зміна тарифу нічого не стирає в мить збереження, але саме
|
||
// вона задає стелі — тобто з неї починається кожне «а чому в
|
||
// нас перестали заводитись хости». Питання ставлять за тиждень
|
||
// після події, і без цього рядка відповідь на нього не має де
|
||
// взятись: bill.entitlements переписується цілком, попереднього
|
||
// стану там не лишається. Тому в meta їдуть обидва тарифи, «з»
|
||
// і «на».
|
||
Label: "Зміна тарифу",
|
||
},
|
||
{
|
||
Key: AuditActionLicenseApply, Group: "Тариф і ліцензія",
|
||
// Найважливіший рядок розділу. Ключ задає стелі ВСІЄЇ
|
||
// інсталяції, включно з чужими кабінетами на спільному
|
||
// хостингу, і робить це одним натисканням. Самого ключа в meta
|
||
// немає й не буде — це секрет; там лише його ідентифікатор,
|
||
// кому виданий і до якої дати.
|
||
Label: "Застосування ліцензійного ключа",
|
||
},
|
||
{
|
||
Key: AuditActionLicenseClear, Group: "Тариф і ліцензія",
|
||
// Destructive не ставимо: рядок у bill.license_keys лишається,
|
||
// зникає лише те, що діє. Але наслідок відчутний — стелі
|
||
// повертаються до «не задані», — і саме тому дія в журналі.
|
||
Label: "Зняття ліцензійного ключа",
|
||
},
|
||
}
|
||
|
||
var auditActionByKey = func() map[string]AuditActionInfo {
|
||
m := make(map[string]AuditActionInfo, len(auditActions))
|
||
for _, a := range auditActions {
|
||
m[a.Key] = a
|
||
}
|
||
return m
|
||
}()
|
||
|
||
// AuditActions — словник для фільтра на сторінці.
|
||
func AuditActions() []AuditActionInfo { return auditActions }
|
||
|
||
// auditObjectTypes — назви типів об'єктів.
|
||
//
|
||
// Окремо від дій, бо це різні питання: дія — що зробили, тип — над чим.
|
||
// Один тип зачіпають кілька дій, і зводити їх в одну таблицю означало б
|
||
// повторювати назву об'єкта в кожному рядку.
|
||
//
|
||
// Перелік, а не мапа: порядок тут значущий — типи йдуть у тому ж
|
||
// порядку, у якому людина зустрічає їх у фільтрі дій, а мапа порядку не
|
||
// має. Раніше поруч лежав окремий список `order`, і будь-який новий тип
|
||
// мовчки не потрапляв у фільтр доти, доки його не допишуть удруге.
|
||
var auditObjectTypes = []AuditActionInfo{
|
||
{Key: AuditObjectDevice, Label: "Хост"},
|
||
{Key: AuditObjectCommandRun, Label: "Прогін команд"},
|
||
{Key: AuditObjectRollback, Label: "Відкат конфігу"},
|
||
{Key: AuditObjectRollbackPolicy, Label: "Правила погодження відкату"},
|
||
{Key: AuditObjectConfig, Label: "Версія конфігу"},
|
||
{Key: AuditObjectBackupDefaults, Label: "Налаштування бекапів"},
|
||
{Key: AuditObjectMirror, Label: "Дзеркало Git"},
|
||
{Key: AuditObjectRole, Label: "Роль"},
|
||
{Key: AuditObjectTelegram, Label: "Прив'язка Telegram"},
|
||
{Key: AuditObjectRetention, Label: "Строки зберігання даних"},
|
||
{Key: AuditObjectSLATarget, Label: "Ціль SLA"},
|
||
{Key: AuditObjectBillingPlan, Label: "Тариф"},
|
||
{Key: AuditObjectLicense, Label: "Ліцензія інсталяції"},
|
||
}
|
||
|
||
var auditObjectTypeByKey = func() map[string]string {
|
||
m := make(map[string]string, len(auditObjectTypes))
|
||
for _, o := range auditObjectTypes {
|
||
m[o.Key] = o.Label
|
||
}
|
||
return m
|
||
}()
|
||
|
||
// AuditObjectTypes — словник типів для фільтра.
|
||
func AuditObjectTypes() []AuditActionInfo { return auditObjectTypes }
|
||
|
||
// decorateAuditEvent дописує до події те, що знає словник.
|
||
func decorateAuditEvent(e *AuditEvent) {
|
||
if a, ok := auditActionByKey[e.Action]; ok {
|
||
e.ActionLabel = a.Label
|
||
e.ActionGroup = a.Group
|
||
e.Destructive = a.Destructive
|
||
} else {
|
||
// Ключа немає в словнику. Не вигадуємо назву з ключа — назва,
|
||
// зібрана з крапок, читається як фраза, але означає лише те, що
|
||
// хтось назвав змінну. Порожня мітка чесніша: сторінка покаже
|
||
// ключ і позначить, що назви для нього немає.
|
||
e.ActionGroup = auditActionGroupFromKey(e.Action)
|
||
}
|
||
if l, ok := auditObjectTypeByKey[e.ObjectType]; ok {
|
||
e.ObjectTypeLabel = l
|
||
}
|
||
}
|
||
|
||
// auditActionGroupFromKey — до якого розділу віднести незнайому дію.
|
||
//
|
||
// Це єдине, що з ключа справді виводиться: перший сегмент — схема бази,
|
||
// і вона не змінюється разом із формулюванням.
|
||
func auditActionGroupFromKey(key string) string {
|
||
head, _, _ := strings.Cut(key, ".")
|
||
switch head {
|
||
case "inv":
|
||
return "Інвентар"
|
||
case "ncm":
|
||
return "Конфігурації"
|
||
case "topo":
|
||
return "Топологія"
|
||
case "alr":
|
||
return "Сповіщення"
|
||
case "core":
|
||
return "Адміністрування"
|
||
case "bill":
|
||
return "Тариф і ліцензія"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// AuditBlindSpots — чесний перелік того, чого журнал НЕ бачить.
|
||
//
|
||
// Сторінка без цього блоку створює хибне відчуття повноти: «тут нічого
|
||
// немає» читається як «нічого не робили», хоча означає лише «це місце
|
||
// продукту в журнал не пише». Найдорожча помилка журналу — не
|
||
// неправильний запис, а відсутній: неправильний помітно, відсутнього
|
||
// немає з чим порівняти.
|
||
//
|
||
// Перелік складено від протилежного до auditActions: усе, що не
|
||
// перелічено там, не записується. Рядки тримаються поруч зі словником
|
||
// саме тому, що правити їх треба разом — новий виклик WriteAudit має
|
||
// одночасно з'явитись у словнику й зникнути звідси.
|
||
func AuditBlindSpots() []string {
|
||
return []string{
|
||
"Вхід у систему, вихід і невдалі спроби входу. Спроби входу — і вдалі, і ні — лягають в окрему таблицю core.login_attempts, а не сюди: журнал аудиту вимагає tenant_id, а на момент перевірки пароля кабінет ще невідомий. Тобто цієї сторінки для питання «хто заходив» замало.",
|
||
"Поодинокі зміни хостів: створення, правка й видалення одного хоста в його картці. У журнал пишуть лише масові дії — а «видалив один хост» і «видалив сорок» відрізняються масштабом, не суттю. Виняток — повернення хоста з архіву й повне видалення: обидві йдуть масовим шляхом навіть для одного хоста, тож записуються завжди.",
|
||
"Склад команди: запрошення, зміна ролі людини, вилучення з кабінету. САМІ ролі — створення, зміна набору прав, видалення — з 0053 у журналі є (core.role.*), а от «кому цю роль видали» — ні. Тобто на питання «звідки в цієї людини такий доступ» журнал відповідає лише наполовину: що дозволяє роль, видно, хто в ній опинився — ні.",
|
||
"Доступи до обладнання: створення, правка й видалення облікових даних, а також те, кому їх призначили.",
|
||
"Мапи, групи, шаблони, правила алертів і канали сповіщень. Правка мапи оборотна й видима в самій мапі, але «хто вимкнув правило, за яким приходив алерт» звідси не видно.",
|
||
"Ручний запуск збору конфігу. Відкат конфігурації журнал бачить з 0060 — намір, погодження, відмову й зміну правил погодження, — а от «зібрати зараз» лишається поза ним: збір нічого не змінює на пристрої.",
|
||
"Читання. Журнал записує зміни, а не перегляди: те, що хтось відкрив чужий конфіг або вивантажив архів, тут не з'явиться — окрім вивантаження звіту про прогін команд.",
|
||
"Дії зондів і колектора: усе, що система робить сама за розкладом, журналом не покривається — це не дії людини, і їхнє місце в логах служб.",
|
||
}
|
||
}
|