Netpulse_SasS/agent/internal/config/config.go
byrsapty ed8fc831bf Дві сесії роботи: 0058–0068, розгортання однією командою, тести
Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.

ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ

  0058  подієві алерти: syslog, ncm, compliance спрацьовують у мить
        події; правило з нереалізованим джерелом більше не зберігається
        мовчки
  0059  snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
        описуються шаблоном, а не Go
  0060  відкат конфігу: план як різниця, маскування паролів із підписом
        плану, обов'язковий контрольний збір, verifying при обриві
  0061  кнопки Telegram: довге опитування, авторизація не з callback_data
  0062  аудит і архів хостів; тест на AST, що падає на ключі без назви
  0063  RLS: три ролі, окремий пул для фонових тактів
  0064  строки зберігання даних і сторінка сховища
  0065  приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
        переклад v1→v2 за RFC 3584 дає правильний OID
  0066  ескалації сповіщень
  0067  алерт про вичерпання диска
  0068  поля заливки конфігу переїхали в каталог профілів

Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.

ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ

  netpulse            установник: одна команда замість 18 змінних і
                      593 рядків інструкції
  RLS з першого запуску  нова інсталяція під політиками одразу;
                      RLS-EXISTING-INSTALL.md лишається тільки для
                      старих інсталяцій
  .forgejo + CI       раннер не зареєстрований

Ці три перевірені компіляцією й міркуванням, але не виконанням.

ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ

Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.

Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.

Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
2026-08-27 17:32:49 +03:00

