package main import ( "bytes" "context" "encoding/binary" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "testing" "time" ) func TestNPMPlusTraffic(t *testing.T) { for _, ip := range []string{"8.8.8.8", "2001:4860:4860::8888"} { line := `[14/Oct/2026:16:24:17 +0000] example.test:8443 ` + ip + ` 0.001 "GET /health?token=secret HTTP/3.0" 200 123 "-" "test" "-" "text/html" TLSv1.3 - - -` e, ok := parseNPMTraffic(line) if !ok || e.IP != ip || e.Domain != "example.test" || e.Path != "/health" || e.Status != 200 { t.Fatalf("%+v %v", e, ok) } } if _, ok := parseNPMTraffic(`[14/Oct/2026:16:24:17 +0000] 443 8.8.8.8 UDP - 200 10 20 0.001`); ok { t.Fatal("stream counted as HTTP") } } func TestDockerTrafficDiscovery(t *testing.T) { var containers []trafficDockerContainer json.Unmarshal([]byte(`[{"Id":"aaa","Image":"ghcr.io/zoeyvid/npmplus:latest","Mounts":[{"Source":"/var/lib/docker/volumes/npm/_data","Destination":"/data"},{"Source":"/srv/logs","Destination":"/data/nginx/logs"}]},{"Id":"bbb","Image":"crowdsecurity/crowdsec:latest"}]`), &containers) d := trafficSources(containers, nil) if d.CrowdContainer != "bbb" || d.Error != "" || strings.Join(d.Dirs, ",") != "/var/lib/docker/volumes/npm/_data/logs,/srv/logs,/var/lib/docker/volumes/npm/_data/nginx" { t.Fatalf("%+v", d) } containers = append(containers, containers[1]) d = trafficSources(containers, nil) if d.CrowdContainer != "" || d.Error == "" { t.Fatal("ambiguous CrowdSec silently chosen") } } func TestDockerTrafficDemux(t *testing.T) { var b bytes.Buffer for _, frame := range []struct { stream byte text string }{{2, "diagnostic"}, {1, "["}, {1, "]"}} { h := []byte{frame.stream, 0, 0, 0, 0, 0, 0, 0} binary.BigEndian.PutUint32(h[4:], uint32(len(frame.text))) b.Write(h) b.WriteString(frame.text) } out, err := readDockerTrafficOutput(&b) if err != nil || string(out) != "[]" { t.Fatalf("%q %v", out, err) } if _, err := readDockerTrafficOutput(bytes.NewReader([]byte{1, 0, 0})); err == nil { t.Fatal("truncation accepted") } if _, err := readDockerTrafficOutput(bytes.NewReader([]byte{1, 0, 0, 0, 255, 255, 255, 255})); err == nil { t.Fatal("oversized frame accepted") } } func TestTrafficMultipleDirectoriesAndNPMPlusRotation(t *testing.T) { a, b := t.TempDir(), t.TempDir() p := filepath.Join(b, "access.log") os.WriteFile(filepath.Join(a, "proxy-host-1_access.log"), nil, 0600) os.WriteFile(p, nil, 0600) s := trafficAgentState{Cursors: map[string]trafficCursor{}} if n, err := s.readLogs(a, b, a); err != nil || n != 2 { t.Fatalf("files %d: %v", n, err) } line := "[" + time.Now().UTC().Format("02/Jan/2006:15:04:05 -0700") + `] example.test 8.8.8.8 0.1 "GET / HTTP/2.0" 200 123 "-"` + "\n" os.WriteFile(p, []byte(line), 0600) s.readLogs(a, b) if len(s.Pending) != 1 { t.Fatal("NPM+ event missing") } s.Pending = nil os.Rename(p, p+".1") os.WriteFile(p, []byte(line), 0600) s.readLogs(a, b) if len(s.Pending) != 1 { t.Fatalf("rotation duplicated/lost events: %d", len(s.Pending)) } } type trafficRoundTrip func(*http.Request) (*http.Response, error) func (f trafficRoundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func TestDockerCrowdExec(t *testing.T) { for _, exitCode := range []int{0, 1} { calls := 0 client := &http.Client{Transport: trafficRoundTrip(func(r *http.Request) (*http.Response, error) { calls++ body := "" status := 200 switch calls { case 1: if r.Method != "POST" || r.URL.Path != "/containers/crowdsec/exec" { t.Fatalf("unexpected %s %s", r.Method, r.URL) } var payload struct { Cmd []string Tty bool } json.NewDecoder(r.Body).Decode(&payload) if strings.Join(payload.Cmd, " ") != "cscli alerts list --since 24h --limit 50 -o json" || payload.Tty { t.Fatalf("unexpected command %+v", payload) } status = 201 body = `{"Id":"exec123"}` case 2: if r.URL.Path != "/exec/exec123/start" { t.Fatal(r.URL) } body = string([]byte{1, 0, 0, 0, 0, 0, 0, 2}) + "[]" case 3: if r.Method != "GET" || r.URL.Path != "/exec/exec123/json" { t.Fatal(r.URL) } body = fmt.Sprintf(`{"Running":false,"ExitCode":%d}`, exitCode) default: t.Fatal("extra Docker request") } return &http.Response{StatusCode: status, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil })} out, err := dockerCrowdWithClient(context.Background(), "crowdsec", client) if calls != 3 || (exitCode == 0 && (err != nil || string(out) != "[]")) || (exitCode != 0 && err == nil) { t.Fatalf("calls=%d output=%s err=%v", calls, out, err) } } }