feat: machine binding and version enforcement
migration 017 adds machine_id to agents table middleware validates X-Machine-ID header on authed routes agent client sends machine ID with requests MIN_AGENT_VERSION config defaults 0.1.22 version utils added for comparison blocks config copying attacks via hardware fingerprint old agents get 426 upgrade required breaking: <0.1.22 agents rejected
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -21,19 +22,36 @@ type Client struct {
|
||||
http *http.Client
|
||||
RapidPollingEnabled bool
|
||||
RapidPollingUntil time.Time
|
||||
machineID string // Cached machine ID for security binding
|
||||
}
|
||||
|
||||
// NewClient creates a new API client
|
||||
func NewClient(baseURL, token string) *Client {
|
||||
// Get machine ID for security binding (v0.1.22+)
|
||||
machineID, err := system.GetMachineID()
|
||||
if err != nil {
|
||||
// Log warning but don't fail - older servers may not require it
|
||||
fmt.Printf("Warning: Failed to get machine ID: %v\n", err)
|
||||
machineID = "" // Will be handled by server validation
|
||||
}
|
||||
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
token: token,
|
||||
baseURL: baseURL,
|
||||
token: token,
|
||||
machineID: machineID,
|
||||
http: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// addMachineIDHeader adds X-Machine-ID header to authenticated requests (v0.1.22+)
|
||||
func (c *Client) addMachineIDHeader(req *http.Request) {
|
||||
if c.machineID != "" {
|
||||
req.Header.Set("X-Machine-ID", c.machineID)
|
||||
}
|
||||
}
|
||||
|
||||
// GetToken returns the current JWT token
|
||||
func (c *Client) GetToken() string {
|
||||
return c.token
|
||||
@@ -46,13 +64,15 @@ func (c *Client) SetToken(token string) {
|
||||
|
||||
// RegisterRequest is the payload for agent registration
|
||||
type RegisterRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OSType string `json:"os_type"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
RegistrationToken string `json:"registration_token,omitempty"` // Fallback method
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Hostname string `json:"hostname"`
|
||||
OSType string `json:"os_type"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
RegistrationToken string `json:"registration_token,omitempty"` // Fallback method
|
||||
MachineID string `json:"machine_id"`
|
||||
PublicKeyFingerprint string `json:"public_key_fingerprint"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// RegisterResponse is returned after successful registration
|
||||
@@ -230,6 +250,7 @@ func (c *Client) GetCommands(agentID uuid.UUID, metrics *SystemMetrics) (*Comman
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -297,6 +318,7 @@ func (c *Client) ReportUpdates(agentID uuid.UUID, report UpdateReport) error {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -314,13 +336,14 @@ func (c *Client) ReportUpdates(agentID uuid.UUID, report UpdateReport) error {
|
||||
|
||||
// LogReport represents an execution log
|
||||
type LogReport struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Action string `json:"action"`
|
||||
Result string `json:"result"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
DurationSeconds int `json:"duration_seconds"`
|
||||
CommandID string `json:"command_id"`
|
||||
Action string `json:"action"`
|
||||
Result string `json:"result"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
DurationSeconds int `json:"duration_seconds"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ReportLog sends an execution log to the server
|
||||
@@ -338,6 +361,7 @@ func (c *Client) ReportLog(agentID uuid.UUID, report LogReport) error {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -392,6 +416,7 @@ func (c *Client) ReportDependencies(agentID uuid.UUID, report DependencyReport)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -437,6 +462,7 @@ func (c *Client) ReportSystemInfo(agentID uuid.UUID, report SystemInfoReport) er
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -474,6 +500,49 @@ func DetectSystem() (osType, osVersion, osArch string) {
|
||||
return
|
||||
}
|
||||
|
||||
// AgentInfo represents agent information from the server
|
||||
type AgentInfo struct {
|
||||
ID string `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
OSType string `json:"os_type"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
LastCheckIn string `json:"last_check_in"`
|
||||
}
|
||||
|
||||
// GetAgent retrieves agent information from the server
|
||||
func (c *Client) GetAgent(agentID string) (*AgentInfo, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/agents/%s", c.baseURL, agentID)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var agent AgentInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&agent); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &agent, nil
|
||||
}
|
||||
|
||||
// parseOSRelease parses /etc/os-release to get proper distro name
|
||||
func parseOSRelease(data []byte) string {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
Reference in New Issue
Block a user