215 lines
9.1 KiB
Go
Raw Permalink 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 config — налаштування агента з прапорців і змінних оточення.
//
// Конфіг-файлу навмисно немає: у зонда рівно жменя параметрів, а
// відсутність YAML економить залежність і кілька сотень кілобайт
// бінарника. Усе інше агент отримує від сервера в Welcome і ModuleControl.
package config
import (
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/netpulse/netpulse/agent/internal/localfiles"
)
type Config struct {
// Куди підключатись: host:port.
Endpoint string
AgentID string
Hostname string
// Токен зонда, виданий при Enroll. Визначає, ЯКИЙ це зонд;
// сертифікат mTLS — що він узагалі має право говорити з сервером.
Token string
// mTLS. Порожні шляхи + Insecure = локальний стенд без шифрування.
CertFile string
KeyFile string
CAFile string
Insecure bool
MaxConcurrency int
BufferMaxItems int
BufferMaxBytes int
MinBackoff time.Duration
MaxBackoff time.Duration
LogLevel string
LogJSON bool
// Модулі, увімкнені до першої відповіді сервера.
DefaultModules []string
// Адреса приймача syslog. Порт нижче 1024 вимагає
// CAP_NET_BIND_SERVICE — тому й налаштовується: віддати зонду це
// право можна не всюди, а перенаправити 514 на 5514 правилом
// фаєрвола можна скрізь.
SyslogListen string
// Адреса приймача SNMP-трапів. Той самий випадок із
// CAP_NET_BIND_SERVICE, що й у syslog: 162 — привілейований порт.
TrapsListen string
// Одноразовий токен запрошення. Якщо заданий — зонд спершу
// реєструється, зберігає посвідчення й лише потім працює.
EnrollToken string
// Бажане ім'я зонда при реєстрації.
EnrollName string
// Де лежить посвідчення між запусками.
IdentityPath string
// Перелік конфіг-файлів, які вільно читати з ЦІЄЇ машини.
//
// Єдине налаштування зонда, яке принципово не може приходити з
// сервера. Зонд читає файли правами машини, і поле «які саме»,
// кероване з кабінету, дало б будь-кому з правом ncm:write читання
// довільного файлу на всіх серверах клієнта одразу. Тому перелік
// лежить у файлі на самій машині — там, де його редагує той, хто
// вже має на ній права, — а сервер про його вміст не знає нічого.
//
// Прапорець задає лише ШЛЯХ до переліку, і сам по собі нічого не
// дозволяє: без файлу збір локальних конфігів не робиться взагалі.
FileCfgPolicy string
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envInt(key string, def int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
// Parse читає прапорці поверх змінних оточення (NETPULSE_*).
func Parse(args []string) (*Config, error) {
host, _ := os.Hostname()
c := &Config{}
fs := flag.NewFlagSet("netpulse-agent", flag.ContinueOnError)
fs.StringVar(&c.Endpoint, "server", envOr("NETPULSE_SERVER", ""), "адреса сервера, host:port")
fs.StringVar(&c.AgentID, "agent-id", envOr("NETPULSE_AGENT_ID", ""), "ідентифікатор зонда")
fs.StringVar(&c.Hostname, "hostname", envOr("NETPULSE_HOSTNAME", host), "ім'я хоста для журналу сервера")
fs.StringVar(&c.Token, "token", envOr("NETPULSE_TOKEN", ""), "токен зонда з Enroll")
fs.StringVar(&c.CertFile, "cert", envOr("NETPULSE_CERT", ""), "клієнтський сертифікат (mTLS)")
fs.StringVar(&c.KeyFile, "key", envOr("NETPULSE_KEY", ""), "приватний ключ")
fs.StringVar(&c.CAFile, "ca", envOr("NETPULSE_CA", ""), "CA сервера")
fs.BoolVar(&c.Insecure, "insecure", os.Getenv("NETPULSE_INSECURE") == "1",
"без TLS — лише для локального стенду")
fs.IntVar(&c.MaxConcurrency, "max-concurrency", envInt("NETPULSE_MAX_CONCURRENCY", 64),
"скільки чеків виконувати паралельно")
fs.IntVar(&c.BufferMaxItems, "buffer-items", envInt("NETPULSE_BUFFER_ITEMS", 50_000),
"ліміт записів у буфері телеметрії")
fs.IntVar(&c.BufferMaxBytes, "buffer-bytes", envInt("NETPULSE_BUFFER_BYTES", 8<<20),
"оціночний ліміт пам'яті буфера в байтах")
fs.DurationVar(&c.MinBackoff, "min-backoff", time.Second, "мінімальна пауза перед реконектом")
fs.DurationVar(&c.MaxBackoff, "max-backoff", 2*time.Minute, "максимальна пауза перед реконектом")
fs.StringVar(&c.LogLevel, "log-level", envOr("NETPULSE_LOG_LEVEL", "info"), "debug|info|warn|error")
fs.BoolVar(&c.LogJSON, "log-json", os.Getenv("NETPULSE_LOG_JSON") == "1", "журнал у JSON")
mods := fs.String("modules", envOr("NETPULSE_MODULES", "icmp"),
"модулі, увімкнені до відповіді сервера (через кому)")
fs.StringVar(&c.SyslogListen, "syslog-listen", envOr("NETPULSE_SYSLOG_LISTEN", ":514"),
"адреса приймача syslog; діє лише з модулем syslog")
fs.StringVar(&c.TrapsListen, "traps-listen", envOr("NETPULSE_TRAPS_LISTEN", ":162"),
"адреса приймача SNMP-трапів; діє лише з модулем traps")
fs.StringVar(&c.EnrollToken, "enroll", envOr("NETPULSE_ENROLL", ""),
"одноразовий токен запрошення з UI")
fs.StringVar(&c.EnrollName, "name", envOr("NETPULSE_NAME", ""),
"бажане ім'я зонда при реєстрації")
fs.StringVar(&c.IdentityPath, "identity", envOr("NETPULSE_IDENTITY", DefaultIdentityPath()),
"файл посвідчення зонда")
fs.StringVar(&c.FileCfgPolicy, "files-policy",
envOr("NETPULSE_FILES_POLICY", localfiles.DefaultPolicyPath()),
"перелік конфіг-файлів цієї машини, дозволених до збору")
if err := fs.Parse(args); err != nil {
return nil, err
}
for _, m := range strings.Split(*mods, ",") {
if m = strings.TrimSpace(m); m != "" {
c.DefaultModules = append(c.DefaultModules, m)
}
}
return c, c.validate()
}
func (c *Config) validate() error {
if c.Endpoint == "" {
return errors.New("не вказано адресу сервера (-server або NETPULSE_SERVER)")
}
// Ідентичність тут не перевіряємо взагалі.
//
// До реєстрації її ще немає, після — вона приходить із файлу
// посвідчення, про який validate не знає. Перевірка живе в main,
// коли всі три джерела (прапорці, файл, реєстрація) вже враховані:
// інакше запуск із самим -identity падав із «зонд не
// зареєстрований», хоча посвідчення лежало поруч.
if !c.Insecure && (c.CertFile == "" || c.KeyFile == "") {
return errors.New("потрібні -cert і -key для mTLS (або -insecure для локального стенду)")
}
if c.MaxConcurrency <= 0 {
return errors.New("-max-concurrency має бути додатним")
}
return nil
}
// TLSConfig збирає mTLS-конфігурацію.
//
// Перевірка сертифіката сервера обов'язкова й не вимикається прапорцем:
// зонд ходить через інтернет, і «тимчасово без перевірки» — це той
// самий канал, яким їдуть паролі від усіх комутаторів клієнта.
func (c *Config) TLSConfig() (*tls.Config, error) {
if c.Insecure {
return nil, nil
}
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
if err != nil {
return nil, fmt.Errorf("клієнтський сертифікат: %w", err)
}
tc := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
}
if c.CAFile != "" {
pem, err := os.ReadFile(c.CAFile)
if err != nil {
return nil, fmt.Errorf("CA: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pem) {
return nil, errors.New("CA: не вдалося розібрати PEM")
}
tc.RootCAs = pool
}
return tc, nil
}