126 lines
4.3 KiB
Go
126 lines
4.3 KiB
Go
package analytics
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"net/netip"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"cloudipvalidator/internal/db"
|
||
|
|
)
|
||
|
|
|
||
|
|
// List kinds served by Analysis.List.
|
||
|
|
const (
|
||
|
|
ListEgressHTTPSAny = "egress_https_any"
|
||
|
|
ListEgressHTTPSAll = "egress_https_all"
|
||
|
|
ListIngressSSHAny = "ingress_ssh_any"
|
||
|
|
ListIngressSSHAll = "ingress_ssh_all"
|
||
|
|
ListError = "error"
|
||
|
|
)
|
||
|
|
|
||
|
|
// List is a table of addresses behind one indicator or one error class.
|
||
|
|
type List struct {
|
||
|
|
Kind string `json:"kind"`
|
||
|
|
Class string `json:"class,omitempty"`
|
||
|
|
Columns []string `json:"columns"`
|
||
|
|
Rows [][]string `json:"rows"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// ErrUnknownList is returned for a list kind that does not exist.
|
||
|
|
type ErrUnknownList string
|
||
|
|
|
||
|
|
func (e ErrUnknownList) Error() string { return fmt.Sprintf("unknown list %q", string(e)) }
|
||
|
|
|
||
|
|
// List builds the table for a kind; class is only used with ListError.
|
||
|
|
func (an *Analysis) List(kind, class string) (*List, error) {
|
||
|
|
l := &List{Kind: kind, Class: class, Rows: [][]string{}}
|
||
|
|
switch kind {
|
||
|
|
case ListEgressHTTPSAny, ListEgressHTTPSAll:
|
||
|
|
l.Columns = []string{"Адрес", "Подсеть", "Валидатор", "https-проверок", "Проваленные цели"}
|
||
|
|
if kind == ListEgressHTTPSAny {
|
||
|
|
l.Columns[3] = "Провалено https"
|
||
|
|
}
|
||
|
|
for _, a := range an.sorted() {
|
||
|
|
if a.https.n == 0 || a.https.ok == a.https.n || (kind == ListEgressHTTPSAll && a.https.ok != 0) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
targets := append([]string(nil), a.https.failedTargets...)
|
||
|
|
sort.Strings(targets)
|
||
|
|
count := fmt.Sprint(a.https.n)
|
||
|
|
if kind == ListEgressHTTPSAny {
|
||
|
|
count = fmt.Sprintf("%d из %d", a.https.n-a.https.ok, a.https.n)
|
||
|
|
}
|
||
|
|
l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, ShortValidator(a.https.validator), count, strings.Join(targets, ", ")})
|
||
|
|
}
|
||
|
|
case ListIngressSSHAny, ListIngressSSHAll:
|
||
|
|
l.Columns = []string{"Адрес", "Подсеть", "Провалено ssh", "Площадки с провалом", "Ошибка"}
|
||
|
|
if kind == ListIngressSSHAll {
|
||
|
|
l.Columns = []string{"Адрес", "Подсеть", "Площадки с провалом ssh", "Ошибка"}
|
||
|
|
}
|
||
|
|
for _, a := range an.sorted() {
|
||
|
|
if a.ssh.n == 0 || a.ssh.ok == a.ssh.n || (kind == ListIngressSSHAll && a.ssh.ok != 0) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
sites := an.sortSites(a.ssh.sites)
|
||
|
|
errs := make([]string, 0, len(a.ssh.errs))
|
||
|
|
for e := range a.ssh.errs {
|
||
|
|
errs = append(errs, e)
|
||
|
|
}
|
||
|
|
sort.Strings(errs)
|
||
|
|
if kind == ListIngressSSHAny {
|
||
|
|
l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, fmt.Sprintf("%d из %d", len(a.ssh.sites), a.ssh.n), strings.Join(sites, ", "), strings.Join(errs, ", ")})
|
||
|
|
} else {
|
||
|
|
l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, strings.Join(sites, ", "), strings.Join(errs, ", ")})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
case ListError:
|
||
|
|
if class == "" {
|
||
|
|
return nil, ErrUnknownList("error without class")
|
||
|
|
}
|
||
|
|
l.Columns = []string{"Адрес", "Подсеть", "Площадка", "Валидатор", "Вердикт адреса", "Статус проверки"}
|
||
|
|
for _, a := range an.sorted() {
|
||
|
|
fs := append([]failedIngress(nil), a.failedIngress...)
|
||
|
|
sort.SliceStable(fs, func(i, j int) bool { return an.siteIndex(fs[i].site) < an.siteIndex(fs[j].site) })
|
||
|
|
for _, f := range fs {
|
||
|
|
if f.class != class {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
status := "провал, в вердикте"
|
||
|
|
if f.late {
|
||
|
|
status = "провал, после вердикта"
|
||
|
|
}
|
||
|
|
l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, f.site, ShortValidator(f.validator), a.res.Verdict, status})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
return nil, ErrUnknownList(kind)
|
||
|
|
}
|
||
|
|
return l, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// sorted returns the non-cancelled addresses in numeric address order.
|
||
|
|
func (an *Analysis) sorted() []*addr {
|
||
|
|
out := make([]*addr, 0, len(an.addrs))
|
||
|
|
for _, a := range an.addrs {
|
||
|
|
if a.res.Verdict != db.ResultCancelled {
|
||
|
|
out = append(out, a)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sort.Slice(out, func(i, j int) bool {
|
||
|
|
x, errX := netip.ParseAddr(out[i].res.IPAddress)
|
||
|
|
y, errY := netip.ParseAddr(out[j].res.IPAddress)
|
||
|
|
if errX != nil || errY != nil {
|
||
|
|
return out[i].res.IPAddress < out[j].res.IPAddress
|
||
|
|
}
|
||
|
|
return x.Less(y)
|
||
|
|
})
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func (an *Analysis) siteIndex(site string) int { return siteIndexOf(an.siteNames, site) }
|
||
|
|
|
||
|
|
func (an *Analysis) sortSites(sites []string) []string {
|
||
|
|
out := append([]string(nil), sites...)
|
||
|
|
sort.SliceStable(out, func(i, j int) bool { return an.siteIndex(out[i]) < an.siteIndex(out[j]) })
|
||
|
|
return out
|
||
|
|
}
|