Перший справжній прогін на живій мережі дав 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. Доказ перевірки зразка лишався на екрані після правки зразка — тобто
ручка робила протилежне до задуманого в мить найвищої довіри.
97 lines
5.3 KiB
Go
97 lines
5.3 KiB
Go
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() }
|