From 2a61953cb963619e9bdb44aa3fd93ddf51fae271 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Fri, 1 May 2026 22:38:08 -0400 Subject: [PATCH] feat(monitor/collectors/cve): cve_events repository with EPSS-only patch --- .../backend/internal/collectors/cve/repo.go | 84 +++++++++++ .../internal/collectors/cve/repo_test.go | 135 ++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/cve/repo.go create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/cve/repo_test.go diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/cve/repo.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/cve/repo.go new file mode 100644 index 00000000..0e546357 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/cve/repo.go @@ -0,0 +1,84 @@ +// ©AngelaMos | 2026 +// repo.go + +package cve + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/jmoiron/sqlx" +) + +type Row struct { + CveID string `db:"cve_id"` + Published time.Time `db:"published"` + LastModified time.Time `db:"last_modified"` + Severity string `db:"severity"` + CVSS float64 `db:"cvss_score"` + EPSSScore *float64 `db:"epss_score"` + EPSSPercentile *float64 `db:"epss_percentile"` + InKEV bool `db:"in_kev"` + Payload json.RawMessage `db:"payload"` +} + +type Repo struct { + db *sqlx.DB +} + +func NewRepo(db *sqlx.DB) *Repo { return &Repo{db: db} } + +func (r *Repo) Upsert(ctx context.Context, row Row) error { + _, err := r.db.ExecContext(ctx, ` + INSERT INTO cve_events (cve_id, published, last_modified, severity, cvss_score, payload) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (cve_id) DO UPDATE SET + last_modified = EXCLUDED.last_modified, + severity = EXCLUDED.severity, + cvss_score = EXCLUDED.cvss_score, + payload = EXCLUDED.payload`, + row.CveID, row.Published, row.LastModified, row.Severity, row.CVSS, []byte(row.Payload), + ) + if err != nil { + return fmt.Errorf("upsert cve %s: %w", row.CveID, err) + } + return nil +} + +func (r *Repo) UpdateEPSS(ctx context.Context, cveID string, score, percentile float64) error { + _, err := r.db.ExecContext(ctx, + `UPDATE cve_events SET epss_score = $2, epss_percentile = $3 WHERE cve_id = $1`, + cveID, score, percentile, + ) + if err != nil { + return fmt.Errorf("update epss %s: %w", cveID, err) + } + return nil +} + +func (r *Repo) Get(ctx context.Context, cveID string) (Row, error) { + var row Row + err := r.db.GetContext(ctx, &row, ` + SELECT cve_id, published, last_modified, severity, cvss_score, + epss_score, epss_percentile, in_kev, payload + FROM cve_events WHERE cve_id = $1`, cveID) + if err != nil { + return Row{}, fmt.Errorf("get cve %s: %w", cveID, err) + } + return row, nil +} + +func (r *Repo) RecentByLastModified(ctx context.Context, limit int) ([]Row, error) { + var rows []Row + err := r.db.SelectContext(ctx, &rows, ` + SELECT cve_id, published, last_modified, severity, cvss_score, + epss_score, epss_percentile, in_kev, payload + FROM cve_events + ORDER BY last_modified DESC LIMIT $1`, limit) + if err != nil { + return nil, fmt.Errorf("recent cves: %w", err) + } + return rows, nil +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/cve/repo_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/cve/repo_test.go new file mode 100644 index 00000000..28c171aa --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/cve/repo_test.go @@ -0,0 +1,135 @@ +// ©AngelaMos | 2026 +// repo_test.go + +package cve_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/cve" +) + +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 cve_events ( + cve_id text PRIMARY KEY, + published timestamptz, + last_modified timestamptz NOT NULL, + severity text NOT NULL DEFAULT '', + cvss_score numeric(3,1) NOT NULL DEFAULT 0, + epss_score numeric(5,4), + epss_percentile numeric(5,4), + in_kev boolean NOT NULL DEFAULT false, + payload jsonb NOT NULL + )`) + require.NoError(t, err) + return db +} + +func TestRepo_UpsertThenEPSSUpdate(t *testing.T) { + db := setupDB(t) + repo := cve.NewRepo(db) + ctx := context.Background() + + row := cve.Row{ + CveID: "CVE-2026-0001", + Published: time.Now().UTC().Add(-time.Hour), + LastModified: time.Now().UTC(), + Severity: "CRITICAL", + CVSS: 9.8, + Payload: json.RawMessage(`{"id":"CVE-2026-0001"}`), + } + require.NoError(t, repo.Upsert(ctx, row)) + + require.NoError(t, repo.UpdateEPSS(ctx, "CVE-2026-0001", 0.9512, 0.9988)) + + got, err := repo.Get(ctx, "CVE-2026-0001") + require.NoError(t, err) + require.NotNil(t, got.EPSSScore) + require.InDelta(t, 0.9512, *got.EPSSScore, 0.0001) + require.NotNil(t, got.EPSSPercentile) + require.InDelta(t, 0.9988, *got.EPSSPercentile, 0.0001) + require.Equal(t, "CRITICAL", got.Severity) + require.InDelta(t, 9.8, got.CVSS, 0.0001) +} + +func TestRepo_UpsertOnConflictUpdatesLastModified(t *testing.T) { + db := setupDB(t) + repo := cve.NewRepo(db) + ctx := context.Background() + + first := cve.Row{ + CveID: "CVE-2026-0002", + Published: time.Now().UTC().Add(-2 * time.Hour), + LastModified: time.Now().UTC().Add(-time.Hour), + Severity: "MEDIUM", + CVSS: 5.0, + Payload: json.RawMessage(`{"v":1}`), + } + require.NoError(t, repo.Upsert(ctx, first)) + + updated := first + updated.LastModified = time.Now().UTC() + updated.Severity = "HIGH" + updated.CVSS = 7.5 + updated.Payload = json.RawMessage(`{"v":2}`) + require.NoError(t, repo.Upsert(ctx, updated)) + + got, err := repo.Get(ctx, "CVE-2026-0002") + require.NoError(t, err) + require.Equal(t, "HIGH", got.Severity) + require.InDelta(t, 7.5, got.CVSS, 0.0001) +} + +func TestRepo_RecentByLastModifiedOrdered(t *testing.T) { + db := setupDB(t) + repo := cve.NewRepo(db) + ctx := context.Background() + + now := time.Now().UTC() + for i, name := range []string{"CVE-A", "CVE-B", "CVE-C"} { + require.NoError(t, repo.Upsert(ctx, cve.Row{ + CveID: name, + Published: now.Add(-time.Duration(10-i) * time.Hour), + LastModified: now.Add(-time.Duration(10-i) * time.Hour), + Severity: "LOW", + Payload: json.RawMessage(`{}`), + })) + } + + rows, err := repo.RecentByLastModified(ctx, 3) + require.NoError(t, err) + require.Len(t, rows, 3) + require.Equal(t, "CVE-C", rows[0].CveID) + require.Equal(t, "CVE-B", rows[1].CveID) + require.Equal(t, "CVE-A", rows[2].CveID) +}