feat(monitor/collectors/ransomware): ransomware.live client with stable triple-hash ID
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).
This commit is contained in:
parent
01a22dc7e4
commit
3d71fd8c84
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
Loading…
Reference in New Issue