За цілий день активної роботи в журналі не з'явилось нічого: цих доменів у ньому просто не було. Проєкт це визнавав сам, у AuditBlindSpots(). Тепер пишуться: правила алертів (створення/зміна/видалення й окремо вимкнення-увімкнення), канали, драбини ескалації, правила відповідності; додавання людини в кабінет, зміна ролі, вилучення, правка профілю й скидання пароля. Два рішення про зміст запису: * вимкнення видно з НАЗВИ дії (alr.rule.disable), а не з різниці подробиць — питання «хто вимкнув правило, за яким приходив алерт» має відповідатись переліком, а не порівнянням; * config каналу не їде в запис ВЗАГАЛІ — там не лише токен бота, а й адреса вебхука (доступ на запис у чужий чат) і заголовок Authorization. Замість нього прапорець secret_changed. Мапи, групи й шаблони свідомо лишились у сліпих зонах: їхня правка видима в самому об'єкті. Туди ж додано дві нові чесні межі — тригери від шаблонів пишуться повз обробник, і пробне повідомлення в канал. Прогнано проти справжньої бази: секрет каналу й пароль учасника в подробиці не потрапляють. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
572 lines
35 KiB
Go
572 lines
35 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"
|
||
|
||
AuditActionAlertRuleCreate = "alr.rule.create"
|
||
AuditActionAlertRuleUpdate = "alr.rule.update"
|
||
AuditActionAlertRuleDelete = "alr.rule.delete"
|
||
AuditActionAlertRuleEnable = "alr.rule.enable"
|
||
AuditActionAlertRuleDisable = "alr.rule.disable"
|
||
|
||
AuditActionChannelCreate = "alr.channel.create"
|
||
AuditActionChannelUpdate = "alr.channel.update"
|
||
AuditActionChannelDelete = "alr.channel.delete"
|
||
|
||
AuditActionEscalationCreate = "alr.escalation_policy.create"
|
||
AuditActionEscalationUpdate = "alr.escalation_policy.update"
|
||
AuditActionEscalationDelete = "alr.escalation_policy.delete"
|
||
|
||
AuditActionComplianceRuleCreate = "ncm.compliance_rule.create"
|
||
AuditActionComplianceRuleUpdate = "ncm.compliance_rule.update"
|
||
AuditActionComplianceRuleDelete = "ncm.compliance_rule.delete"
|
||
|
||
AuditActionRoleCreate = "core.role.create"
|
||
AuditActionRoleUpdate = "core.role.update"
|
||
AuditActionRoleDelete = "core.role.delete"
|
||
|
||
AuditActionMemberAdd = "core.member.add"
|
||
AuditActionMemberRole = "core.member.role"
|
||
AuditActionMemberRemove = "core.member.remove"
|
||
AuditActionMemberProfile = "core.member.profile"
|
||
AuditActionMemberPassword = "core.member.password"
|
||
|
||
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"
|
||
AuditObjectComplianceRule = "ncm.compliance_rule"
|
||
AuditObjectAlertRule = "alr.rule"
|
||
AuditObjectChannel = "alr.channel"
|
||
AuditObjectEscalation = "alr.escalation_policy"
|
||
AuditObjectRole = "core.role"
|
||
AuditObjectMember = "core.member"
|
||
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: AuditActionComplianceRuleCreate, Group: "Конфігурації",
|
||
Label: "Створення правила відповідності",
|
||
},
|
||
{
|
||
Key: AuditActionComplianceRuleUpdate, Group: "Конфігурації",
|
||
// Зразок правила їде в meta повністю, і саме він тут головний.
|
||
// Правило «Telnet вимкнено», якому непомітно послабили зразок,
|
||
// далі показує зелене — тобто відповідає «вимогу дотримано» на
|
||
// вимогу, якої вже немає. Вимкнення теж лягає сюди полем
|
||
// enabled, а не окремою дією: вимкнене правило лишається
|
||
// видимим у списку й окремим розділом у звіті (див. шапку
|
||
// DeleteComplianceRule), тож факт «вимогу знято» видно й без
|
||
// журналу — на відміну від алертного правила, яке зі списку
|
||
// нікуди не дінеться, але перестане будити.
|
||
Label: "Зміна правила відповідності",
|
||
},
|
||
{
|
||
Key: AuditActionComplianceRuleDelete, Group: "Конфігурації",
|
||
// Найдорожчий рядок розділу. Кабінет, у якому правило стерли,
|
||
// виглядає у звіті рівно як кабінет, де вимогу скрізь виконано:
|
||
// ні порушень, ні згадки, що питання взагалі ставили.
|
||
Label: "Видалення правила відповідності", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionAlertRuleCreate, Group: "Сповіщення",
|
||
Label: "Створення правила алертів",
|
||
},
|
||
{
|
||
Key: AuditActionAlertRuleUpdate, Group: "Сповіщення",
|
||
Label: "Зміна правила алертів",
|
||
},
|
||
{
|
||
Key: AuditActionAlertRuleDelete, Group: "Сповіщення",
|
||
Label: "Видалення правила алертів", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionAlertRuleDisable, Group: "Сповіщення",
|
||
// Окрема дія, а не поле в «Зміні правила», і це головне рішення
|
||
// розділу. Питання, заради якого сюди приходять, звучить «хто
|
||
// вимкнув правило, за яким приходив алерт», а не «яке поле
|
||
// змінилось із сімнадцяти»: вимкнення гасить усі активні алерти
|
||
// правила й прибирає його з вибірки движка, тобто мережа
|
||
// замовкає рівно в тому місці, де на неї покладались. Шукати це
|
||
// звірянням двох знімків meta означало б знати заздалегідь, що
|
||
// саме сталось.
|
||
Label: "Вимкнення правила алертів",
|
||
},
|
||
{
|
||
Key: AuditActionAlertRuleEnable, Group: "Сповіщення",
|
||
Label: "Увімкнення правила алертів",
|
||
},
|
||
{
|
||
Key: AuditActionChannelCreate, Group: "Сповіщення",
|
||
Label: "Створення каналу сповіщень",
|
||
},
|
||
{
|
||
Key: AuditActionChannelUpdate, Group: "Сповіщення",
|
||
// Ні токена бота, ні вмісту config у meta немає й не буде:
|
||
// config каналу несе адресу вебхука та довільні заголовки, а це
|
||
// такий самий секрет, як токен (див. channelAuditMeta).
|
||
Label: "Зміна каналу сповіщень",
|
||
},
|
||
{
|
||
Key: AuditActionChannelDelete, Group: "Сповіщення",
|
||
Label: "Видалення каналу сповіщень", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionEscalationCreate, Group: "Сповіщення",
|
||
Label: "Створення драбини ескалації",
|
||
},
|
||
{
|
||
Key: AuditActionEscalationUpdate, Group: "Сповіщення",
|
||
Label: "Зміна драбини ескалації",
|
||
},
|
||
{
|
||
Key: AuditActionEscalationDelete, Group: "Сповіщення",
|
||
// У meta їде rule_count — скільки правил лишилось без ескалації
|
||
// цієї ж миті (ON DELETE SET NULL у 0066). Без цього числа
|
||
// видалення драбини виглядає як прибирання невикористаного
|
||
// рядка, а насправді це мовчазне зняття резервного механізму з
|
||
// десятка тригерів.
|
||
Label: "Видалення драбини ескалації", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionRoleCreate, Group: "Адміністрування",
|
||
Label: "Створення ролі",
|
||
},
|
||
{
|
||
Key: AuditActionRoleUpdate, Group: "Адміністрування",
|
||
Label: "Зміна прав ролі",
|
||
},
|
||
{
|
||
Key: AuditActionRoleDelete, Group: "Адміністрування",
|
||
Label: "Видалення ролі", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionMemberAdd, Group: "Адміністрування",
|
||
// Друга половина відповіді на «звідки в цієї людини такий
|
||
// доступ». Перша (що дозволяє роль) уже була в core.role.*, і
|
||
// сама по собі вона не відповідає ні на що: права без імені —
|
||
// це опис можливості, а не факт її видачі.
|
||
Label: "Додавання людини в кабінет",
|
||
},
|
||
{
|
||
Key: AuditActionMemberRole, Group: "Адміністрування",
|
||
// У meta стоять обидві ролі, «з» і «на», разом із ключем і
|
||
// назвою кожної. Ключ — бо за ним шукають, назва — бо через рік
|
||
// роль може називатись інакше або не існувати зовсім.
|
||
Label: "Зміна ролі учасника",
|
||
},
|
||
{
|
||
Key: AuditActionMemberRemove, Group: "Адміністрування",
|
||
// Destructive: членства більше немає, сесії в цьому кабінеті
|
||
// відкликані тією ж транзакцією. Сам обліковий запис лишається
|
||
// (людина може працювати в іншому кабінеті) — саме тому в meta
|
||
// їде роль, яку вилучений мав на момент вилучення: після
|
||
// видалення рядка memberships її не відновити нізвідки.
|
||
Label: "Вилучення людини з кабінету", Destructive: true,
|
||
},
|
||
{
|
||
Key: AuditActionMemberProfile, Group: "Адміністрування",
|
||
Label: "Зміна профілю учасника адміністратором",
|
||
},
|
||
{
|
||
Key: AuditActionMemberPassword, Group: "Адміністрування",
|
||
// Окрема дія з тієї ж причини, що й вимкнення правила: факт має
|
||
// бути видно з назви. Адміністратор, який змінив людині пароль,
|
||
// відкликав їй усі сесії й тепер знає, чим зайти під її іменем;
|
||
// далі в журналі підуть дії від ЇЇ облікового запису, і зв'язати
|
||
// їх із тим, хто пароль поставив, можна лише через цей рядок.
|
||
// Самого пароля в meta немає — ні старого, ні нового.
|
||
Label: "Зміна пароля учасника адміністратором",
|
||
},
|
||
{
|
||
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: AuditObjectComplianceRule, Label: "Правило відповідності"},
|
||
{Key: AuditObjectAlertRule, Label: "Правило алертів"},
|
||
{Key: AuditObjectChannel, Label: "Канал сповіщень"},
|
||
{Key: AuditObjectEscalation, Label: "Драбина ескалації"},
|
||
{Key: AuditObjectRole, Label: "Роль"},
|
||
{Key: AuditObjectMember, 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, а на момент перевірки пароля кабінет ще невідомий. Тобто цієї сторінки для питання «хто заходив» замало.",
|
||
"Поодинокі зміни хостів: створення, правка й видалення одного хоста в його картці. У журнал пишуть лише масові дії — а «видалив один хост» і «видалив сорок» відрізняються масштабом, не суттю. Виняток — повернення хоста з архіву й повне видалення: обидві йдуть масовим шляхом навіть для одного хоста, тож записуються завжди.",
|
||
"Видимість учасника: перелік груп, якими обмежено його доступ (memberships.scope_group_ids). Хто в кабінеті й з якою роллю, журнал бачить (core.member.*), а от звуження чи розширення видимості всередині ролі — ні; зараз його й нема чим змінити через API, тож рядок стоїть тут на випадок, коли така ручка з'явиться.",
|
||
"Доступи до обладнання: створення, правка й видалення облікових даних, а також те, кому їх призначили.",
|
||
"Мапи, групи й шаблони. Правка тут оборотна й видима в самому об'єкті: мапа зберігає ревізії й уміє відкат, склад групи видно у групі, у шаблона є час останньої правки. Хто саме правив — не видно, і це свідома межа, а не пропуск. Правила алертів, канали сповіщень і драбини ескалації з цього рядка прибрано: вони в журналі є (alr.*), бо їхня правка не лишає по собі нічого — вимкнене правило виглядає точно так само, як правило, за яким просто нічого не сталось.",
|
||
"Тригери, породжені шаблоном. Шаблон описує правило один раз на клас пристроїв, і правлять його там; окремого сліду про кожне похідне правило журнал не пише — інакше одна правка шаблону давала б сотню однакових рядків, за якими не видно самої правки.",
|
||
"Пробне повідомлення в канал (кнопка «Перевірити»). Нічого не змінює й нічого не вмикає — це діагностика, а не дія над каналом.",
|
||
"Ручний запуск збору конфігу. Відкат конфігурації журнал бачить з 0060 — намір, погодження, відмову й зміну правил погодження, — а от «зібрати зараз» лишається поза ним: збір нічого не змінює на пристрої.",
|
||
"Читання. Журнал записує зміни, а не перегляди: те, що хтось відкрив чужий конфіг або вивантажив архів, тут не з'явиться — окрім вивантаження звіту про прогін команд.",
|
||
"Дії зондів і колектора: усе, що система робить сама за розкладом, журналом не покривається — це не дії людини, і їхнє місце в логах служб.",
|
||
}
|
||
}
|