Netpulse_SasS/server/internal/store/csv_safe.go
byrsapty 6de3565bbe
All checks were successful
CI / hygiene (push) Successful in 9s
CI / web (push) Successful in 1m9s
CI / server (push) Successful in 1m56s
CI / agent (push) Successful in 2m56s
Відповідність: редагування правил, звіт і пʼять знахідок рецензії
Перший справжній прогін на живій мережі дав 18 порушень із 28: типові
SNMP-community на всіх шести хостах, telnet на керуванні на чотирьох,
паролі відкритим і зворотним текстом. Механізм працює — тому з
результатом тепер треба щось робити.

РЕДАГУВАННЯ. Вбудовані правила замкнені на те, що визначає ПИТАННЯ
(name, kind, pattern, config_type) і відкриті на політику кабінету
(enabled, severity, selector, remediation). Причина замка — доказ:
тест читає зразки з міграції й показує для кожного конфіг, де він
мусить спрацювати і де не мусить. Переписаний руками зразок цього
доказу не має, а значок «вбудоване» лишається — у звіті для аудитора
рядок означав би вже не те, що в довіднику. Для правок є копія.

Перевірка зразка на живому конфізі ДО збереження: віддає рядки з
номерами й окремо розрізняє «конфігу немає» від «нічого не знайшов».
Для правил «не має бути» нуль збігів підсвічується: це те саме, що
показало б правило з опискою.

ЗНАХІДКИ РЕЦЕНЗІЇ — всі пʼять підтверджені:

1. Перше збереження будь-якого вбудованого правила стирало результати.
   Селектор порівнювався в базі, але порівнювались різні представлення
   одного значення: міграція кладе {}, Go марширує сім ключів із null.
   Тепер порівняння за ЗНАЧЕННЯМ у Go, колонка канонізується сама.
2. CSV приймав ін’єкцію формул — у клітинку йде сирий рядок конфігу, а
   файл відкриває аудитор. Одне місце екранування на всі три звіти:
   дублювати захист у трьох файлах означає забути його в четвертому.
3. Знахідки вимкнених правил і зниклих хостів лишались назавжди й
   рахувались як чинні. Три заслони: фільтр у списку, прибирання при
   прогоні, і звіт їх не рахує.
4. Лічильники в списку правил рахувались по всіх хостах повз права —
   інженер філії бачив «5 з 12», а в знахідках дві. Тепер це одне
   число, а не два.
5. Доказ перевірки зразка лишався на екрані після правки зразка — тобто
   ручка робила протилежне до задуманого в мить найвищої довіри.
2026-08-28 13:40:40 +03:00

97 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package store
import (
"encoding/csv"
"strconv"
)
// Одне місце, де вивантаження захищається від ін'єкції формул.
//
// ПРИЧИНА. Excel і LibreOffice розбирають клітинку, що починається з
// `=`, `+`, `-`, `@`, а також з табуляції чи CR (їх вони спершу
// відкидають, а тоді дивляться на наступний символ), як ФОРМУЛУ, а не
// як текст. Формула виду `=cmd|'/C calc'!A0` виконується при відкритті
// файла — і виконується на машині того, хто його відкрив.
//
// У наших звітах у клітинки їде рівно те, що зробило б таку атаку
// дешевою: `v.Line` — це СИРИЙ рядок конфігу пристрою, тобто текст, який
// у мережу міг покласти хто завгодно з доступом до заліза; поруч —
// зразок правила, порада, назва правила й імена хостів. Файл за задумом
// відкриває керівник або аудитор, тобто людина з правами й без звички
// підозрювати таблицю.
//
// НАСЛІДОК, якби захисту не було: звіт про відповідність вимогам стає
// способом виконати код на робочій станції аудитора — тобто рівно
// протилежним до того, чим він є.
//
// ЧОМУ ОДНЕ МІСЦЕ, А НЕ ТРИ. Письменників CSV у репозиторії три:
// ComplianceReportCSV, SLAReportCSV і writeCSVReport (звіт NCM). Захист,
// переписаний у кожному, гарантовано забудуть у четвертому — і забудуть
// мовчки, бо зіпсований файл виглядає як звичайний. Тому всі троє пишуть
// не в `*csv.Writer`, а в safeCSV нижче.
//
// ЧОГО ЦЕ НЕ РОБИТЬ. Це не санітизація вмісту: рядок конфігу лишається
// собою, лише перестає бути формулою. І це не заміна перевірці на боці
// того, хто файл відкриває, — це найдешевший шар, який ми можемо
// покласти самі.
// csvFormulaLead — символи, з яких табличний процесор починає читати
// формулу. `\t` і `\r` тут не для краси: обидва відкидаються перед
// розбором, тож `\t=1+1` — це та сама формула, лише в обгортці.
func csvFormulaLead(b byte) bool {
switch b {
case '=', '+', '-', '@', '\t', '\r':
return true
}
return false
}
// csvSafeCell — клітинка, яку таблиця прочитає як текст.
//
// Апостроф попереду — домовленість самого Excel: він означає «далі
// текст» і в самій клітинці не показується.
//
// ВИНЯТОК ДЛЯ ЧИСЕЛ. `-5` і `-0.75` починаються з небезпечного символу й
// формулою не є. Екранувати їх означало б перетворити число на текст —
// і стовпчик, який читач звіту складає, перестав би складатися. Тому
// перед екрануванням питаємо, чи це взагалі число: якщо так, лишаємо як
// є. Помилитись у цей бік безпечно — `=1+1` числом не є, а
// `-2+3+cmd|'/C calc'!A0` не розбирається як число й буде екранований.
func csvSafeCell(s string) string {
if s == "" || !csvFormulaLead(s[0]) {
return s
}
if _, err := strconv.ParseFloat(s, 64); err == nil {
return s
}
return "'" + s
}
// safeCSV — тонка обгортка над encoding/csv, яка не вміє писати
// неекрановану клітинку.
//
// Обгортка бере готовий *csv.Writer, а не створює його сама: у трьох
// наших звітів різні домовленості (роздільник, CRLF), і зводити їх до
// одного набору означало б непомітно змінити формат двох із них.
type safeCSV struct{ cw *csv.Writer }
func newSafeCSV(cw *csv.Writer) *safeCSV { return &safeCSV{cw: cw} }
// Write пише рядок, екрануючи кожну клітинку.
//
// Порожній рядок (nil) проходить як є: у наших звітах він розділяє
// розділи, і Excel його показує, а імпортери пропускають.
func (s *safeCSV) Write(row []string) error {
if row == nil {
return s.cw.Write(nil)
}
out := make([]string, len(row))
for i, v := range row {
out[i] = csvSafeCell(v)
}
return s.cw.Write(out)
}
func (s *safeCSV) Flush() { s.cw.Flush() }
func (s *safeCSV) Error() error { return s.cw.Error() }