mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-01 07:16:22 +02:00
Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates
This commit is contained in:
parent
694e32be26
commit
41cbfd6e0a
178 changed files with 36008 additions and 399 deletions
159
internal/application/sidecar/client.go
Normal file
159
internal/application/sidecar/client.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
package sidecar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
domsoc "github.com/syntrex/gomcp/internal/domain/soc"
|
||||
)
|
||||
|
||||
// BusClient sends security events to the SOC Event Bus via HTTP POST.
|
||||
type BusClient struct {
|
||||
baseURL string
|
||||
sensorID string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
// NewBusClient creates a client for the SOC Event Bus.
|
||||
func NewBusClient(baseURL, sensorID, apiKey string) *BusClient {
|
||||
return &BusClient{
|
||||
baseURL: baseURL,
|
||||
sensorID: sensorID,
|
||||
apiKey: apiKey,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
MaxIdleConnsPerHost: 5,
|
||||
},
|
||||
},
|
||||
maxRetries: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// ingestPayload matches the SOC ingest API expected JSON.
|
||||
type ingestPayload struct {
|
||||
Source string `json:"source"`
|
||||
SensorID string `json:"sensor_id"`
|
||||
SensorKey string `json:"sensor_key,omitempty"`
|
||||
Severity string `json:"severity"`
|
||||
Category string `json:"category"`
|
||||
Subcategory string `json:"subcategory,omitempty"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Description string `json:"description"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// SendEvent posts a SOCEvent to the Event Bus.
|
||||
// Accepts context for graceful cancellation during retries (L-2 fix).
|
||||
func (c *BusClient) SendEvent(ctx context.Context, evt *domsoc.SOCEvent) error {
|
||||
payload := ingestPayload{
|
||||
Source: string(evt.Source),
|
||||
SensorID: c.sensorID,
|
||||
SensorKey: c.apiKey,
|
||||
Severity: string(evt.Severity),
|
||||
Category: evt.Category,
|
||||
Subcategory: evt.Subcategory,
|
||||
Confidence: evt.Confidence,
|
||||
Description: evt.Description,
|
||||
SessionID: evt.SessionID,
|
||||
Metadata: evt.Metadata,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sidecar: marshal event: %w", err)
|
||||
}
|
||||
|
||||
url := c.baseURL + "/api/v1/soc/events"
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= c.maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
// Context-aware backoff: cancellable during shutdown (H-1 fix).
|
||||
backoff := time.Duration(attempt*attempt) * 500 * time.Millisecond
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("sidecar: send cancelled during retry: %w", ctx.Err())
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("sidecar: create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
slog.Warn("sidecar: bus POST failed, retrying",
|
||||
"attempt", attempt+1, "error", err)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = fmt.Errorf("bus returned %d", resp.StatusCode)
|
||||
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
||||
// Client error — don't retry.
|
||||
return lastErr
|
||||
}
|
||||
slog.Warn("sidecar: bus returned server error, retrying",
|
||||
"attempt", attempt+1, "status", resp.StatusCode)
|
||||
}
|
||||
|
||||
return fmt.Errorf("sidecar: exhausted retries: %w", lastErr)
|
||||
}
|
||||
|
||||
// Heartbeat sends a sensor heartbeat to the Event Bus.
|
||||
func (c *BusClient) Heartbeat() error {
|
||||
payload := map[string]string{
|
||||
"sensor_id": c.sensorID,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sidecar: marshal heartbeat: %w", err)
|
||||
}
|
||||
|
||||
url := c.baseURL + "/api/soc/sensors/heartbeat"
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("heartbeat returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Healthy checks if the bus is reachable (M-4 fix: /healthz not /health).
|
||||
func (c *BusClient) Healthy() bool {
|
||||
resp, err := c.httpClient.Get(c.baseURL + "/healthz")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue