2

Add on-demand IP investigation and richer IP details

This commit is contained in:
2026-03-12 01:53:44 +01:00
parent 33dd9bac76
commit c5e1c4ff36
13 changed files with 1561 additions and 144 deletions

View File

@@ -23,23 +23,29 @@ import (
)
type Service struct {
cfg *config.Config
store *store.Store
evaluator *engine.Evaluator
blocker opnsense.AliasClient
logger *log.Logger
cfg *config.Config
store *store.Store
evaluator *engine.Evaluator
blocker opnsense.AliasClient
investigator Investigator
logger *log.Logger
}
func New(cfg *config.Config, db *store.Store, blocker opnsense.AliasClient, logger *log.Logger) *Service {
type Investigator interface {
Investigate(ctx context.Context, ip string, userAgents []string) (model.IPInvestigation, error)
}
func New(cfg *config.Config, db *store.Store, blocker opnsense.AliasClient, investigator Investigator, logger *log.Logger) *Service {
if logger == nil {
logger = log.New(io.Discard, "", 0)
}
return &Service{
cfg: cfg,
store: db,
evaluator: engine.NewEvaluator(),
blocker: blocker,
logger: logger,
cfg: cfg,
store: db,
evaluator: engine.NewEvaluator(),
blocker: blocker,
investigator: investigator,
logger: logger,
}
}
@@ -75,7 +81,40 @@ func (s *Service) GetIPDetails(ctx context.Context, ip string) (model.IPDetails,
if err != nil {
return model.IPDetails{}, err
}
return s.store.GetIPDetails(ctx, normalized, 100, 100, 100)
details, err := s.store.GetIPDetails(ctx, normalized, 0, 100, 100)
if err != nil {
return model.IPDetails{}, err
}
return s.decorateDetails(ctx, details)
}
func (s *Service) InvestigateIP(ctx context.Context, ip string) (model.IPDetails, error) {
normalized, err := normalizeIP(ip)
if err != nil {
return model.IPDetails{}, err
}
details, err := s.store.GetIPDetails(ctx, normalized, 0, 100, 100)
if err != nil {
return model.IPDetails{}, err
}
if s.investigator != nil {
investigation, found, err := s.store.GetInvestigation(ctx, normalized)
if err != nil {
return model.IPDetails{}, err
}
shouldRefresh := !found || time.Since(investigation.UpdatedAt) >= s.cfg.Investigation.RefreshAfter.Duration
if shouldRefresh {
fresh, err := s.investigator.Investigate(ctx, normalized, collectUserAgents(details.RecentEvents))
if err != nil {
return model.IPDetails{}, err
}
if err := s.store.SaveInvestigation(ctx, fresh); err != nil {
return model.IPDetails{}, err
}
details.Investigation = &fresh
}
}
return s.decorateDetails(ctx, details)
}
func (s *Service) ForceBlock(ctx context.Context, ip string, actor string, reason string) error {
@@ -410,3 +449,58 @@ func defaultReason(reason string, fallback string) string {
}
return strings.TrimSpace(reason)
}
func (s *Service) decorateDetails(ctx context.Context, details model.IPDetails) (model.IPDetails, error) {
if details.State.IP != "" && details.Investigation == nil {
investigation, found, err := s.store.GetInvestigation(ctx, details.State.IP)
if err != nil {
return model.IPDetails{}, err
}
if found {
details.Investigation = &investigation
}
}
details.OPNsense = s.resolveOPNsenseStatus(ctx, details.State)
details.Actions = actionAvailability(details.State, details.OPNsense)
return details, nil
}
func (s *Service) resolveOPNsenseStatus(ctx context.Context, state model.IPState) model.OPNsenseStatus {
status := model.OPNsenseStatus{Configured: s.blocker != nil}
if s.blocker == nil || state.IP == "" {
return status
}
status.CheckedAt = time.Now().UTC()
present, err := s.blocker.IsIPPresent(ctx, state.IP)
if err != nil {
status.Error = err.Error()
return status
}
status.Present = present
return status
}
func actionAvailability(state model.IPState, backend model.OPNsenseStatus) model.ActionAvailability {
present := false
if backend.Configured && backend.Error == "" {
present = backend.Present
} else {
present = state.State == model.IPStateBlocked || state.ManualOverride == model.ManualOverrideForceBlock
}
return model.ActionAvailability{
CanBlock: !present,
CanUnblock: present,
CanClearOverride: state.ManualOverride != model.ManualOverrideNone,
}
}
func collectUserAgents(events []model.Event) []string {
items := make([]string, 0, len(events))
for _, event := range events {
if strings.TrimSpace(event.UserAgent) == "" {
continue
}
items = append(items, event.UserAgent)
}
return items
}