Complete RedFlag codebase with two major security audit implementations.
== A-1: Ed25519 Key Rotation Support ==
Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management
Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing
== A-2: Replay Attack Fixes (F-1 through F-7) ==
F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH - Migration 026: expires_at column with partial index
F-6 HIGH - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH - Agent-side executedIDs dedup map with cleanup
F-4 HIGH - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt
Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.
All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
810 lines
24 KiB
Plaintext
810 lines
24 KiB
Plaintext
package services
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// AgentTemplate defines a template for different agent types
|
|
type AgentTemplate struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
BaseConfig map[string]interface{} `json:"base_config"`
|
|
Secrets []string `json:"required_secrets"`
|
|
Validation ValidationRules `json:"validation"`
|
|
}
|
|
|
|
// ValidationRules defines validation rules for configuration
|
|
type ValidationRules struct {
|
|
RequiredFields []string `json:"required_fields"`
|
|
AllowedValues map[string][]string `json:"allowed_values"`
|
|
Patterns map[string]string `json:"patterns"`
|
|
Constraints map[string]interface{} `json:"constraints"`
|
|
}
|
|
|
|
// PublicKeyResponse represents the server's public key response
|
|
type PublicKeyResponse struct {
|
|
PublicKey string `json:"public_key"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
Algorithm string `json:"algorithm"`
|
|
KeySize int `json:"key_size"`
|
|
}
|
|
|
|
// ConfigBuilder handles dynamic agent configuration generation
|
|
type ConfigBuilder struct {
|
|
serverURL string
|
|
templates map[string]AgentTemplate
|
|
httpClient *http.Client
|
|
publicKeyCache map[string]string
|
|
scannerConfigQ *queries.ScannerConfigQueries
|
|
}
|
|
|
|
// NewConfigBuilder creates a new configuration builder
|
|
func NewConfigBuilder(serverURL string, db queries.DBInterface) *ConfigBuilder {
|
|
return &ConfigBuilder{
|
|
serverURL: serverURL,
|
|
templates: getAgentTemplates(),
|
|
httpClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
publicKeyCache: make(map[string]string),
|
|
scannerConfigQ: queries.NewScannerConfigQueries(db),
|
|
}
|
|
}
|
|
|
|
// AgentSetupRequest represents a request to set up a new agent
|
|
type AgentSetupRequest struct {
|
|
ServerURL string `json:"server_url" binding:"required"`
|
|
Environment string `json:"environment" binding:"required"`
|
|
AgentType string `json:"agent_type" binding:"required,oneof=linux-server windows-workstation docker-host"`
|
|
Organization string `json:"organization" binding:"required"`
|
|
CustomSettings map[string]interface{} `json:"custom_settings,omitempty"`
|
|
DeploymentID string `json:"deployment_id,omitempty"`
|
|
AgentID string `json:"agent_id,omitempty"` // Optional: existing agent ID for upgrades
|
|
}
|
|
|
|
// BuildAgentConfig builds a complete agent configuration
|
|
func (cb *ConfigBuilder) BuildAgentConfig(req AgentSetupRequest) (*AgentConfiguration, error) {
|
|
// Validate request
|
|
if err := cb.validateRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine agent ID - use existing if provided and valid, otherwise generate new
|
|
agentID := cb.determineAgentID(req.AgentID)
|
|
|
|
// Fetch server public key
|
|
serverPublicKey, err := cb.fetchServerPublicKey(req.ServerURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch server public key: %w", err)
|
|
}
|
|
|
|
// Generate registration token
|
|
registrationToken, err := cb.generateRegistrationToken(agentID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate registration token: %w", err)
|
|
}
|
|
|
|
// Get template
|
|
template, exists := cb.templates[req.AgentType]
|
|
if !exists {
|
|
return nil, fmt.Errorf("unknown agent type: %s", req.AgentType)
|
|
}
|
|
|
|
// Build base configuration
|
|
config := cb.buildFromTemplate(template, req.CustomSettings)
|
|
|
|
// Override scanner timeouts from database (user-configurable)
|
|
cb.overrideScannerTimeoutsFromDB(config)
|
|
|
|
// Inject deployment-specific values
|
|
cb.injectDeploymentValues(config, req, agentID, registrationToken, serverPublicKey)
|
|
|
|
// Apply environment-specific defaults
|
|
cb.applyEnvironmentDefaults(config, req.Environment)
|
|
// Validate request
|
|
if err := cb.validateRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine agent ID - use existing if provided and valid, otherwise generate new
|
|
agentID := cb.determineAgentID(req.AgentID)
|
|
|
|
// Fetch server public key
|
|
serverPublicKey, err := cb.fetchServerPublicKey(req.ServerURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch server public key: %w", err)
|
|
}
|
|
|
|
// Generate registration token
|
|
registrationToken, err := cb.generateRegistrationToken(agentID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate registration token: %w", err)
|
|
}
|
|
|
|
// Get template
|
|
template, exists := cb.templates[req.AgentType]
|
|
if !exists {
|
|
return nil, fmt.Errorf("unknown agent type: %s", req.AgentType)
|
|
}
|
|
|
|
// Build base configuration
|
|
config := cb.buildFromTemplate(template, req.CustomSettings)
|
|
|
|
// Inject deployment-specific values
|
|
cb.injectDeploymentValues(config, req, agentID, registrationToken, serverPublicKey)
|
|
|
|
// Apply environment-specific defaults
|
|
cb.applyEnvironmentDefaults(config, req.Environment)
|
|
|
|
// Validate final configuration
|
|
if err := cb.validateConfiguration(config, template); err != nil {
|
|
return nil, fmt.Errorf("configuration validation failed: %w", err)
|
|
}
|
|
|
|
// Separate sensitive and non-sensitive data
|
|
publicConfig, secrets := cb.separateSecrets(config)
|
|
|
|
// Create Docker secrets if needed
|
|
var secretsCreated bool
|
|
var secretsPath string
|
|
if len(secrets) > 0 {
|
|
secretsManager := NewSecretsManager()
|
|
|
|
// Generate encryption key if not set
|
|
if secretsManager.GetEncryptionKey() == "" {
|
|
key, err := secretsManager.GenerateEncryptionKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate encryption key: %w", err)
|
|
}
|
|
secretsManager.SetEncryptionKey(key)
|
|
}
|
|
|
|
// Create Docker secrets
|
|
if err := secretsManager.CreateDockerSecrets(secrets); err != nil {
|
|
return nil, fmt.Errorf("failed to create Docker secrets: %w", err)
|
|
}
|
|
|
|
secretsCreated = true
|
|
secretsPath = secretsManager.GetSecretsPath()
|
|
}
|
|
|
|
// Determine platform from agent type
|
|
platform := "linux-amd64" // Default
|
|
if req.AgentType == "windows-workstation" {
|
|
platform = "windows-amd64"
|
|
}
|
|
|
|
return &AgentConfiguration{
|
|
AgentID: agentID,
|
|
PublicConfig: publicConfig,
|
|
Secrets: secrets,
|
|
Template: req.AgentType,
|
|
Environment: req.Environment,
|
|
ServerURL: req.ServerURL,
|
|
Organization: req.Organization,
|
|
Platform: platform,
|
|
ConfigVersion: "5", // Config schema version
|
|
AgentVersion: "0.1.23.6", // Agent binary version
|
|
BuildTime: time.Now(),
|
|
SecretsCreated: secretsCreated,
|
|
SecretsPath: secretsPath,
|
|
}, nil
|
|
}
|
|
|
|
// AgentConfiguration represents a complete agent configuration
|
|
type AgentConfiguration struct {
|
|
AgentID string `json:"agent_id"`
|
|
PublicConfig map[string]interface{} `json:"public_config"`
|
|
Secrets map[string]string `json:"secrets"`
|
|
Template string `json:"template"`
|
|
Environment string `json:"environment"`
|
|
ServerURL string `json:"server_url"`
|
|
Organization string `json:"organization"`
|
|
Platform string `json:"platform"`
|
|
ConfigVersion string `json:"config_version"` // Config schema version (e.g., "5")
|
|
AgentVersion string `json:"agent_version"` // Agent binary version (e.g., "0.1.23.6")
|
|
BuildTime time.Time `json:"build_time"`
|
|
SecretsCreated bool `json:"secrets_created"`
|
|
SecretsPath string `json:"secrets_path,omitempty"`
|
|
}
|
|
|
|
// validateRequest validates the setup request
|
|
func (cb *ConfigBuilder) validateRequest(req AgentSetupRequest) error {
|
|
if req.ServerURL == "" {
|
|
return fmt.Errorf("server_url is required")
|
|
}
|
|
|
|
if req.Environment == "" {
|
|
return fmt.Errorf("environment is required")
|
|
}
|
|
|
|
if req.AgentType == "" {
|
|
return fmt.Errorf("agent_type is required")
|
|
}
|
|
|
|
if req.Organization == "" {
|
|
return fmt.Errorf("organization is required")
|
|
}
|
|
|
|
// Check if agent type exists
|
|
if _, exists := cb.templates[req.AgentType]; !exists {
|
|
return fmt.Errorf("unknown agent type: %s", req.AgentType)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// fetchServerPublicKey fetches the server's public key with caching
|
|
func (cb *ConfigBuilder) fetchServerPublicKey(serverURL string) (string, error) {
|
|
// Check cache first
|
|
if cached, exists := cb.publicKeyCache[serverURL]; exists {
|
|
return cached, nil
|
|
}
|
|
|
|
// Fetch from server
|
|
resp, err := cb.httpClient.Get(serverURL + "/api/v1/public-key")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to fetch public key: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("server returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var keyResp PublicKeyResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&keyResp); err != nil {
|
|
return "", fmt.Errorf("failed to decode public key response: %w", err)
|
|
}
|
|
|
|
// Cache the key
|
|
cb.publicKeyCache[serverURL] = keyResp.PublicKey
|
|
|
|
return keyResp.PublicKey, nil
|
|
}
|
|
|
|
// generateRegistrationToken generates a secure registration token
|
|
func (cb *ConfigBuilder) generateRegistrationToken(agentID string) (string, error) {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Combine agent ID with random bytes for uniqueness
|
|
data := append([]byte(agentID), bytes...)
|
|
token := hex.EncodeToString(data)
|
|
|
|
// Ensure token doesn't exceed reasonable length
|
|
if len(token) > 128 {
|
|
token = token[:128]
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// buildFromTemplate builds configuration from template
|
|
func (cb *ConfigBuilder) buildFromTemplate(template AgentTemplate, customSettings map[string]interface{}) map[string]interface{} {
|
|
config := make(map[string]interface{})
|
|
|
|
// Deep copy base configuration
|
|
for k, v := range template.BaseConfig {
|
|
config[k] = cb.deepCopy(v)
|
|
}
|
|
|
|
// Apply custom settings
|
|
if customSettings != nil {
|
|
cb.mergeSettings(config, customSettings)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// injectDeploymentValues injects deployment-specific values into configuration
|
|
func (cb *ConfigBuilder) injectDeploymentValues(config map[string]interface{}, req AgentSetupRequest, agentID, registrationToken, serverPublicKey string) {
|
|
config["version"] = "5" // Config schema version (for migration system)
|
|
config["agent_version"] = "0.1.23.6" // Agent binary version (MUST match the binary being served)
|
|
config["server_url"] = req.ServerURL
|
|
config["agent_id"] = agentID
|
|
config["registration_token"] = registrationToken
|
|
config["server_public_key"] = serverPublicKey
|
|
config["organization"] = req.Organization
|
|
config["environment"] = req.Environment
|
|
config["agent_type"] = req.AgentType
|
|
|
|
if req.DeploymentID != "" {
|
|
config["deployment_id"] = req.DeploymentID
|
|
}
|
|
}
|
|
|
|
// determineAgentID checks if an existing agent ID is provided and valid, otherwise generates new
|
|
func (cb *ConfigBuilder) determineAgentID(providedAgentID string) string {
|
|
if providedAgentID != "" {
|
|
// Validate it's a proper UUID
|
|
if _, err := uuid.Parse(providedAgentID); err == nil {
|
|
return providedAgentID
|
|
}
|
|
}
|
|
// Generate new UUID if none provided or invalid
|
|
return uuid.New().String()
|
|
}
|
|
|
|
// applyEnvironmentDefaults applies environment-specific configuration defaults
|
|
func (cb *ConfigBuilder) applyEnvironmentDefaults(config map[string]interface{}, environment string) {
|
|
environmentDefaults := map[string]interface{}{
|
|
"development": map[string]interface{}{
|
|
"logging": map[string]interface{}{
|
|
"level": "debug",
|
|
"max_size": 50,
|
|
"max_backups": 2,
|
|
"max_age": 7,
|
|
},
|
|
"check_in_interval": 60, // More frequent polling in development
|
|
},
|
|
"staging": map[string]interface{}{
|
|
"logging": map[string]interface{}{
|
|
"level": "info",
|
|
"max_size": 100,
|
|
"max_backups": 3,
|
|
"max_age": 14,
|
|
},
|
|
"check_in_interval": 180,
|
|
},
|
|
"production": map[string]interface{}{
|
|
"logging": map[string]interface{}{
|
|
"level": "warn",
|
|
"max_size": 200,
|
|
"max_backups": 5,
|
|
"max_age": 30,
|
|
},
|
|
"check_in_interval": 300, // 5 minutes for production
|
|
},
|
|
"testing": map[string]interface{}{
|
|
"logging": map[string]interface{}{
|
|
"level": "debug",
|
|
"max_size": 10,
|
|
"max_backups": 1,
|
|
"max_age": 1,
|
|
},
|
|
"check_in_interval": 30, // Very frequent for testing
|
|
},
|
|
}
|
|
|
|
if defaults, exists := environmentDefaults[environment]; exists {
|
|
if defaultsMap, ok := defaults.(map[string]interface{}); ok {
|
|
cb.mergeSettings(config, defaultsMap)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateConfiguration validates the final configuration
|
|
func (cb *ConfigBuilder) validateConfiguration(config map[string]interface{}, template AgentTemplate) error {
|
|
// Check required fields
|
|
for _, field := range template.Validation.RequiredFields {
|
|
if _, exists := config[field]; !exists {
|
|
return fmt.Errorf("required field missing: %s", field)
|
|
}
|
|
}
|
|
|
|
// Validate allowed values
|
|
for field, allowedValues := range template.Validation.AllowedValues {
|
|
if value, exists := config[field]; exists {
|
|
if strValue, ok := value.(string); ok {
|
|
if !cb.containsString(allowedValues, strValue) {
|
|
return fmt.Errorf("invalid value for %s: %s (allowed: %v)", field, strValue, allowedValues)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate constraints
|
|
for field, constraint := range template.Validation.Constraints {
|
|
if value, exists := config[field]; exists {
|
|
if err := cb.validateConstraint(field, value, constraint); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// separateSecrets separates sensitive data from public configuration
|
|
func (cb *ConfigBuilder) separateSecrets(config map[string]interface{}) (map[string]interface{}, map[string]string) {
|
|
publicConfig := make(map[string]interface{})
|
|
secrets := make(map[string]string)
|
|
|
|
// Copy all values to public config initially
|
|
for k, v := range config {
|
|
publicConfig[k] = cb.deepCopy(v)
|
|
}
|
|
|
|
// Extract known sensitive fields
|
|
sensitiveFields := []string{
|
|
"registration_token",
|
|
"server_public_key",
|
|
}
|
|
|
|
for _, field := range sensitiveFields {
|
|
if value, exists := publicConfig[field]; exists {
|
|
if strValue, ok := value.(string); ok {
|
|
secrets[field] = strValue
|
|
delete(publicConfig, field)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract nested sensitive fields
|
|
if proxy, exists := publicConfig["proxy"].(map[string]interface{}); exists {
|
|
if username, exists := proxy["username"].(string); exists && username != "" {
|
|
secrets["proxy_username"] = username
|
|
delete(proxy, "username")
|
|
}
|
|
if password, exists := proxy["password"].(string); exists && password != "" {
|
|
secrets["proxy_password"] = password
|
|
delete(proxy, "password")
|
|
}
|
|
}
|
|
|
|
if tls, exists := publicConfig["tls"].(map[string]interface{}); exists {
|
|
if certFile, exists := tls["cert_file"].(string); exists && certFile != "" {
|
|
secrets["tls_cert"] = certFile
|
|
delete(tls, "cert_file")
|
|
}
|
|
if keyFile, exists := tls["key_file"].(string); exists && keyFile != "" {
|
|
secrets["tls_key"] = keyFile
|
|
delete(tls, "key_file")
|
|
}
|
|
if caFile, exists := tls["ca_file"].(string); exists && caFile != "" {
|
|
secrets["tls_ca"] = caFile
|
|
delete(tls, "ca_file")
|
|
}
|
|
}
|
|
|
|
return publicConfig, secrets
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func (cb *ConfigBuilder) deepCopy(value interface{}) interface{} {
|
|
if m, ok := value.(map[string]interface{}); ok {
|
|
result := make(map[string]interface{})
|
|
for k, v := range m {
|
|
result[k] = cb.deepCopy(v)
|
|
}
|
|
return result
|
|
}
|
|
if s, ok := value.([]interface{}); ok {
|
|
result := make([]interface{}, len(s))
|
|
for i, v := range s {
|
|
result[i] = cb.deepCopy(v)
|
|
}
|
|
return result
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (cb *ConfigBuilder) mergeSettings(target map[string]interface{}, source map[string]interface{}) {
|
|
for key, value := range source {
|
|
if existing, exists := target[key]; exists {
|
|
if existingMap, ok := existing.(map[string]interface{}); ok {
|
|
if sourceMap, ok := value.(map[string]interface{}); ok {
|
|
cb.mergeSettings(existingMap, sourceMap)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
target[key] = cb.deepCopy(value)
|
|
}
|
|
}
|
|
|
|
func (cb *ConfigBuilder) containsString(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetTemplates returns the available agent templates
|
|
func (cb *ConfigBuilder) GetTemplates() map[string]AgentTemplate {
|
|
return getAgentTemplates()
|
|
}
|
|
|
|
// GetTemplate returns a specific agent template
|
|
func (cb *ConfigBuilder) GetTemplate(agentType string) (AgentTemplate, bool) {
|
|
template, exists := getAgentTemplates()[agentType]
|
|
return template, exists
|
|
}
|
|
|
|
func (cb *ConfigBuilder) validateConstraint(field string, value interface{}, constraint interface{}) error {
|
|
constraints, ok := constraint.(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if numValue, ok := value.(float64); ok {
|
|
if min, exists := constraints["min"].(float64); exists && numValue < min {
|
|
return fmt.Errorf("value for %s is below minimum: %f < %f", field, numValue, min)
|
|
}
|
|
if max, exists := constraints["max"].(float64); exists && numValue > max {
|
|
return fmt.Errorf("value for %s is above maximum: %f > %f", field, numValue, max)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getAgentTemplates returns the available agent templates
|
|
// overrideScannerTimeoutsFromDB overrides scanner timeouts with values from database
|
|
// This allows users to configure scanner timeouts via the web UI
|
|
func (cb *ConfigBuilder) overrideScannerTimeoutsFromDB(config map[string]interface{}) {
|
|
if cb.scannerConfigQ == nil {
|
|
// No database connection, use defaults
|
|
return
|
|
}
|
|
|
|
// Get subsystems section
|
|
subsystems, exists := config["subsystems"].(map[string]interface{})
|
|
if !exists {
|
|
return
|
|
}
|
|
|
|
// List of scanners that can have configurable timeouts
|
|
scannerNames := []string{"apt", "dnf", "docker", "windows", "winget", "system", "storage", "updates"}
|
|
|
|
for _, scannerName := range scannerNames {
|
|
scannerConfig, exists := subsystems[scannerName].(map[string]interface{})
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
// Get timeout from database
|
|
timeout := cb.scannerConfigQ.GetScannerTimeoutWithDefault(scannerName, 30*time.Minute)
|
|
scannerConfig["timeout"] = int(timeout.Nanoseconds())
|
|
}
|
|
}
|
|
|
|
func getAgentTemplates() map[string]AgentTemplate {
|
|
return map[string]AgentTemplate{
|
|
"linux-server": {
|
|
Name: "Linux Server Agent",
|
|
Description: "Optimized for Linux server deployments with package management",
|
|
BaseConfig: map[string]interface{}{
|
|
"check_in_interval": 300,
|
|
"network": map[string]interface{}{
|
|
"timeout": 30000000000,
|
|
"retry_count": 3,
|
|
"retry_delay": 5000000000,
|
|
"max_idle_conn": 10,
|
|
},
|
|
"proxy": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"tls": map[string]interface{}{
|
|
"insecure_skip_verify": false,
|
|
},
|
|
"logging": map[string]interface{}{
|
|
"level": "info",
|
|
"max_size": 100,
|
|
"max_backups": 3,
|
|
"max_age": 28,
|
|
},
|
|
"subsystems": map[string]interface{}{
|
|
"apt": map[string]interface{}{
|
|
"enabled": true,
|
|
"timeout": 30000000000,
|
|
"circuit_breaker": map[string]interface{}{
|
|
"enabled": true,
|
|
"failure_threshold": 3,
|
|
"failure_window": 600000000000,
|
|
"open_duration": 1800000000000,
|
|
"half_open_attempts": 2,
|
|
},
|
|
},
|
|
"dnf": map[string]interface{}{
|
|
"enabled": true,
|
|
"timeout": 1800000000000, // 30 minutes - configurable via server settings
|
|
"circuit_breaker": map[string]interface{}{
|
|
"enabled": true,
|
|
"failure_threshold": 3,
|
|
"failure_window": 600000000000,
|
|
"open_duration": 1800000000000,
|
|
"half_open_attempts": 2,
|
|
},
|
|
},
|
|
"docker": map[string]interface{}{
|
|
"enabled": true,
|
|
"timeout": 60000000000,
|
|
"circuit_breaker": map[string]interface{}{
|
|
"enabled": true,
|
|
"failure_threshold": 3,
|
|
"failure_window": 600000000000,
|
|
"open_duration": 1800000000000,
|
|
"half_open_attempts": 2,
|
|
},
|
|
},
|
|
"windows": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"winget": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"storage": map[string]interface{}{
|
|
"enabled": true,
|
|
"timeout": 10000000000,
|
|
"circuit_breaker": map[string]interface{}{
|
|
"enabled": true,
|
|
"failure_threshold": 3,
|
|
"failure_window": 600000000000,
|
|
"open_duration": 1800000000000,
|
|
"half_open_attempts": 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Secrets: []string{"registration_token", "server_public_key"},
|
|
Validation: ValidationRules{
|
|
RequiredFields: []string{"server_url", "organization"},
|
|
AllowedValues: map[string][]string{
|
|
"environment": {"development", "staging", "production", "testing"},
|
|
},
|
|
Patterns: map[string]string{
|
|
"server_url": "^https?://.+",
|
|
},
|
|
Constraints: map[string]interface{}{
|
|
"check_in_interval": map[string]interface{}{"min": 30, "max": 3600},
|
|
},
|
|
},
|
|
},
|
|
"windows-workstation": {
|
|
Name: "Windows Workstation Agent",
|
|
Description: "Optimized for Windows workstation deployments",
|
|
BaseConfig: map[string]interface{}{
|
|
"check_in_interval": 300,
|
|
"network": map[string]interface{}{
|
|
"timeout": 30000000000,
|
|
"retry_count": 3,
|
|
"retry_delay": 5000000000,
|
|
"max_idle_conn": 10,
|
|
},
|
|
"proxy": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"tls": map[string]interface{}{
|
|
"insecure_skip_verify": false,
|
|
},
|
|
"logging": map[string]interface{}{
|
|
"level": "info",
|
|
"max_size": 100,
|
|
"max_backups": 3,
|
|
"max_age": 28,
|
|
},
|
|
"subsystems": map[string]interface{}{
|
|
"apt": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"dnf": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"docker": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"windows": map[string]interface{}{
|
|
"enabled": true,
|
|
"timeout": 600000000000,
|
|
"circuit_breaker": map[string]interface{}{
|
|
"enabled": true,
|
|
"failure_threshold": 2,
|
|
"failure_window": 900000000000,
|
|
"open_duration": 3600000000000,
|
|
"half_open_attempts": 3,
|
|
},
|
|
},
|
|
"winget": map[string]interface{}{
|
|
"enabled": true,
|
|
"timeout": 120000000000,
|
|
"circuit_breaker": map[string]interface{}{
|
|
"enabled": true,
|
|
"failure_threshold": 3,
|
|
"failure_window": 600000000000,
|
|
"open_duration": 1800000000000,
|
|
"half_open_attempts": 2,
|
|
},
|
|
},
|
|
"storage": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
},
|
|
},
|
|
Secrets: []string{"registration_token", "server_public_key"},
|
|
Validation: ValidationRules{
|
|
RequiredFields: []string{"server_url", "organization"},
|
|
AllowedValues: map[string][]string{
|
|
"environment": {"development", "staging", "production", "testing"},
|
|
},
|
|
Patterns: map[string]string{
|
|
"server_url": "^https?://.+",
|
|
},
|
|
Constraints: map[string]interface{}{
|
|
"check_in_interval": map[string]interface{}{"min": 30, "max": 3600},
|
|
},
|
|
},
|
|
},
|
|
"docker-host": {
|
|
Name: "Docker Host Agent",
|
|
Description: "Optimized for Docker host deployments",
|
|
BaseConfig: map[string]interface{}{
|
|
"check_in_interval": 300,
|
|
"network": map[string]interface{}{
|
|
"timeout": 30000000000,
|
|
"retry_count": 3,
|
|
"retry_delay": 5000000000,
|
|
"max_idle_conn": 10,
|
|
},
|
|
"proxy": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"tls": map[string]interface{}{
|
|
"insecure_skip_verify": false,
|
|
},
|
|
"logging": map[string]interface{}{
|
|
"level": "info",
|
|
"max_size": 100,
|
|
"max_backups": 3,
|
|
"max_age": 28,
|
|
},
|
|
"subsystems": map[string]interface{}{
|
|
"apt": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"dnf": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"docker": map[string]interface{}{
|
|
"enabled": true,
|
|
"timeout": 60000000000,
|
|
"circuit_breaker": map[string]interface{}{
|
|
"enabled": true,
|
|
"failure_threshold": 3,
|
|
"failure_window": 600000000000,
|
|
"open_duration": 1800000000000,
|
|
"half_open_attempts": 2,
|
|
},
|
|
},
|
|
"windows": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"winget": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
"storage": map[string]interface{}{
|
|
"enabled": false,
|
|
},
|
|
},
|
|
},
|
|
Secrets: []string{"registration_token", "server_public_key"},
|
|
Validation: ValidationRules{
|
|
RequiredFields: []string{"server_url", "organization"},
|
|
AllowedValues: map[string][]string{
|
|
"environment": {"development", "staging", "production", "testing"},
|
|
},
|
|
Patterns: map[string]string{
|
|
"server_url": "^https?://.+",
|
|
},
|
|
Constraints: map[string]interface{}{
|
|
"check_in_interval": map[string]interface{}{"min": 30, "max": 3600},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|