feat(monitor/collectors/cfradar): outages + hijacks repository with diff lookups
This commit is contained in:
parent
fd861d587e
commit
ada39ef6d9
|
|
@ -70,6 +70,7 @@ require (
|
||||||
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
|
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
|
||||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||||
|
github.com/lib/pq v1.12.3 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
github.com/magiconair/properties v1.8.10 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
github.com/mdelapenya/tlscert v0.2.0 // indirect
|
github.com/mdelapenya/tlscert v0.2.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,8 @@ github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLO
|
||||||
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
|
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||||
|
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
// ©AngelaMos | 2026
|
||||||
|
// repo.go
|
||||||
|
|
||||||
|
package cfradar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutageRow struct {
|
||||||
|
ID string `db:"id"`
|
||||||
|
StartedAt time.Time `db:"started_at"`
|
||||||
|
EndedAt *time.Time `db:"ended_at"`
|
||||||
|
Locations pq.StringArray `db:"locations"`
|
||||||
|
ASNs pq.Int32Array `db:"asns"`
|
||||||
|
Cause string `db:"cause"`
|
||||||
|
OutageType string `db:"outage_type"`
|
||||||
|
Payload json.RawMessage `db:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HijackRow struct {
|
||||||
|
ID int64 `db:"id"`
|
||||||
|
DetectedAt time.Time `db:"detected_at"`
|
||||||
|
StartedAt time.Time `db:"started_at"`
|
||||||
|
DurationSec int32 `db:"duration_sec"`
|
||||||
|
Confidence int16 `db:"confidence"`
|
||||||
|
HijackerASN int32 `db:"hijacker_asn"`
|
||||||
|
VictimASNs pq.Int32Array `db:"victim_asns"`
|
||||||
|
Prefixes []string `db:"-"`
|
||||||
|
Payload json.RawMessage `db:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repo struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepo(db *sqlx.DB) *Repo { return &Repo{db: db} }
|
||||||
|
|
||||||
|
func (r *Repo) UpsertOutage(ctx context.Context, o OutageRow) error {
|
||||||
|
if o.Locations == nil {
|
||||||
|
o.Locations = pq.StringArray{}
|
||||||
|
}
|
||||||
|
if o.ASNs == nil {
|
||||||
|
o.ASNs = pq.Int32Array{}
|
||||||
|
}
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO outage_events (id, started_at, ended_at, locations, asns, cause, outage_type, payload)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
ended_at = EXCLUDED.ended_at,
|
||||||
|
locations = EXCLUDED.locations,
|
||||||
|
asns = EXCLUDED.asns,
|
||||||
|
cause = EXCLUDED.cause,
|
||||||
|
outage_type = EXCLUDED.outage_type,
|
||||||
|
payload = EXCLUDED.payload`,
|
||||||
|
o.ID, o.StartedAt, o.EndedAt, o.Locations, o.ASNs, o.Cause, o.OutageType, []byte(o.Payload),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("upsert outage %s: %w", o.ID, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) UpsertHijack(ctx context.Context, h HijackRow) error {
|
||||||
|
if h.VictimASNs == nil {
|
||||||
|
h.VictimASNs = pq.Int32Array{}
|
||||||
|
}
|
||||||
|
if h.Prefixes == nil {
|
||||||
|
h.Prefixes = []string{}
|
||||||
|
}
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO bgp_hijack_events
|
||||||
|
(id, detected_at, started_at, duration_sec, confidence, hijacker_asn, victim_asns, prefixes, payload)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::cidr[], $9)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
detected_at = EXCLUDED.detected_at,
|
||||||
|
duration_sec = EXCLUDED.duration_sec,
|
||||||
|
confidence = EXCLUDED.confidence,
|
||||||
|
payload = EXCLUDED.payload`,
|
||||||
|
h.ID, h.DetectedAt, h.StartedAt, h.DurationSec, h.Confidence, h.HijackerASN,
|
||||||
|
h.VictimASNs, pq.Array(h.Prefixes), []byte(h.Payload),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("upsert hijack %d: %w", h.ID, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) KnownOutageIDs(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 outage_events WHERE id = ANY($1::text[])`, pq.Array(ids)); err != nil {
|
||||||
|
return nil, fmt.Errorf("known outage ids: %w", err)
|
||||||
|
}
|
||||||
|
out := make(map[string]bool, len(found))
|
||||||
|
for _, id := range found {
|
||||||
|
out[id] = true
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) KnownHijackIDs(ctx context.Context, ids []int64) (map[int64]bool, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return map[int64]bool{}, nil
|
||||||
|
}
|
||||||
|
var found []int64
|
||||||
|
if err := r.db.SelectContext(ctx, &found,
|
||||||
|
`SELECT id FROM bgp_hijack_events WHERE id = ANY($1::bigint[])`, pq.Array(ids)); err != nil {
|
||||||
|
return nil, fmt.Errorf("known hijack ids: %w", err)
|
||||||
|
}
|
||||||
|
out := make(map[int64]bool, len(found))
|
||||||
|
for _, id := range found {
|
||||||
|
out[id] = true
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
// ©AngelaMos | 2026
|
||||||
|
// repo_test.go
|
||||||
|
|
||||||
|
package cfradar_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||||
|
|
||||||
|
"github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/cfradar"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 outage_events (
|
||||||
|
id text PRIMARY KEY,
|
||||||
|
started_at timestamptz NOT NULL,
|
||||||
|
ended_at timestamptz,
|
||||||
|
locations text[] NOT NULL DEFAULT '{}',
|
||||||
|
asns integer[] NOT NULL DEFAULT '{}',
|
||||||
|
cause text NOT NULL DEFAULT '',
|
||||||
|
outage_type text NOT NULL DEFAULT '',
|
||||||
|
payload jsonb NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE bgp_hijack_events (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
detected_at timestamptz NOT NULL,
|
||||||
|
started_at timestamptz NOT NULL,
|
||||||
|
duration_sec integer NOT NULL DEFAULT 0,
|
||||||
|
confidence smallint NOT NULL DEFAULT 0,
|
||||||
|
hijacker_asn integer NOT NULL DEFAULT 0,
|
||||||
|
victim_asns integer[] NOT NULL DEFAULT '{}',
|
||||||
|
prefixes cidr[] NOT NULL DEFAULT '{}',
|
||||||
|
payload jsonb NOT NULL
|
||||||
|
)`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepo_UpsertOutageIsIdempotent(t *testing.T) {
|
||||||
|
db := setupDB(t)
|
||||||
|
repo := cfradar.NewRepo(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
row := cfradar.OutageRow{
|
||||||
|
ID: "out-1",
|
||||||
|
StartedAt: time.Now().UTC().Add(-time.Hour),
|
||||||
|
Locations: pq.StringArray{"DE", "AT"},
|
||||||
|
ASNs: pq.Int32Array{15169},
|
||||||
|
Cause: "fiber-cut",
|
||||||
|
OutageType: "regional",
|
||||||
|
Payload: json.RawMessage(`{"id":"out-1"}`),
|
||||||
|
}
|
||||||
|
require.NoError(t, repo.UpsertOutage(ctx, row))
|
||||||
|
require.NoError(t, repo.UpsertOutage(ctx, row))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepo_UpsertHijackAndDiff(t *testing.T) {
|
||||||
|
db := setupDB(t)
|
||||||
|
repo := cfradar.NewRepo(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
row := cfradar.HijackRow{
|
||||||
|
ID: 7700001,
|
||||||
|
DetectedAt: time.Now().UTC(),
|
||||||
|
StartedAt: time.Now().UTC().Add(-2 * time.Minute),
|
||||||
|
Confidence: 9,
|
||||||
|
HijackerASN: 64500,
|
||||||
|
VictimASNs: pq.Int32Array{15169, 13335},
|
||||||
|
Prefixes: []string{"203.0.113.0/24"},
|
||||||
|
Payload: json.RawMessage(`{"id":7700001}`),
|
||||||
|
}
|
||||||
|
require.NoError(t, repo.UpsertHijack(ctx, row))
|
||||||
|
|
||||||
|
known, err := repo.KnownHijackIDs(ctx, []int64{7700001, 7700002})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, map[int64]bool{7700001: true}, known)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepo_KnownOutageIDs(t *testing.T) {
|
||||||
|
db := setupDB(t)
|
||||||
|
repo := cfradar.NewRepo(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
require.NoError(t, repo.UpsertOutage(ctx, cfradar.OutageRow{
|
||||||
|
ID: "out-known", StartedAt: time.Now().UTC(),
|
||||||
|
Payload: json.RawMessage(`{}`),
|
||||||
|
}))
|
||||||
|
|
||||||
|
known, err := repo.KnownOutageIDs(ctx, []string{"out-known", "out-new"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, map[string]bool{"out-known": true}, known)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepo_KnownIDsHandlesEmptySlice(t *testing.T) {
|
||||||
|
db := setupDB(t)
|
||||||
|
repo := cfradar.NewRepo(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
out, err := repo.KnownOutageIDs(ctx, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, out)
|
||||||
|
|
||||||
|
hij, err := repo.KnownHijackIDs(ctx, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, hij)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue