WIP: Save current state - security subsystems, migrations, logging

This commit is contained in:
Fimeg
2025-12-16 14:19:59 -05:00
parent f792ab23c7
commit f7c8d23c5d
89 changed files with 8884 additions and 1394 deletions

View File

@@ -7,10 +7,13 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/event"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/models"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
"github.com/google/uuid"
)
@@ -23,6 +26,8 @@ type Client struct {
RapidPollingEnabled bool
RapidPollingUntil time.Time
machineID string // Cached machine ID for security binding
eventBuffer *event.Buffer
agentID uuid.UUID
}
// NewClient creates a new API client
@@ -45,6 +50,58 @@ func NewClient(baseURL, token string) *Client {
}
}
// NewClientWithEventBuffer creates a new API client with event buffering capability
func NewClientWithEventBuffer(baseURL, token string, statePath string, agentID uuid.UUID) *Client {
client := NewClient(baseURL, token)
client.agentID = agentID
// Initialize event buffer if state path is provided
if statePath != "" {
eventBufferPath := filepath.Join(statePath, "events_buffer.json")
client.eventBuffer = event.NewBuffer(eventBufferPath)
}
return client
}
// bufferEvent buffers a system event for later reporting
func (c *Client) bufferEvent(eventType, eventSubtype, severity, component, message string, metadata map[string]interface{}) {
if c.eventBuffer == nil {
return // Event buffering not enabled
}
// Use agent ID if available, otherwise create event with nil agent ID
var agentIDPtr *uuid.UUID
if c.agentID != uuid.Nil {
agentIDPtr = &c.agentID
}
event := &models.SystemEvent{
ID: uuid.New(),
AgentID: agentIDPtr,
EventType: eventType,
EventSubtype: eventSubtype,
Severity: severity,
Component: component,
Message: message,
Metadata: metadata,
CreatedAt: time.Now(),
}
// Buffer the event (best effort - don't fail if buffering fails)
if err := c.eventBuffer.BufferEvent(event); err != nil {
fmt.Printf("Warning: Failed to buffer event: %v\n", err)
}
}
// GetBufferedEvents returns all buffered events and clears the buffer
func (c *Client) GetBufferedEvents() ([]*models.SystemEvent, error) {
if c.eventBuffer == nil {
return nil, nil // Event buffering not enabled
}
return c.eventBuffer.GetBufferedEvents()
}
// addMachineIDHeader adds X-Machine-ID header to authenticated requests (v0.1.22+)
func (c *Client) addMachineIDHeader(req *http.Request) {
if c.machineID != "" {
@@ -95,11 +152,25 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
body, err := json.Marshal(req)
if err != nil {
// Buffer registration failure event
c.bufferEvent("registration_failure", "marshal_error", "error", "client",
fmt.Sprintf("Failed to marshal registration request: %v", err),
map[string]interface{}{
"error": err.Error(),
"hostname": req.Hostname,
})
return nil, err
}
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
// Buffer registration failure event
c.bufferEvent("registration_failure", "request_creation_error", "error", "client",
fmt.Sprintf("Failed to create registration request: %v", err),
map[string]interface{}{
"error": err.Error(),
"hostname": req.Hostname,
})
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
@@ -112,22 +183,49 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
resp, err := c.http.Do(httpReq)
if err != nil {
// Buffer registration failure event
c.bufferEvent("registration_failure", "network_error", "error", "client",
fmt.Sprintf("Registration request failed: %v", err),
map[string]interface{}{
"error": err.Error(),
"hostname": req.Hostname,
"server_url": c.baseURL,
})
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("registration failed: %s - %s", resp.Status, string(bodyBytes))
errorMsg := fmt.Sprintf("registration failed: %s - %s", resp.Status, string(bodyBytes))
// Buffer registration failure event
c.bufferEvent("registration_failure", "api_error", "error", "client",
errorMsg,
map[string]interface{}{
"status_code": resp.StatusCode,
"response_body": string(bodyBytes),
"hostname": req.Hostname,
"server_url": c.baseURL,
})
return nil, fmt.Errorf(errorMsg)
}
var result RegisterResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
// Buffer registration failure event
c.bufferEvent("registration_failure", "decode_error", "error", "client",
fmt.Sprintf("Failed to decode registration response: %v", err),
map[string]interface{}{
"error": err.Error(),
"hostname": req.Hostname,
})
return nil, err
}
// Update client token
// Update client token and agent ID
c.token = result.Token
c.agentID = result.AgentID
return &result, nil
}
@@ -136,6 +234,7 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
type TokenRenewalRequest struct {
AgentID uuid.UUID `json:"agent_id"`
RefreshToken string `json:"refresh_token"`
AgentVersion string `json:"agent_version,omitempty"` // Agent's current version for upgrade tracking
}
// TokenRenewalResponse is returned after successful token renewal
@@ -144,38 +243,79 @@ type TokenRenewalResponse struct {
}
// RenewToken uses refresh token to get a new access token (proper implementation)
func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string) error {
func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string, agentVersion string) error {
url := fmt.Sprintf("%s/api/v1/agents/renew", c.baseURL)
renewalReq := TokenRenewalRequest{
AgentID: agentID,
RefreshToken: refreshToken,
AgentVersion: agentVersion,
}
body, err := json.Marshal(renewalReq)
if err != nil {
// Buffer token renewal failure event
c.bufferEvent("token_renewal_failure", "marshal_error", "error", "client",
fmt.Sprintf("Failed to marshal token renewal request: %v", err),
map[string]interface{}{
"error": err.Error(),
"agent_id": agentID.String(),
})
return err
}
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
// Buffer token renewal failure event
c.bufferEvent("token_renewal_failure", "request_creation_error", "error", "client",
fmt.Sprintf("Failed to create token renewal request: %v", err),
map[string]interface{}{
"error": err.Error(),
"agent_id": agentID.String(),
})
return err
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(httpReq)
if err != nil {
// Buffer token renewal failure event
c.bufferEvent("token_renewal_failure", "network_error", "error", "client",
fmt.Sprintf("Token renewal request failed: %v", err),
map[string]interface{}{
"error": err.Error(),
"agent_id": agentID.String(),
"server_url": c.baseURL,
})
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("token renewal failed: %s - %s", resp.Status, string(bodyBytes))
errorMsg := fmt.Sprintf("token renewal failed: %s - %s", resp.Status, string(bodyBytes))
// Buffer token renewal failure event
c.bufferEvent("token_renewal_failure", "api_error", "error", "client",
errorMsg,
map[string]interface{}{
"status_code": resp.StatusCode,
"response_body": string(bodyBytes),
"agent_id": agentID.String(),
"server_url": c.baseURL,
})
return fmt.Errorf(errorMsg)
}
var result TokenRenewalResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
// Buffer token renewal failure event
c.bufferEvent("token_renewal_failure", "decode_error", "error", "client",
fmt.Sprintf("Failed to decode token renewal response: %v", err),
map[string]interface{}{
"error": err.Error(),
"agent_id": agentID.String(),
})
return err
}
@@ -187,11 +327,15 @@ func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string) error {
// Command represents a command from the server
type Command struct {
ID string `json:"id"`
Type string `json:"type"`
Params map[string]interface{} `json:"params"`
ID string `json:"id"`
Type string `json:"type"`
Params map[string]interface{} `json:"params"`
Signature string `json:"signature,omitempty"` // Ed25519 signature of the command
}
// CommandItem is an alias for Command for consistency with server models
type CommandItem = Command
// CommandsResponse contains pending commands
type CommandsResponse struct {
Commands []Command `json:"commands"`