You've already forked caddy-opnsense-blocker
Add on-demand IP investigation and richer IP details
This commit is contained in:
@@ -94,6 +94,14 @@ CREATE TABLE IF NOT EXISTS source_offsets (
|
||||
offset INTEGER NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ip_investigations (
|
||||
ip TEXT PRIMARY KEY,
|
||||
payload_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ip_investigations_updated_at ON ip_investigations(updated_at DESC, ip ASC);
|
||||
`
|
||||
|
||||
type Store struct {
|
||||
@@ -492,6 +500,53 @@ func (s *Store) GetIPDetails(ctx context.Context, ip string, eventLimit, decisio
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetInvestigation(ctx context.Context, ip string) (model.IPInvestigation, bool, error) {
|
||||
row := s.db.QueryRowContext(ctx, `SELECT payload_json, updated_at FROM ip_investigations WHERE ip = ?`, ip)
|
||||
var payload string
|
||||
var updatedAt string
|
||||
if err := row.Scan(&payload, &updatedAt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return model.IPInvestigation{}, false, nil
|
||||
}
|
||||
return model.IPInvestigation{}, false, fmt.Errorf("query ip investigation %q: %w", ip, err)
|
||||
}
|
||||
var item model.IPInvestigation
|
||||
if err := json.Unmarshal([]byte(payload), &item); err != nil {
|
||||
return model.IPInvestigation{}, false, fmt.Errorf("decode ip investigation %q: %w", ip, err)
|
||||
}
|
||||
parsed, err := parseTime(updatedAt)
|
||||
if err != nil {
|
||||
return model.IPInvestigation{}, false, fmt.Errorf("parse ip investigation updated_at: %w", err)
|
||||
}
|
||||
item.UpdatedAt = parsed
|
||||
return item, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) SaveInvestigation(ctx context.Context, item model.IPInvestigation) error {
|
||||
if item.UpdatedAt.IsZero() {
|
||||
item.UpdatedAt = time.Now().UTC()
|
||||
}
|
||||
payload, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode ip investigation: %w", err)
|
||||
}
|
||||
_, err = s.db.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO ip_investigations (ip, payload_json, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(ip) DO UPDATE SET
|
||||
payload_json = excluded.payload_json,
|
||||
updated_at = excluded.updated_at`,
|
||||
item.IP,
|
||||
string(payload),
|
||||
formatTime(item.UpdatedAt),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert ip investigation %q: %w", item.IP, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSourceOffset(ctx context.Context, sourceName string) (model.SourceOffset, bool, error) {
|
||||
row := s.db.QueryRowContext(ctx, `SELECT source_name, path, inode, offset, updated_at FROM source_offsets WHERE source_name = ?`, sourceName)
|
||||
var offset model.SourceOffset
|
||||
@@ -536,10 +591,7 @@ func (s *Store) SaveSourceOffset(ctx context.Context, offset model.SourceOffset)
|
||||
}
|
||||
|
||||
func (s *Store) listEventsForIP(ctx context.Context, ip string, limit int) ([]model.Event, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
query := `
|
||||
SELECT e.id, e.source_name, e.profile_name, e.occurred_at, e.remote_ip, e.client_ip, e.host,
|
||||
e.method, e.uri, e.path, e.status, e.user_agent, e.decision, e.decision_reason,
|
||||
e.decision_reasons_json, e.enforced, e.raw_json, e.created_at,
|
||||
@@ -547,17 +599,19 @@ func (s *Store) listEventsForIP(ctx context.Context, ip string, limit int) ([]mo
|
||||
FROM events e
|
||||
LEFT JOIN ip_state s ON s.ip = e.client_ip
|
||||
WHERE e.client_ip = ?
|
||||
ORDER BY e.occurred_at DESC, e.id DESC
|
||||
LIMIT ?`,
|
||||
ip,
|
||||
limit,
|
||||
)
|
||||
ORDER BY e.occurred_at DESC, e.id DESC`
|
||||
args := []any{ip}
|
||||
if limit > 0 {
|
||||
query += ` LIMIT ?`
|
||||
args = append(args, limit)
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list events for ip %q: %w", ip, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]model.Event, 0, limit)
|
||||
items := make([]model.Event, 0, max(limit, 0))
|
||||
for rows.Next() {
|
||||
item, err := scanEvent(rows)
|
||||
if err != nil {
|
||||
@@ -651,6 +705,13 @@ func (s *Store) listBackendActionsForIP(ctx context.Context, ip string, limit in
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func max(left int, right int) int {
|
||||
if left > right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
func getIPStateDB(ctx context.Context, db queryer, ip string) (model.IPState, bool, error) {
|
||||
row := db.QueryRowContext(ctx, `
|
||||
SELECT ip, first_seen_at, last_seen_at, last_source_name, last_user_agent, latest_status,
|
||||
|
||||
@@ -113,4 +113,20 @@ func TestStoreRecordsEventsAndState(t *testing.T) {
|
||||
if len(details.RecentEvents) != 1 || len(details.Decisions) != 1 || len(details.BackendActions) != 1 {
|
||||
t.Fatalf("unexpected ip details: %+v", details)
|
||||
}
|
||||
|
||||
investigation := model.IPInvestigation{
|
||||
IP: event.ClientIP,
|
||||
UpdatedAt: occurredAt,
|
||||
Bot: &model.BotMatch{Name: "Googlebot", ProviderID: "google_official", Icon: "🤖", Method: "published_ranges", Verified: true},
|
||||
}
|
||||
if err := db.SaveInvestigation(ctx, investigation); err != nil {
|
||||
t.Fatalf("save investigation: %v", err)
|
||||
}
|
||||
loadedInvestigation, found, err := db.GetInvestigation(ctx, event.ClientIP)
|
||||
if err != nil {
|
||||
t.Fatalf("get investigation: %v", err)
|
||||
}
|
||||
if !found || loadedInvestigation.Bot == nil || loadedInvestigation.Bot.Name != "Googlebot" {
|
||||
t.Fatalf("unexpected investigation payload: found=%v investigation=%+v", found, loadedInvestigation)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user