This commit is contained in:
Magnus Root 2026-08-26 12:58:09 +03:00
parent 1f2d554d6a
commit 8707068ecd
4 changed files with 572 additions and 3 deletions

View file

@ -49,6 +49,7 @@ auspex [флаг] [аргументы]
├── -fd → runFDCheck() ├── -fd → runFDCheck()
├── -dt → runDockerTUI() ├── -dt → runDockerTUI()
├── -fw → runFirewallTUI() ├── -fw → runFirewallTUI()
├── -au → runAudit(category)
├── -v → версия (+ вариант сборки: full/generic) ├── -v → версия (+ вариант сборки: full/generic)
└── -h → usage └── -h → usage
``` ```
@ -389,6 +390,32 @@ Truncate — единственное действие, которое осво
--- ---
### `audit.go` — комплексный аудит-скан (read-only)
**Точка входа:** `runAudit(category string)`
Не проверяет "что-то одно", а прогоняет 7 категорий и печатает единый отчёт, отсортированный по severity (critical → warning → info). Полностью read-only — ничего не чинит и не меняет, только собирает находки. Задуман как первый шаг при поиске бага: что на тренировочном стенде (не связанном с WAF), что на реальном сервере.
**Категории:** `systemd`, `nginx` (детектит и nginx, и angie), `docker`, `cert`, `disk`, `network`, `firewall` — можно запустить одну через `auspex -au <категория>` (есть алиасы: `svc`→systemd, `angie`/`proxy`→nginx, `tls`/`ssl`→cert, `logs`→disk, `dns`→network, `iptables`/`ufw`→firewall).
**Переиспользует существующие модули напрямую** (не дублирует парсинг):
- `ccFindCerts`/`ccParseCert` (certcheck.go) — сроки сертификатов
- `dcCheckDockerAvailable`/`dcGetContainers` (dockercheck.go) — статус контейнеров
- `fdScan`/`fdHumanSize` (fdcheck.go) — удалённые-но-открытые файлы как признак утечки места
- `fwDetectBackend`/`ListRules` (firewall.go) — список firewall-правил
**Кросс-проверки между категориями — самая ценная часть:**
| Проверка | Как | Что выявляет |
|---|---|---|
| nginx `listen` ↔ реальные сокеты | парсит `nginx/angie -T`, сверяет с `ss -tlnp`/`-ulnp` (`auListeningPorts`) | конфиг применён, но процесс не слушает порт (не перечитал конфиг / упал) |
| nginx upstream ↔ доступность | парсит `server host:port;` из `-T`, TCP dial с таймаутом 2 сек | бэкенд не отвечает или неверный адрес в upstream |
| сертификат на диске ↔ то, что реально отдаёт TLS | `ccParseCert` против `tls.Dial` на `:443` | сервис не перечитал сертификат после обновления (самый частый "необъяснимый" баг с TLS) |
| firewall-правила ↔ слушающие порты | пересечение `ListRules()` и `auListeningPorts()` в обе стороны | правило разрешает порт, где никто не слушает (устарело) — и наоборот, порт слушает без явного allow (может блокироваться default policy) |
Кросс-проверки по firewall и network — эвристические (не учитывают, например, ACCEPT default policy), поэтому такие находки идут с severity `warning`, а не `critical`, и в конце отчёта есть явная оговорка об этом.
---
### `webcheck.go` — проверка доступности сайтов ### `webcheck.go` — проверка доступности сайтов
**Точка входа:** `runWebCheck(sidArg string)` **Точка входа:** `runWebCheck(sidArg string)`
@ -573,6 +600,7 @@ primary (cfgPrimaryDBHost) → ping OK? → использовать
| dockerview | `dv` | | dockerview | `dv` |
| fdcheck | `fd` | | fdcheck | `fd` |
| firewall | `fw` | | firewall | `fw` |
| audit | `au` |
| email | `email` | | email | `email` |
| mattermost | `mm` | | mattermost | `mm` |
| webcheck | `wc` | | webcheck | `wc` |

View file

@ -17,6 +17,7 @@
| `-fd` | Поиск удалённых-но-открытых файловых дескрипторов (recover/reopen/truncate/restart/kill) | | `-fd` | Поиск удалённых-но-открытых файловых дескрипторов (recover/reopen/truncate/restart/kill) |
| `-dt` | Docker Live — интерактивный TUI (аналог lazydocker): логи, шелл, stop/start/restart/remove | | `-dt` | Docker Live — интерактивный TUI (аналог lazydocker): логи, шелл, stop/start/restart/remove |
| `-fw` | Firewall — TUI для ufw/nftables/iptables: просмотр правил + формы (allow/deny, forward, rate-limit) с dry-run и автооткатом | | `-fw` | Firewall — TUI для ufw/nftables/iptables: просмотр правил + формы (allow/deny, forward, rate-limit) с dry-run и автооткатом |
| `-au` | Audit — комплексная read-only диагностика (systemd/nginx/docker/cert/disk/network/firewall) с кросс-проверками между категориями; `auspex -au <категория>` для одной секции |
Модули **[WAF]** требуют БД `waf_info` и/или структуру каталогов PT AF и доступны Модули **[WAF]** требуют БД `waf_info` и/или структуру каталогов PT AF и доступны
только в сборке `full` (см. [Сборка](#сборка)). только в сборке `full` (см. [Сборка](#сборка)).
@ -36,6 +37,7 @@ auspex/
├── dockerview.go # -dt: Docker Live TUI (аналог lazydocker) ├── dockerview.go # -dt: Docker Live TUI (аналог lazydocker)
├── firewall.go # -fw: бэкенды ufw/nftables/iptables (detect/list/apply/undo) ├── firewall.go # -fw: бэкенды ufw/nftables/iptables (detect/list/apply/undo)
├── firewall_tui.go # -fw: TUI-слой (формы, review, rollback-таймер) ├── firewall_tui.go # -fw: TUI-слой (формы, review, rollback-таймер)
├── audit.go # -au: комплексный аудит-скан (read-only)
├── portsview.go # -ss: TUI просмотр занятых портов ├── portsview.go # -ss: TUI просмотр занятых портов
├── fdcheck.go # -fd: удалённые-но-открытые файловые дескрипторы ├── fdcheck.go # -fd: удалённые-но-открытые файловые дескрипторы
├── portcheck.go # -p: проверка доступности портов [WAF, build tag] ├── portcheck.go # -p: проверка доступности портов [WAF, build tag]
@ -93,6 +95,8 @@ sudo cp auspex /usr/local/bin/auspex
./auspex -fd # Поиск удалённых-но-открытых файловых дескрипторов ./auspex -fd # Поиск удалённых-но-открытых файловых дескрипторов
./auspex -dt # Docker Live TUI (аналог lazydocker) ./auspex -dt # Docker Live TUI (аналог lazydocker)
./auspex -fw # Firewall TUI (ufw/nftables/iptables) ./auspex -fw # Firewall TUI (ufw/nftables/iptables)
./auspex -au # Полный аудит-скан
./auspex -au nginx # Аудит только одной категории
./auspex -h # Справка ./auspex -h # Справка
./auspex -v # Версия ./auspex -v # Версия
``` ```

518
audit.go Normal file
View file

@ -0,0 +1,518 @@
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
)
// =============================================================================
// audit — комплексная диагностика сервера по категориям
// =============================================================================
//
// В отличие от остальных модулей, это не "проверка одной вещи", а сквозной
// скан: systemd, nginx/Angie, Docker, сертификаты, диск/логи, сеть/DNS,
// firewall — плюс КРОСС-ПРОВЕРКИ между категориями (слушающие порты против
// firewall-правил, конфиг nginx против реальных сокетов, сертификат на диске
// против того, что реально отдаётся по TLS). Такие несостыковки — самый частый
// источник "необъяснимых" багов что на тренировочных стендах, что в проде.
//
// ПОЛНОСТЬЮ READ-ONLY: модуль ничего не меняет и не чинит — только собирает
// находки и печатает отчёт с рекомендациями, что проверить дальше руками.
// =============================================================================
type auFinding struct {
Category string
Severity string // "critical" | "warning" | "info"
Message string
}
type auSocket struct {
Proto string
Port string
}
var auCategories = []string{"systemd", "nginx", "docker", "cert", "disk", "network", "firewall"}
var auAliases = map[string]string{
"svc": "systemd", "service": "systemd", "services": "systemd", "systemctl": "systemd",
"angie": "nginx", "proxy": "nginx", "reverse-proxy": "nginx",
"container": "docker", "containers": "docker",
"certs": "cert", "tls": "cert", "ssl": "cert", "certificate": "cert", "certificates": "cert",
"logs": "disk", "log": "disk", "space": "disk",
"dns": "network", "net": "network",
"fw": "firewall", "iptables": "firewall", "nftables": "firewall", "ufw": "firewall",
}
func runAudit(filter string) error {
runners := map[string]func() []auFinding{
"systemd": auCheckSystemd,
"nginx": auCheckNginx,
"docker": auCheckDocker,
"cert": auCheckCerts,
"disk": auCheckDiskAndLogs,
"network": auCheckNetwork,
"firewall": auCheckFirewall,
}
order := auCategories
if filter != "" {
f := strings.ToLower(strings.TrimSpace(filter))
if canon, ok := auAliases[f]; ok {
f = canon
}
if _, ok := runners[f]; !ok {
return fmt.Errorf("неизвестная категория %q — доступны: %s", filter, strings.Join(auCategories, ", "))
}
order = []string{f}
}
fmt.Printf("%s🔍 Auspex Audit — комплексная диагностика%s\n\n", cCyan, cReset)
var all []auFinding
for _, name := range order {
fmt.Printf("%s▶ %s...%s\n", cBlue, name, cReset)
all = append(all, runners[name]()...)
}
auPrintReport(all)
return nil
}
func auPrintReport(all []auFinding) {
var critical, warning, info []auFinding
for _, f := range all {
switch f.Severity {
case "critical":
critical = append(critical, f)
case "warning":
warning = append(warning, f)
default:
info = append(info, f)
}
}
fmt.Println()
if len(critical) > 0 {
fmt.Printf("%s🎯 Наиболее вероятные причины проблемы (%d):%s\n", cRed, len(critical), cReset)
for _, f := range critical {
fmt.Printf(" 🔴 [%s] %s\n", f.Category, f.Message)
}
fmt.Println()
} else {
fmt.Printf("%sКритичных находок не обнаружено.%s\n\n", cGreen, cReset)
}
if len(warning) > 0 {
fmt.Printf("%s⚠ Требует внимания (%d):%s\n", cYellow, len(warning), cReset)
for _, f := range warning {
fmt.Printf(" 🟡 [%s] %s\n", f.Category, f.Message)
}
fmt.Println()
}
if len(info) > 0 {
fmt.Printf("%sСправочно (%d):%s\n", cCyan, len(info), cReset)
for _, f := range info {
fmt.Printf(" [%s] %s\n", f.Category, f.Message)
}
fmt.Println()
}
fmt.Printf("Итого: %d критично, %d предупреждений, %d информационных\n", len(critical), len(warning), len(info))
fmt.Println()
fmt.Printf("%sНапоминание: это подсказки, а не диагноз. Часть находок (например\n"+
"'порт слушает, но нет явного allow-правила') может быть ложной — не забывайте\n"+
"про default policy firewall'а и специфику конкретного стенда.%s\n", cYellow, cReset)
}
// ── auListeningPorts — общий хелпер для nginx- и firewall-кросс-проверок ─────
func auListeningPorts() ([]auSocket, error) {
var socks []auSocket
for _, flag := range []string{"-tlnp", "-ulnp"} {
proto := "tcp"
if flag == "-ulnp" {
proto = "udp"
}
out, err := exec.Command("ss", flag).Output()
if err != nil {
continue
}
for i, line := range strings.Split(string(out), "\n") {
if i == 0 || strings.TrimSpace(line) == "" {
continue
}
fields := strings.Fields(line)
port := ""
for _, f := range fields {
idx := strings.LastIndex(f, ":")
if idx == -1 || idx == len(f)-1 {
continue
}
if p, err := strconv.Atoi(f[idx+1:]); err == nil {
port = strconv.Itoa(p)
break
}
}
if port != "" {
socks = append(socks, auSocket{Proto: proto, Port: port})
}
}
}
return socks, nil
}
// ── systemd ──────────────────────────────────────────────────────────────────
func auCheckSystemd() []auFinding {
if _, err := exec.LookPath("systemctl"); err != nil {
return []auFinding{{"systemd", "info", "systemctl не найден — пропущено (не systemd-система?)"}}
}
raw, err := exec.Command("systemctl", "list-units", "--type=service", "--all", "--no-legend", "--plain").Output()
if err != nil {
return []auFinding{{"systemd", "warning", fmt.Sprintf("не удалось получить список служб: %v", err)}}
}
var out []auFinding
failed := 0
for _, line := range strings.Split(string(raw), "\n") {
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
unit, active, sub := fields[0], fields[2], fields[3]
if active == "failed" {
failed++
out = append(out, auFinding{"systemd", "critical",
fmt.Sprintf("служба %s в состоянии failed (%s) — journalctl -u %s -n 50", unit, sub, unit)})
}
}
if failed == 0 {
out = append(out, auFinding{"systemd", "info", "служб в состоянии failed не найдено"})
}
return out
}
// ── nginx / Angie ────────────────────────────────────────────────────────────
func auCheckNginx() []auFinding {
var out []auFinding
found := false
for _, bin := range []string{"nginx", "angie"} {
if _, err := exec.LookPath(bin); err != nil {
continue
}
found = true
testOut, testErr := exec.Command(bin, "-t").CombinedOutput()
if testErr != nil {
out = append(out, auFinding{"nginx", "critical",
fmt.Sprintf("'%s -t' провалилась: %s", bin, strings.TrimSpace(string(testOut)))})
} else {
out = append(out, auFinding{"nginx", "info", fmt.Sprintf("'%s -t': синтаксис конфигурации в порядке", bin)})
}
activeOut, _ := exec.Command("systemctl", "is-active", bin).Output()
if strings.TrimSpace(string(activeOut)) != "active" {
out = append(out, auFinding{"nginx", "critical",
fmt.Sprintf("%s не активен (systemctl is-active: %s)", bin, strings.TrimSpace(string(activeOut)))})
}
if dump, err := exec.Command(bin, "-T").Output(); err == nil {
out = append(out, auCheckNginxListenPorts(bin, string(dump))...)
out = append(out, auCheckNginxUpstreams(string(dump))...)
}
}
if !found {
out = append(out, auFinding{"nginx", "info", "nginx/angie не найдены на этом хосте — пропущено"})
}
return out
}
var auNginxListenRe = regexp.MustCompile(`listen\s+(?:\S+:)?(\d+)`)
func auCheckNginxListenPorts(bin, dump string) []auFinding {
var out []auFinding
ports := map[string]bool{}
for _, m := range auNginxListenRe.FindAllStringSubmatch(dump, -1) {
ports[m[1]] = true
}
listening, _ := auListeningPorts()
listenSet := map[string]bool{}
for _, s := range listening {
listenSet[s.Port] = true
}
for port := range ports {
if !listenSet[port] {
out = append(out, auFinding{"nginx", "critical", fmt.Sprintf(
"%s настроен слушать порт %s, но по факту никто на нём не слушает — процесс не применил конфиг или упал после reload",
bin, port)})
}
}
return out
}
var auNginxUpstreamRe = regexp.MustCompile(`server\s+([\w.\-]+:\d+)\s*;`)
func auCheckNginxUpstreams(dump string) []auFinding {
var out []auFinding
seen := map[string]bool{}
for _, m := range auNginxUpstreamRe.FindAllStringSubmatch(dump, -1) {
addr := m[1]
if seen[addr] {
continue
}
seen[addr] = true
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
out = append(out, auFinding{"nginx", "critical",
fmt.Sprintf("upstream %s недоступен (%v) — бэкенд не отвечает или указан неверный адрес", addr, err)})
} else {
conn.Close()
}
}
return out
}
// ── Docker ───────────────────────────────────────────────────────────────────
func auCheckDocker() []auFinding {
if err := dcCheckDockerAvailable(); err != nil {
return []auFinding{{"docker", "info", fmt.Sprintf("Docker недоступен — пропущено (%v)", err)}}
}
containers, err := dcGetContainers()
if err != nil {
return []auFinding{{"docker", "warning", fmt.Sprintf("не удалось получить список контейнеров: %v", err)}}
}
if len(containers) == 0 {
return []auFinding{{"docker", "info", "контейнеров не найдено"}}
}
var out []auFinding
for _, c := range containers {
switch {
case c.Status == "running" && c.Health == "unhealthy":
out = append(out, auFinding{"docker", "critical",
fmt.Sprintf("контейнер %s запущен, но healthcheck сообщает unhealthy — docker logs %s", c.Name, c.Name)})
case c.Status == "restarting":
out = append(out, auFinding{"docker", "critical",
fmt.Sprintf("контейнер %s в состоянии restarting (похоже на crash-loop) — docker logs %s", c.Name, c.Name)})
case c.Status == "exited" || c.Status == "dead":
out = append(out, auFinding{"docker", "critical",
fmt.Sprintf("контейнер %s остановлен (%s) — docker logs %s", c.Name, c.Status, c.Name)})
}
}
if len(out) == 0 {
out = append(out, auFinding{"docker", "info", fmt.Sprintf("все %d контейнер(ов) в порядке", len(containers))})
}
return out
}
// ── Сертификаты / TLS ────────────────────────────────────────────────────────
func auCheckCerts() []auFinding {
paths, err := ccFindCerts(cfgSSLDir)
if err != nil {
return []auFinding{{"cert", "warning", fmt.Sprintf("не удалось просканировать %s: %v", cfgSSLDir, err)}}
}
if len(paths) == 0 {
return []auFinding{{"cert", "info", fmt.Sprintf("сертификаты в %s не найдены", cfgSSLDir)}}
}
var out []auFinding
var newest ccCertInfo
haveNewest := false
for _, p := range paths {
info, err := ccParseCert(p)
if err != nil {
continue
}
switch {
case info.DaysLeft < 0:
out = append(out, auFinding{"cert", "critical",
fmt.Sprintf("%s (CN=%s) истёк %d дн. назад", info.Path, info.CommonName, -info.DaysLeft)})
case info.DaysLeft <= cfgCertCriticalDays:
out = append(out, auFinding{"cert", "critical",
fmt.Sprintf("%s (CN=%s) истекает через %d дн.", info.Path, info.CommonName, info.DaysLeft)})
case info.DaysLeft <= cfgCertWarningDays:
out = append(out, auFinding{"cert", "warning",
fmt.Sprintf("%s (CN=%s) истекает через %d дн.", info.Path, info.CommonName, info.DaysLeft)})
}
if !haveNewest || info.NotAfter.After(newest.NotAfter) {
newest = info
haveNewest = true
}
}
if haveNewest {
if served, err := auFetchServedCert("127.0.0.1:443", 2*time.Second); err == nil {
if served.NotAfter.Before(newest.NotAfter.Add(-24 * time.Hour)) {
out = append(out, auFinding{"cert", "critical", fmt.Sprintf(
"на диске есть сертификат новее (истекает %s), чем реально отдаётся по TLS на :443 (истекает %s) — похоже, сервис не перечитал сертификат после обновления/выпуска",
newest.NotAfter.Format("2006-01-02"), served.NotAfter.Format("2006-01-02"))})
}
}
}
if len(out) == 0 {
out = append(out, auFinding{"cert", "info", fmt.Sprintf("проверено %d сертификат(ов), проблем не найдено", len(paths))})
}
return out
}
func auFetchServedCert(addr string, timeout time.Duration) (*x509.Certificate, error) {
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", addr, &tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, err
}
defer conn.Close()
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
return nil, fmt.Errorf("сертификат не получен в TLS-хендшейке")
}
return certs[0], nil
}
// ── Диск и логи ──────────────────────────────────────────────────────────────
func auCheckDiskAndLogs() []auFinding {
var out []auFinding
if dfOut, err := exec.Command("df", "-P").Output(); err == nil {
for i, line := range strings.Split(string(dfOut), "\n") {
if i == 0 {
continue
}
fields := strings.Fields(line)
if len(fields) < 6 {
continue
}
fs, usePct, mount := fields[0], fields[4], fields[5]
if strings.Contains(fs, "tmpfs") || strings.Contains(fs, "overlay") || strings.Contains(fs, "squashfs") {
continue
}
n, err := strconv.Atoi(strings.TrimSuffix(usePct, "%"))
if err != nil {
continue
}
switch {
case n >= 95:
out = append(out, auFinding{"disk", "critical", fmt.Sprintf("%s заполнен на %d%% — критично мало места", mount, n)})
case n >= 85:
out = append(out, auFinding{"disk", "warning", fmt.Sprintf("%s заполнен на %d%%", mount, n)})
}
}
}
entries := fdScan()
var leaked int64
for _, e := range entries {
leaked += e.SizeBytes
}
switch {
case leaked > 500*1024*1024:
out = append(out, auFinding{"disk", "critical",
fmt.Sprintf("удалённые-но-открытые файлы держат %s места (см. -fd для деталей)", fdHumanSize(leaked))})
case leaked > 50*1024*1024:
out = append(out, auFinding{"disk", "warning",
fmt.Sprintf("удалённые-но-открытые файлы держат %s (см. -fd)", fdHumanSize(leaked))})
}
if len(out) == 0 {
out = append(out, auFinding{"disk", "info", "диск и файловые дескрипторы в порядке"})
}
return out
}
// ── Сеть / DNS ───────────────────────────────────────────────────────────────
func auCheckNetwork() []auFinding {
var out []auFinding
data, err := os.ReadFile("/etc/resolv.conf")
if err != nil || !strings.Contains(string(data), "nameserver") {
out = append(out, auFinding{"network", "warning", "в /etc/resolv.conf не найдено ни одного nameserver — DNS может не работать"})
} else {
out = append(out, auFinding{"network", "info", "nameserver в /etc/resolv.conf присутствует"})
}
if routeOut, err := exec.Command("ip", "route", "show", "default").Output(); err == nil {
if strings.TrimSpace(string(routeOut)) == "" {
out = append(out, auFinding{"network", "warning",
"default route не найден — если сервер должен иметь исходящий доступ, это проблема (в изолированных стендах может быть ожидаемо)"})
} else {
out = append(out, auFinding{"network", "info", "default route настроен"})
}
}
if _, err := net.LookupHost("localhost"); err != nil {
out = append(out, auFinding{"network", "warning",
fmt.Sprintf("не удалось разрешить даже 'localhost': %v — похоже, сломан резолвинг на уровне NSS/glibc", err)})
}
return out
}
// ── Firewall ─────────────────────────────────────────────────────────────────
func auCheckFirewall() []auFinding {
backend, err := fwDetectBackend()
if err != nil {
return []auFinding{{"firewall", "warning", fmt.Sprintf("не удалось определить firewall backend: %v", err)}}
}
rules, err := backend.ListRules()
if err != nil {
return []auFinding{{"firewall", "warning", fmt.Sprintf("не удалось получить правила (%s): %v", backend.Name(), err)}}
}
out := []auFinding{{"firewall", "info", fmt.Sprintf("backend: %s, правил в списке: %d", backend.Name(), len(rules))}}
listening, _ := auListeningPorts()
listenSet := map[string]bool{}
for _, s := range listening {
listenSet[s.Port] = true
}
allowed := map[string]bool{}
for _, r := range rules {
if r.Port == "" {
continue
}
action := strings.ToUpper(r.Action)
if action == "ACCEPT" || action == "ALLOW" {
allowed[r.Port] = true
}
}
for port := range allowed {
if !listenSet[port] {
out = append(out, auFinding{"firewall", "warning", fmt.Sprintf(
"правило разрешает порт %s, но на нём никто не слушает — либо сервис упал, либо правило устарело", port)})
}
}
for port := range listenSet {
if port == "22" {
continue // почти всегда разрешён отдельно/default policy — шумный false-positive
}
if !allowed[port] {
out = append(out, auFinding{"firewall", "warning", fmt.Sprintf(
"порт %s слушает, но явного allow-правила для него не найдено — проверьте доступность снаружи (может маскироваться default policy)", port)})
}
}
return out
}

21
main.go
View file

@ -7,7 +7,7 @@ import (
"strings" "strings"
) )
const appVersion = "1.4.0" const appVersion = "1.5.0"
// Цвета терминала (общие для всех модулей) // Цвета терминала (общие для всех модулей)
const ( const (
@ -123,6 +123,16 @@ func runByFlag(flag string, extraArgs []string) {
os.Exit(1) os.Exit(1)
} }
case "-au":
category := ""
if len(extraArgs) > 0 {
category = extraArgs[0]
}
if err := runAudit(category); err != nil {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
os.Exit(1)
}
case "-v", "--version": case "-v", "--version":
fmt.Printf("auspex v%s (%s)\n", appVersion, buildVariant) fmt.Printf("auspex v%s (%s)\n", appVersion, buildVariant)
@ -197,6 +207,13 @@ func showMenu() {
if err := runFirewallTUI(); err != nil { if err := runFirewallTUI(); err != nil {
fmt.Fprintf(os.Stderr, "%sОшибка: %v%s\n", cRed, err, cReset) fmt.Fprintf(os.Stderr, "%sОшибка: %v%s\n", cRed, err, cReset)
} }
case "12", "-au", "au":
fmt.Printf(" Категория (Enter = все: systemd/nginx/docker/cert/disk/network/firewall): ")
catInput, _ := reader.ReadString('\n')
catInput = strings.TrimSpace(catInput)
if err := runAudit(catInput); err != nil {
fmt.Fprintf(os.Stderr, "%sОшибка: %v%s\n", cRed, err, cReset)
}
case "0", "q", "quit", "exit": case "0", "q", "quit", "exit":
fmt.Println("Выход.") fmt.Println("Выход.")
return return
@ -224,6 +241,7 @@ func printMenuOptions() {
fmt.Printf(" %s9%s) %s-fd%s Удалённые-но-открытые файловые дескрипторы\n", cGreen, cReset, cCyan, cReset) fmt.Printf(" %s9%s) %s-fd%s Удалённые-но-открытые файловые дескрипторы\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s10%s) %s-dt%s Docker Live — TUI (аналог lazydocker)\n", cGreen, cReset, cCyan, cReset) fmt.Printf(" %s10%s) %s-dt%s Docker Live — TUI (аналог lazydocker)\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s11%s) %s-fw%s Firewall — TUI (ufw/nftables/iptables)\n", cGreen, cReset, cCyan, cReset) fmt.Printf(" %s11%s) %s-fw%s Firewall — TUI (ufw/nftables/iptables)\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s12%s) %s-au%s Audit — комплексная диагностика [категория]\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s0%s) Выход\n\n", cGreen, cReset) fmt.Printf(" %s0%s) Выход\n\n", cGreen, cReset)
fmt.Printf("%sДля быстрого вызова: auspex <ключ>%s\n", cYellow, cReset) fmt.Printf("%sДля быстрого вызова: auspex <ключ>%s\n", cYellow, cReset)
fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv | auspex -dc | auspex -w <sid>%s\n\n", cYellow, cReset) fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv | auspex -dc | auspex -w <sid>%s\n\n", cYellow, cReset)
@ -260,6 +278,7 @@ func printUsage() {
fmt.Printf(" %-12s Удалённые-но-открытые файловые дескрипторы (recover/reopen/restart/kill)\n", "-fd") fmt.Printf(" %-12s Удалённые-но-открытые файловые дескрипторы (recover/reopen/restart/kill)\n", "-fd")
fmt.Printf(" %-12s Docker Live — интерактивный TUI (аналог lazydocker)\n", "-dt") fmt.Printf(" %-12s Docker Live — интерактивный TUI (аналог lazydocker)\n", "-dt")
fmt.Printf(" %-12s Firewall — интерактивный TUI (ufw/nftables/iptables, dry-run 'D')\n", "-fw") fmt.Printf(" %-12s Firewall — интерактивный TUI (ufw/nftables/iptables, dry-run 'D')\n", "-fw")
fmt.Printf(" %-12s Комплексный аудит-скан по категориям [категория] (read-only)\n", "-au")
fmt.Printf(" %-12s Показать версию\n", "-v") fmt.Printf(" %-12s Показать версию\n", "-v")
fmt.Printf(" %-12s Показать эту справку\n\n", "-h") fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
fmt.Printf("Без аргументов запускается интерактивное меню.\n") fmt.Printf("Без аргументов запускается интерактивное меню.\n")