package main import ( "encoding/json" "testing" "time" ) func TestMonitoringRulesValidation(t *testing.T) { rules, err := validateMonitoringRules([]MonitoringRule{{IP: "2001:db8:0:0::1", Domain: "EXAMPLE.org", PathPrefix: "/health"}}) if err != nil || rules[0].IP != "2001:db8::1" || rules[0].Domain != "example.org" { t.Fatalf("normalization: %+v %v", rules, err) } for _, r := range []MonitoringRule{{IP: "192.168.0.0/24"}, {IP: "8.8.8.8", Domain: "https://example.org"}, {IP: "8.8.8.8", PathPrefix: "health"}} { if _, err := validateMonitoringRules([]MonitoringRule{r}); err == nil { t.Fatalf("bad rule accepted: %+v", r) } } if _, err := validateMonitoringRules(make([]MonitoringRule, 33)); err == nil { t.Fatal("unbounded rules") } } func TestMonitoringFiltersKeepHistoryAndSecurityEvents(t *testing.T) { s, a := trafficFixture(t) rules := []MonitoringRule{{IP: "192.168.0.10"}, {IP: "8.8.8.8", Domain: "example.org", PathPrefix: "/health"}} s.SaveTrafficSettings(TrafficSettings{Enabled: true, AgentID: a.ID, MonitoringRules: rules}) now := time.Now().Unix() batch := TrafficBatch{Events: []TrafficEvent{ {ID: "probe", Time: now, Kind: "request", IP: "192.168.0.10", Domain: "other.org", Path: "/"}, {ID: "health", Time: now, Kind: "request", IP: "8.8.8.8", Domain: "EXAMPLE.org", Path: "/health/live"}, {ID: "visitor", Time: now, Kind: "request", IP: "8.8.8.8", Domain: "example.org", Path: "/login"}, {ID: "other-domain", Time: now, Kind: "request", IP: "8.8.8.8", Domain: "other.org", Path: "/health"}, {ID: "alert", Time: now, Kind: "alert", IP: "192.168.0.10"}, {ID: "ban", Time: now, Kind: "decision", IP: "192.168.0.10"}, }} if err := s.SaveTraffic(a.ID, batch); err != nil { t.Fatal(err) } all, err := s.TrafficView("", "", false) if err != nil || all.Requests != 4 { t.Fatalf("all: %+v %v", all, err) } tagged := 0 for _, e := range all.Events { if e.Monitoring { tagged++ } } if tagged != 2 { t.Fatalf("tags %d", tagged) } filtered, err := s.TrafficView("", "", false, true) if err != nil || filtered.Requests != 2 || filtered.Alerts != 1 || filtered.Decisions != 1 || len(filtered.Events) != 4 { t.Fatalf("filter: %+v %v", filtered, err) } if len(filtered.Series) != 1 || filtered.Series[0].Count != 2 { t.Fatalf("graph leaks probes: %+v", filtered.Series) } var count int s.db.QueryRow("SELECT COUNT(*) FROM traffic_events").Scan(&count) if count != 6 { t.Fatal("history deleted") } s.SaveTrafficSettings(TrafficSettings{Enabled: true, AgentID: a.ID, MonitoringRules: []MonitoringRule{}}) filtered, err = s.TrafficView("", "", false, true) if err != nil || filtered.Requests != 4 { t.Fatal("cleared rules not applied retroactively") } } func TestMonitoringPrefixIsLiteralAndDefaultCanBeCleared(t *testing.T) { s, a := trafficFixture(t) if len(s.TrafficSettings().MonitoringRules) != 1 { t.Fatal("missing suggested rule") } rules := []MonitoringRule{{IP: "8.8.8.8", PathPrefix: "/health_%"}} s.SaveTrafficSettings(TrafficSettings{Enabled: true, AgentID: a.ID, MonitoringRules: rules}) s.SaveTraffic(a.ID, TrafficBatch{Events: []TrafficEvent{{ID: "x", Time: time.Now().Unix(), Kind: "request", IP: "8.8.8.8", Path: "/health_ok"}}}) v, err := s.TrafficView("", "", false, true) if err != nil || v.Requests != 1 { t.Fatal("prefix used as wildcard") } s.SaveTrafficSettings(TrafficSettings{Enabled: true, AgentID: a.ID}) if len(s.TrafficSettings().MonitoringRules) != 0 { t.Fatal("cannot clear default") } } func TestNetworkMapAndMonitoringSourceOnly(t *testing.T) { s, a := trafficFixture(t) rules := []MonitoringRule{{IP: "192.168.0.10"}, {IP: "8.8.8.8", Domain: "example.org"}} s.SaveTrafficSettings(TrafficSettings{Enabled: true, NetworkEnabled: true, AgentID: a.ID, MonitoringRules: rules}) incoming, _ := parseNetworkFlow(networkNATLine) probe := incoming probe.Source = "192.168.0.10" outgoing := incoming outgoing.Source = "192.168.0.12" outgoing.Destination = "1.1.1.1" outgoing.Protocol = "udp" local := incoming local.Source = "192.168.0.15" local.Destination = "192.168.0.10" local.Protocol = "icmpv6" s.SaveNetwork(a.ID, NetworkSnapshot{At: time.Now().Unix(), Flows: []NetworkFlow{incoming, probe, outgoing, local}}) snapshot, err := s.ReadNetworkSnapshot(a.ID, rules, true) if err != nil || len(snapshot.Flows) != 3 || snapshot.MonitoringHidden != 1 { t.Fatalf("source-only: %+v %v", snapshot, err) } mapped := makeNetworkMap(snapshot, false) if mapped.TCP != 1 || mapped.UDP != 1 || mapped.ICMP != 1 || mapped.Unmapped != 1 || len(mapped.Points) != 2 { t.Fatalf("map: %+v", mapped) } for _, p := range mapped.Points { if p.Protocol == "udp" && p.Direction != "out" { t.Fatal("outbound direction lost") } if p.Protocol == "tcp" && p.Direction != "in" { t.Fatal("inbound direction lost") } } mapped = makeNetworkMap(snapshot, true) if mapped.ICMP != 0 { t.Fatal("LAN flow leaked") } view, err := s.TrafficView("example.org", "alert", false, true) if err != nil || view.Network == nil || view.Network.TCP != 1 { t.Fatal("HTTP filters incorrectly hide network layer") } encoded, _ := json.Marshal(view) if len(encoded) == 0 { t.Fatal("map not serializable") } s.SaveTrafficSettings(TrafficSettings{Enabled: true, AgentID: a.ID}) view, _ = s.TrafficView("", "", false) if view.Network != nil { t.Fatal("disabled network leaked") } v6 := NetworkFlow{Source: "2001:db8:0:0::1"} if !isMonitoringFlow(v6, []MonitoringRule{{IP: "2001:db8::1"}}) { t.Fatal("IPv6 representation mismatch") } }