From 01a22dc7e4c00ddcd4f01c15878a7d5963052332 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Fri, 1 May 2026 22:45:23 -0400 Subject: [PATCH] feat(monitor/collectors/ransomware): victims repository with idempotent insert --- .../internal/collectors/ransomware/repo.go | 60 +++++++++++++ .../collectors/ransomware/repo_test.go | 87 +++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/repo.go create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/repo_test.go diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/repo.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/repo.go new file mode 100644 index 00000000..3c03d231 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/repo.go @@ -0,0 +1,60 @@ +// ©AngelaMos | 2026 +// repo.go + +package ransomware + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/jmoiron/sqlx" + "github.com/lib/pq" +) + +type Row struct { + ID string `db:"id"` + PostTitle string `db:"post_title"` + GroupName string `db:"group_name"` + DiscoveredAt time.Time `db:"discovered_at"` + Country string `db:"country"` + Sector string `db:"sector"` + Payload json.RawMessage `db:"payload"` +} + +type Repo struct { + db *sqlx.DB +} + +func NewRepo(db *sqlx.DB) *Repo { return &Repo{db: db} } + +func (r *Repo) Insert(ctx context.Context, row Row) error { + _, err := r.db.ExecContext(ctx, ` + INSERT INTO ransomware_victims + (id, post_title, group_name, discovered_at, country, sector, payload) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO NOTHING`, + row.ID, row.PostTitle, row.GroupName, row.DiscoveredAt, row.Country, row.Sector, []byte(row.Payload), + ) + if err != nil { + return fmt.Errorf("insert ransom %s: %w", row.ID, err) + } + return nil +} + +func (r *Repo) KnownIDs(ctx context.Context, ids []string) (map[string]bool, error) { + if len(ids) == 0 { + return map[string]bool{}, nil + } + var found []string + if err := r.db.SelectContext(ctx, &found, + `SELECT id FROM ransomware_victims WHERE id = ANY($1::text[])`, pq.Array(ids)); err != nil { + return nil, fmt.Errorf("ransom known ids: %w", err) + } + out := make(map[string]bool, len(found)) + for _, id := range found { + out[id] = true + } + return out, nil +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/repo_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/repo_test.go new file mode 100644 index 00000000..70c1da24 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/ransomware/repo_test.go @@ -0,0 +1,87 @@ +// ©AngelaMos | 2026 +// repo_test.go + +package ransomware_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/modules/postgres" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/ransomware" +) + +func setupDB(t *testing.T) *sqlx.DB { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + pg, err := postgres.Run(ctx, "postgres:17-alpine", + postgres.WithDatabase("monitor"), + postgres.WithUsername("monitor"), + postgres.WithPassword("monitor"), + postgres.BasicWaitStrategies(), + ) + require.NoError(t, err) + t.Cleanup(func() { _ = pg.Terminate(context.Background()) }) + + dsn, err := pg.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := sqlx.ConnectContext(ctx, "pgx", dsn) + require.NoError(t, err) + t.Cleanup(func() { _ = db.Close() }) + + _, err = db.ExecContext(ctx, ` + CREATE TABLE ransomware_victims ( + id text PRIMARY KEY, + post_title text NOT NULL, + group_name text NOT NULL, + discovered_at timestamptz NOT NULL, + country text NOT NULL DEFAULT '', + sector text NOT NULL DEFAULT '', + payload jsonb NOT NULL + )`) + require.NoError(t, err) + return db +} + +func TestRepo_InsertIsIdempotent(t *testing.T) { + db := setupDB(t) + repo := ransomware.NewRepo(db) + ctx := context.Background() + + row := ransomware.Row{ + ID: "id-1", + PostTitle: "Acme Corp", + GroupName: "lockbit", + DiscoveredAt: time.Now().UTC(), + Country: "US", + Sector: "Healthcare", + Payload: json.RawMessage(`{}`), + } + require.NoError(t, repo.Insert(ctx, row)) + require.NoError(t, repo.Insert(ctx, row)) + + known, err := repo.KnownIDs(ctx, []string{"id-1", "id-2"}) + require.NoError(t, err) + require.True(t, known["id-1"]) + require.False(t, known["id-2"]) +} + +func TestRepo_KnownIDsHandlesEmptySlice(t *testing.T) { + db := setupDB(t) + repo := ransomware.NewRepo(db) + ctx := context.Background() + + out, err := repo.KnownIDs(ctx, nil) + require.NoError(t, err) + require.Empty(t, out) +}