feat(monitor/collectors/cve): cve_events repository with EPSS-only patch
This commit is contained in:
parent
ee358e38f1
commit
2a61953cb9
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue