From 3d71fd8c8451765504cdd3a12ab2c5b456b9f03f Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Fri, 1 May 2026 22:47:19 -0400 Subject: [PATCH] feat(monitor/collectors/ransomware): ransomware.live client with stable triple-hash ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-crafted fixture matches spec §8.5 schema. Live API endpoint may move; client takes BaseURL config for swap-in once api.ransomware.live publishes a stable JSON endpoint (current free tier returns HTML landing page on /recentvictims). --- .../internal/collectors/ransomware/client.go | 72 +++++++++++++++++++ .../collectors/ransomware/client_test.go | 44 ++++++++++++ .../ransomware/testdata/recentvictims.json | 26 +++++++ 3 files changed, 142 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/client.go create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/client_test.go create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/testdata/recentvictims.json diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/client.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/client.go new file mode 100644 index 00000000..eec9d944 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/client.go @@ -0,0 +1,72 @@ +// ©AngelaMos | 2026 +// client.go + +package ransomware + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "time" + + "golang.org/x/time/rate" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/httpx" +) + +const ( + defaultRansomBaseURL = "https://api.ransomware.live" + pathRecentVictims = "/recentvictims" + defaultRansomRate = time.Second + defaultRansomBudget = 5 + defaultRansomBreakerWin = 60 * time.Second + idHashBytes = 16 +) + +type ClientConfig struct { + BaseURL string +} + +type Client struct { + hx *httpx.Client +} + +func NewClient(cfg ClientConfig) *Client { + if cfg.BaseURL == "" { + cfg.BaseURL = defaultRansomBaseURL + } + return &Client{ + hx: httpx.New(httpx.Config{ + Name: "ransomware", + BaseURL: cfg.BaseURL, + Rate: rate.Every(defaultRansomRate), + Burst: 1, + ConsecutiveFailureBudget: defaultRansomBudget, + BreakerTimeout: defaultRansomBreakerWin, + }), + } +} + +type Victim struct { + PostTitle string `json:"post_title"` + GroupName string `json:"group_name"` + Discovered time.Time `json:"discovered"` + Country string `json:"country"` + Activity string `json:"activity"` + Website string `json:"website"` + Description string `json:"description"` +} + +func (v Victim) ID() string { + h := sha256.Sum256([]byte(v.PostTitle + "|" + v.GroupName + "|" + v.Discovered.UTC().Format(time.RFC3339))) + return hex.EncodeToString(h[:idHashBytes]) +} + +func (c *Client) FetchRecent(ctx context.Context) ([]Victim, error) { + var out []Victim + if err := c.hx.GetJSON(ctx, pathRecentVictims, nil, &out); err != nil { + return nil, fmt.Errorf("fetch recentvictims: %w", err) + } + return out, nil +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/client_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/client_test.go new file mode 100644 index 00000000..063ce9bb --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/client_test.go @@ -0,0 +1,44 @@ +// ©AngelaMos | 2026 +// client_test.go + +package ransomware_test + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/ransomware" +) + +func TestClient_FetchRecentVictimsDecodes(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + body, err := os.ReadFile("testdata/recentvictims.json") + require.NoError(t, err) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(body) + })) + defer srv.Close() + + c := ransomware.NewClient(ransomware.ClientConfig{BaseURL: srv.URL}) + vs, err := c.FetchRecent(context.Background()) + require.NoError(t, err) + require.Len(t, vs, 3) + require.Equal(t, "Acme Healthcare Corp", vs[0].PostTitle) + require.Equal(t, "lockbit", vs[0].GroupName) +} + +func TestVictim_IDIsStableAndCollisionFree(t *testing.T) { + d, _ := time.Parse(time.RFC3339, "2026-05-01T00:00:00Z") + v1 := ransomware.Victim{PostTitle: "X", GroupName: "lockbit", Discovered: d} + v2 := ransomware.Victim{PostTitle: "X", GroupName: "lockbit", Discovered: d} + v3 := ransomware.Victim{PostTitle: "Y", GroupName: "lockbit", Discovered: d} + + require.Equal(t, v1.ID(), v2.ID()) + require.NotEqual(t, v1.ID(), v3.ID()) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/testdata/recentvictims.json b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/testdata/recentvictims.json new file mode 100644 index 00000000..1f290f3d --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/testdata/recentvictims.json @@ -0,0 +1,26 @@ +[ + { + "post_title": "Acme Healthcare Corp", + "group_name": "lockbit", + "discovered": "2026-05-01T14:32:00Z", + "country": "US", + "activity": "Healthcare", + "website": "acme-health.example.com", + "description": "Patient records claimed leaked" + }, + { + "post_title": "Northern Steel Manufacturing", + "group_name": "blackcat", + "discovered": "2026-05-01T11:08:00Z", + "country": "DE", + "activity": "Manufacturing", + "website": "northern-steel.example.de" + }, + { + "post_title": "Banco Nacional", + "group_name": "play", + "discovered": "2026-04-30T22:15:00Z", + "country": "AR", + "activity": "Finance" + } +]