diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/client.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/client.go new file mode 100644 index 00000000..a68c17bc --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/client.go @@ -0,0 +1,85 @@ +// ©AngelaMos | 2026 +// client.go + +package dshield + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "time" + + "golang.org/x/time/rate" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/httpx" +) + +const ( + KindTopPorts = "topports" + KindTopIPs = "topips" + KindDailySummary = "dailysummary" + + defaultTopN = 10 + defaultDShieldRate = 2 * time.Second + defaultDShieldBudget = 5 + defaultBreakerWindow = 60 * time.Second +) + +type ClientConfig struct { + BaseURL string + TopN int +} + +type Client struct { + hx *httpx.Client + topN int +} + +func NewClient(cfg ClientConfig) *Client { + if cfg.BaseURL == "" { + cfg.BaseURL = "https://isc.sans.edu" + } + if cfg.TopN <= 0 { + cfg.TopN = defaultTopN + } + return &Client{ + hx: httpx.New(httpx.Config{ + Name: "dshield", + BaseURL: cfg.BaseURL, + Rate: rate.Every(defaultDShieldRate), + Burst: 1, + ConsecutiveFailureBudget: defaultDShieldBudget, + BreakerTimeout: defaultBreakerWindow, + }), + topN: cfg.TopN, + } +} + +type SnapshotPayload struct { + Kind string + Payload json.RawMessage +} + +func (c *Client) FetchAll(ctx context.Context) ([]SnapshotPayload, error) { + yesterday := time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02") + + endpoints := []struct { + Kind string + Path string + }{ + {KindTopPorts, "/api/topports/records/" + strconv.Itoa(c.topN) + "/" + yesterday + "?json"}, + {KindTopIPs, "/api/topips/?json"}, + {KindDailySummary, "/api/dailysummary/" + yesterday + "/?json"}, + } + + out := make([]SnapshotPayload, 0, len(endpoints)) + for _, ep := range endpoints { + var raw json.RawMessage + if err := c.hx.GetJSON(ctx, ep.Path, nil, &raw); err != nil { + return nil, fmt.Errorf("dshield %s: %w", ep.Kind, err) + } + out = append(out, SnapshotPayload{Kind: ep.Kind, Payload: raw}) + } + return out, nil +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/client_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/client_test.go new file mode 100644 index 00000000..74881918 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/client_test.go @@ -0,0 +1,93 @@ +// ©AngelaMos | 2026 +// client_test.go + +package dshield_test + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/dshield" +) + +func newFixtureServer(t *testing.T) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + mux.HandleFunc("/api/topports/", func(w http.ResponseWriter, r *http.Request) { + require.Contains(t, r.URL.Path, "/api/topports/records/") + body, err := os.ReadFile(filepath.Join("testdata", "topports.json")) + require.NoError(t, err) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(body) + }) + mux.HandleFunc("/api/topips/", func(w http.ResponseWriter, _ *http.Request) { + body, err := os.ReadFile(filepath.Join("testdata", "topips.json")) + require.NoError(t, err) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(body) + }) + mux.HandleFunc("/api/dailysummary/", func(w http.ResponseWriter, r *http.Request) { + require.True(t, strings.HasPrefix(r.URL.Path, "/api/dailysummary/")) + body, err := os.ReadFile(filepath.Join("testdata", "dailysummary.json")) + require.NoError(t, err) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(body) + }) + return httptest.NewServer(mux) +} + +func TestClient_FetchAllReturnsThreeKinds(t *testing.T) { + srv := newFixtureServer(t) + defer srv.Close() + + c := dshield.NewClient(dshield.ClientConfig{BaseURL: srv.URL, TopN: 10}) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + snaps, err := c.FetchAll(ctx) + require.NoError(t, err) + require.Len(t, snaps, 3) + + kinds := map[string]bool{} + for _, s := range snaps { + kinds[s.Kind] = true + require.NotEmpty(t, s.Payload, "kind %s payload empty", s.Kind) + } + require.True(t, kinds[dshield.KindTopPorts]) + require.True(t, kinds[dshield.KindTopIPs]) + require.True(t, kinds[dshield.KindDailySummary]) +} + +func TestClient_FetchAllUsesYesterdayDate(t *testing.T) { + var topPortsPath, dailyPath string + mux := http.NewServeMux() + mux.HandleFunc("/api/topports/", func(w http.ResponseWriter, r *http.Request) { + topPortsPath = r.URL.Path + _, _ = w.Write([]byte(`{}`)) + }) + mux.HandleFunc("/api/topips/", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`[]`)) + }) + mux.HandleFunc("/api/dailysummary/", func(w http.ResponseWriter, r *http.Request) { + dailyPath = r.URL.Path + _, _ = w.Write([]byte(`[]`)) + }) + srv := httptest.NewServer(mux) + defer srv.Close() + + c := dshield.NewClient(dshield.ClientConfig{BaseURL: srv.URL, TopN: 5}) + _, _ = c.FetchAll(context.Background()) + + yesterday := time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02") + require.Contains(t, topPortsPath, "/records/5/"+yesterday) + require.Contains(t, dailyPath, "/dailysummary/"+yesterday) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/dailysummary.json b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/dailysummary.json new file mode 100644 index 00000000..59f2fafc --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/dailysummary.json @@ -0,0 +1 @@ +[{"date":"2026-05-01","records":17196921,"sources":146555,"targets":871}] diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/topips.json b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/topips.json new file mode 100644 index 00000000..32610be1 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/topips.json @@ -0,0 +1 @@ +[{"rank":1,"source":"13.94.254.200","reports":319739,"targets":1},{"rank":2,"source":"194.224.249.214","reports":309508,"targets":1},{"rank":3,"source":"52.157.207.201","reports":194780,"targets":1},{"rank":4,"source":"89.248.163.109","reports":96279,"targets":150},{"rank":5,"source":"143.244.186.35","reports":92007,"targets":8},{"rank":6,"source":"18.116.198.191","reports":59907,"targets":1},{"rank":7,"source":"152.89.198.163","reports":41378,"targets":478},{"rank":8,"source":"152.89.198.103","reports":41346,"targets":479},{"rank":9,"source":"152.89.198.187","reports":41332,"targets":477},{"rank":10,"source":"185.122.204.71","reports":41221,"targets":479}] diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/topports.json b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/topports.json new file mode 100644 index 00000000..e63e7484 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/dshield/testdata/topports.json @@ -0,0 +1 @@ +{"0":{"rank":1,"targetport":23,"records":5756817,"targets":316,"sources":52950},"1":{"rank":2,"targetport":80,"records":1151422,"targets":316,"sources":9421},"2":{"rank":3,"targetport":5555,"records":1106744,"targets":298,"sources":3668},"3":{"rank":4,"targetport":8080,"records":978049,"targets":309,"sources":7062},"4":{"rank":5,"targetport":22,"records":514078,"targets":326,"sources":9113},"5":{"rank":6,"targetport":2222,"records":371960,"targets":318,"sources":7388},"6":{"rank":7,"targetport":51223,"records":313702,"targets":15,"sources":1293},"7":{"rank":8,"targetport":8000,"records":241520,"targets":323,"sources":6078},"8":{"rank":9,"targetport":443,"records":231949,"targets":432,"sources":8308},"9":{"rank":10,"targetport":68,"records":215704,"targets":185,"sources":209},"date":"2026-05-01","limit":10}