diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/kev/repo.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/kev/repo.go new file mode 100644 index 00000000..75edabc3 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/kev/repo.go @@ -0,0 +1,77 @@ +// ©AngelaMos | 2026 +// repo.go + +package kev + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/jmoiron/sqlx" + "github.com/lib/pq" +) + +type Row struct { + CveID string `db:"cve_id"` + Vendor string `db:"vendor"` + Product string `db:"product"` + VulnerabilityName string `db:"vulnerability_name"` + DateAdded time.Time `db:"date_added"` + DueDate *time.Time `db:"due_date"` + RansomwareUse string `db:"ransomware_use"` + 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 kev_entries + (cve_id, vendor, product, vulnerability_name, date_added, due_date, ransomware_use, payload) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (cve_id) DO UPDATE SET + vendor = EXCLUDED.vendor, + product = EXCLUDED.product, + vulnerability_name = EXCLUDED.vulnerability_name, + date_added = EXCLUDED.date_added, + due_date = EXCLUDED.due_date, + ransomware_use = EXCLUDED.ransomware_use, + payload = EXCLUDED.payload`, + row.CveID, row.Vendor, row.Product, row.VulnerabilityName, + row.DateAdded, row.DueDate, row.RansomwareUse, []byte(row.Payload), + ) + if err != nil { + return fmt.Errorf("insert kev %s: %w", row.CveID, 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 cve_id FROM kev_entries WHERE cve_id = ANY($1::text[])`, pq.Array(ids)); err != nil { + return nil, fmt.Errorf("kev known ids: %w", err) + } + out := make(map[string]bool, len(found)) + for _, id := range found { + out[id] = true + } + return out, nil +} + +func (r *Repo) Count(ctx context.Context) (int64, error) { + var n int64 + if err := r.db.GetContext(ctx, &n, `SELECT count(*) FROM kev_entries`); err != nil { + return 0, fmt.Errorf("kev count: %w", err) + } + return n, nil +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/kev/repo_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/kev/repo_test.go new file mode 100644 index 00000000..2cda2b07 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/kev/repo_test.go @@ -0,0 +1,115 @@ +// ©AngelaMos | 2026 +// repo_test.go + +package kev_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/kev" +) + +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 kev_entries ( + cve_id text PRIMARY KEY, + vendor text NOT NULL DEFAULT '', + product text NOT NULL DEFAULT '', + vulnerability_name text NOT NULL DEFAULT '', + date_added date NOT NULL, + due_date date, + ransomware_use text NOT NULL DEFAULT '', + payload jsonb NOT NULL + )`) + require.NoError(t, err) + return db +} + +func TestRepo_InsertAndKnownIDs(t *testing.T) { + db := setupDB(t) + repo := kev.NewRepo(db) + ctx := context.Background() + added := time.Date(2026, 4, 30, 0, 0, 0, 0, time.UTC) + + require.NoError(t, repo.Insert(ctx, kev.Row{ + CveID: "CVE-2024-3094", + Vendor: "JiaT75", + Product: "xz", + VulnerabilityName: "Backdoor", + DateAdded: added, + Payload: json.RawMessage(`{"cveID":"CVE-2024-3094"}`), + })) + + known, err := repo.KnownIDs(ctx, []string{"CVE-2024-3094", "CVE-2024-MISSING"}) + require.NoError(t, err) + require.True(t, known["CVE-2024-3094"]) + require.False(t, known["CVE-2024-MISSING"]) +} + +func TestRepo_InsertIsIdempotent(t *testing.T) { + db := setupDB(t) + repo := kev.NewRepo(db) + ctx := context.Background() + + row := kev.Row{ + CveID: "CVE-2024-X", + Vendor: "v1", + DateAdded: time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC), + Payload: json.RawMessage(`{}`), + } + require.NoError(t, repo.Insert(ctx, row)) + + updated := row + updated.Vendor = "v2" + require.NoError(t, repo.Insert(ctx, updated)) + + count, err := repo.Count(ctx) + require.NoError(t, err) + require.EqualValues(t, 1, count) +} + +func TestRepo_CountReflectsTotal(t *testing.T) { + db := setupDB(t) + repo := kev.NewRepo(db) + ctx := context.Background() + added := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC) + + for _, id := range []string{"CVE-A", "CVE-B", "CVE-C"} { + require.NoError(t, repo.Insert(ctx, kev.Row{ + CveID: id, DateAdded: added, Payload: json.RawMessage(`{}`), + })) + } + + count, err := repo.Count(ctx) + require.NoError(t, err) + require.EqualValues(t, 3, count) +}