2

Add background IP intel and restore dashboard stats

This commit is contained in:
2026-03-12 11:10:59 +01:00
parent 14a711038b
commit 1822e2148a
9 changed files with 506 additions and 31 deletions

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
@@ -591,6 +592,51 @@ func (s *Store) GetInvestigation(ctx context.Context, ip string) (model.IPInvest
return item, true, nil
}
func (s *Store) GetInvestigationsForIPs(ctx context.Context, ips []string) (map[string]model.IPInvestigation, error) {
unique := uniqueNonEmptyStrings(ips)
if len(unique) == 0 {
return map[string]model.IPInvestigation{}, nil
}
placeholders := make([]string, len(unique))
args := make([]any, 0, len(unique))
for index, ip := range unique {
placeholders[index] = "?"
args = append(args, ip)
}
rows, err := s.db.QueryContext(ctx, fmt.Sprintf(
`SELECT ip, payload_json, updated_at FROM ip_investigations WHERE ip IN (%s)`,
strings.Join(placeholders, ", "),
), args...)
if err != nil {
return nil, fmt.Errorf("query ip investigations: %w", err)
}
defer rows.Close()
items := make(map[string]model.IPInvestigation, len(unique))
for rows.Next() {
var ip string
var payload string
var updatedAt string
if err := rows.Scan(&ip, &payload, &updatedAt); err != nil {
return nil, fmt.Errorf("scan ip investigation: %w", err)
}
var item model.IPInvestigation
if err := json.Unmarshal([]byte(payload), &item); err != nil {
return nil, fmt.Errorf("decode ip investigation %q: %w", ip, err)
}
parsed, err := parseTime(updatedAt)
if err != nil {
return nil, fmt.Errorf("parse ip investigation updated_at for %q: %w", ip, err)
}
item.UpdatedAt = parsed
items[ip] = item
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate ip investigations: %w", err)
}
return items, nil
}
func (s *Store) SaveInvestigation(ctx context.Context, item model.IPInvestigation) error {
if item.UpdatedAt.IsZero() {
item.UpdatedAt = time.Now().UTC()
@@ -616,6 +662,39 @@ func (s *Store) SaveInvestigation(ctx context.Context, item model.IPInvestigatio
return nil
}
func (s *Store) ListRecentUserAgentsForIP(ctx context.Context, ip string, limit int) ([]string, error) {
if limit <= 0 {
limit = 10
}
rows, err := s.db.QueryContext(ctx, `
SELECT user_agent
FROM events
WHERE client_ip = ? AND TRIM(user_agent) <> ''
GROUP BY user_agent
ORDER BY MAX(occurred_at) DESC, user_agent ASC
LIMIT ?`,
ip,
limit,
)
if err != nil {
return nil, fmt.Errorf("list recent user agents for ip %q: %w", ip, err)
}
defer rows.Close()
items := make([]string, 0, limit)
for rows.Next() {
var userAgent string
if err := rows.Scan(&userAgent); err != nil {
return nil, fmt.Errorf("scan recent user agent for ip %q: %w", ip, err)
}
items = append(items, userAgent)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate recent user agents for ip %q: %w", ip, err)
}
return items, nil
}
func (s *Store) GetSourceOffset(ctx context.Context, sourceName string) (model.SourceOffset, bool, error) {
row := s.db.QueryRowContext(ctx, `SELECT source_name, path, inode, offset, updated_at FROM source_offsets WHERE source_name = ?`, sourceName)
var offset model.SourceOffset
@@ -1086,6 +1165,24 @@ func boolToInt(value bool) int {
return 0
}
func uniqueNonEmptyStrings(items []string) []string {
seen := make(map[string]struct{}, len(items))
result := make([]string, 0, len(items))
for _, item := range items {
trimmed := strings.TrimSpace(item)
if trimmed == "" {
continue
}
if _, ok := seen[trimmed]; ok {
continue
}
seen[trimmed] = struct{}{}
result = append(result, trimmed)
}
sort.Strings(result)
return result
}
type queryer interface {
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

View File

@@ -139,4 +139,18 @@ func TestStoreRecordsEventsAndState(t *testing.T) {
if !found || loadedInvestigation.Bot == nil || loadedInvestigation.Bot.Name != "Googlebot" {
t.Fatalf("unexpected investigation payload: found=%v investigation=%+v", found, loadedInvestigation)
}
investigations, err := db.GetInvestigationsForIPs(ctx, []string{event.ClientIP, "198.51.100.99"})
if err != nil {
t.Fatalf("get investigations for ips: %v", err)
}
if len(investigations) != 1 || investigations[event.ClientIP].Bot == nil {
t.Fatalf("unexpected investigations map: %+v", investigations)
}
userAgents, err := db.ListRecentUserAgentsForIP(ctx, event.ClientIP, 10)
if err != nil {
t.Fatalf("list recent user agents: %v", err)
}
if len(userAgents) != 1 || userAgents[0] != event.UserAgent {
t.Fatalf("unexpected user agents: %#v", userAgents)
}
}