// Package audit pushes local audit events to auth-portal ingest API. package audit import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "strings" "time" "evobgp/internal/httpclient" "evobgp/internal/store" ) const ingestPath = "/api/v1/ingest/audit" // PortalPusher sends audit rows to auth-portal (best-effort, async-friendly). type PortalPusher struct { BaseURL string Secret string HTTPClient *http.Client MarkPushed func(id string) error } // PushEvent posts one audit entry to portal ingest. func (p *PortalPusher) PushEvent(ctx context.Context, entry *store.AuditEntry) error { if p == nil || entry == nil { return nil } base := strings.TrimRight(strings.TrimSpace(p.BaseURL), "/") secret := strings.TrimSpace(p.Secret) if base == "" || secret == "" { return nil } hc := p.HTTPClient if hc == nil { hc = httpclient.New(15 * time.Second) } body := map[string]any{ "events": []map[string]any{p.eventPayload(entry)}, } raw, err := json.Marshal(body) if err != nil { return fmt.Errorf("audit: marshal ingest: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, base+ingestPath, bytes.NewReader(raw)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+secret) resp, err := hc.Do(req) if err != nil { return fmt.Errorf("audit: portal ingest: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= 300 { b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return fmt.Errorf("audit: portal ingest %s: %s", resp.Status, strings.TrimSpace(string(b))) } if p.MarkPushed != nil { if err := p.MarkPushed(entry.ID); err != nil { log.Printf("audit: mark portal pushed id=%s: %v", entry.ID, err) } } return nil } func (p *PortalPusher) eventPayload(entry *store.AuditEntry) map[string]any { ev := map[string]any{ "event_id": entry.EventID, "source_app": store.AuditSourceAppBGP, "action": entry.Action, "severity": entry.Severity, "summary": entry.Summary, "created_at": entry.CreatedAt.UTC().Format(time.RFC3339Nano), } if entry.ActorUserID != "" { ev["actor_user_id"] = entry.ActorUserID } else { ev["actor_user_id"] = nil } if entry.ActorEmail != "" { ev["actor_email"] = entry.ActorEmail } else { ev["actor_email"] = nil } if entry.ActorName != "" { ev["actor_name"] = entry.ActorName } else { ev["actor_name"] = nil } if entry.TargetType != "" { ev["target_type"] = entry.TargetType } else { ev["target_type"] = nil } if entry.TargetID != "" { ev["target_id"] = entry.TargetID } else { ev["target_id"] = nil } if entry.Details != nil { ev["details"] = entry.Details } else { ev["details"] = nil } if entry.IP != "" { ev["ip"] = entry.IP } else { ev["ip"] = nil } return ev }