|
1 | 1 | package validations |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "encoding/json" |
4 | 5 | "fmt" |
| 6 | + "maps" |
5 | 7 | "net/http" |
| 8 | + "strings" |
6 | 9 |
|
7 | 10 | "github.com/threatwinds/go-sdk/utils" |
8 | 11 | "github.com/utmstack/UTMStack/plugins/modules-config/config" |
9 | 12 | ) |
10 | 13 |
|
11 | | -func ValidateSOCAIConfig(config *config.ModuleGroup) error { |
12 | | - var apiKey, provider string |
| 14 | +// isAnthropicProvider detects if the URL is for Anthropic API |
| 15 | +func isAnthropicProvider(url string) bool { |
| 16 | + return strings.Contains(url, "anthropic.com") |
| 17 | +} |
| 18 | + |
| 19 | +// SOCAIConfig holds the parsed SOC-AI configuration |
| 20 | +type SOCAIConfig struct { |
| 21 | + AutoAnalyze bool |
| 22 | + IncidentCreation bool |
| 23 | + ChangeAlertStatus bool |
| 24 | + URL string |
| 25 | + Model string |
| 26 | + AuthType string // "custom-headers", "none" |
| 27 | + MaxTokens string |
| 28 | + CustomHeaders map[string]string // All headers including auth (from frontend) |
| 29 | +} |
13 | 30 |
|
14 | | - if config == nil { |
| 31 | +func ValidateSOCAIConfig(cfg *config.ModuleGroup) error { |
| 32 | + if cfg == nil { |
15 | 33 | return fmt.Errorf("SOC_AI configuration is nil") |
16 | 34 | } |
17 | 35 |
|
18 | | - for _, cnf := range config.ModuleGroupConfigurations { |
19 | | - switch { |
20 | | - case cnf.ConfKey == "utmstack.socai.key": |
21 | | - apiKey = cnf.ConfValue |
22 | | - case cnf.ConfKey == "utmstack.socai.provider": |
23 | | - provider = cnf.ConfValue |
24 | | - } |
| 36 | + socai := parseSOCAIConfig(cfg) |
| 37 | + |
| 38 | + // Validate required fields |
| 39 | + if socai.URL == "" { |
| 40 | + return fmt.Errorf("URL is required in SOC_AI configuration") |
| 41 | + } |
| 42 | + if socai.Model == "" { |
| 43 | + return fmt.Errorf("Model is required in SOC_AI configuration") |
25 | 44 | } |
26 | 45 |
|
27 | | - if apiKey == "" { |
28 | | - return fmt.Errorf("API Key is required in SOC_AI configuration") |
| 46 | + // Validate authType (optional - defaults to "none" if not specified) |
| 47 | + if socai.AuthType == "" { |
| 48 | + socai.AuthType = "none" |
29 | 49 | } |
30 | | - if provider == "" { |
31 | | - return fmt.Errorf("Provider is required in SOC_AI configuration") |
32 | | - } else if provider != "openai" { |
33 | | - return nil |
| 50 | + if socai.AuthType != "custom-headers" && socai.AuthType != "none" { |
| 51 | + return fmt.Errorf("invalid authType '%s', must be 'custom-headers' or 'none'", socai.AuthType) |
34 | 52 | } |
35 | 53 |
|
36 | | - url := "https://api.openai.com/v1/chat/completions" |
37 | | - headers := map[string]string{ |
38 | | - "Authorization": fmt.Sprintf("Bearer %s", apiKey), |
39 | | - "Content-Type": "application/json", |
| 54 | + // Validate required fields based on authType |
| 55 | + if socai.AuthType == "custom-headers" && len(socai.CustomHeaders) == 0 { |
| 56 | + return fmt.Errorf("Custom Headers are required when authType is 'custom-headers'") |
40 | 57 | } |
41 | 58 |
|
42 | | - response, status, err := utils.DoReq[map[string]any](url, nil, "GET", headers, false) |
43 | | - if err != nil || status != http.StatusOK { |
44 | | - if status == http.StatusRequestTimeout { |
45 | | - return fmt.Errorf("SOC_AI connection timed out") |
46 | | - } |
47 | | - if status == http.StatusUnauthorized { |
48 | | - return fmt.Errorf("SOC_AI API Key is invalid") |
49 | | - } |
50 | | - fmt.Printf("Error validating SOC_AI connection: %v, status code: %d, response: %v\n", err, status, response) |
51 | | - return fmt.Errorf("SOC_AI API Key is invalid") |
| 59 | + // Anthropic requires maxTokens |
| 60 | + if isAnthropicProvider(socai.URL) && socai.MaxTokens == "" { |
| 61 | + return fmt.Errorf("Max Tokens is required for Anthropic provider") |
| 62 | + } |
| 63 | + |
| 64 | + // Test connection |
| 65 | + if err := testSOCAIConnection(socai); err != nil { |
| 66 | + return err |
52 | 67 | } |
53 | 68 |
|
54 | 69 | return nil |
55 | 70 | } |
| 71 | + |
| 72 | +func parseSOCAIConfig(cfg *config.ModuleGroup) SOCAIConfig { |
| 73 | + socai := SOCAIConfig{ |
| 74 | + AuthType: "none", // default - auth via custom headers or apiKey |
| 75 | + CustomHeaders: make(map[string]string), |
| 76 | + } |
| 77 | + |
| 78 | + for _, cnf := range cfg.ModuleGroupConfigurations { |
| 79 | + switch cnf.ConfKey { |
| 80 | + case "utmstack.socai.autoAnalyze": |
| 81 | + socai.AutoAnalyze = cnf.ConfValue == "true" |
| 82 | + case "utmstack.socai.incidentCreation": |
| 83 | + socai.IncidentCreation = cnf.ConfValue == "true" |
| 84 | + case "utmstack.socai.changeAlertStatus": |
| 85 | + socai.ChangeAlertStatus = cnf.ConfValue == "true" |
| 86 | + case "utmstack.socai.url": |
| 87 | + socai.URL = cnf.ConfValue |
| 88 | + case "utmstack.socai.model": |
| 89 | + socai.Model = cnf.ConfValue |
| 90 | + case "utmstack.socai.authType": |
| 91 | + if cnf.ConfValue != "" { |
| 92 | + socai.AuthType = cnf.ConfValue |
| 93 | + } |
| 94 | + case "utmstack.socai.maxTokens": |
| 95 | + socai.MaxTokens = cnf.ConfValue |
| 96 | + case "utmstack.socai.customHeaders": |
| 97 | + if cnf.ConfValue != "" { |
| 98 | + if err := json.Unmarshal([]byte(cnf.ConfValue), &socai.CustomHeaders); err != nil { |
| 99 | + fmt.Printf("Warning: Failed to parse customHeaders JSON: %v\n", err) |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + return socai |
| 106 | +} |
| 107 | + |
| 108 | +func testSOCAIConnection(socai SOCAIConfig) error { |
| 109 | + headers := map[string]string{ |
| 110 | + "Content-Type": "application/json", |
| 111 | + } |
| 112 | + |
| 113 | + // Add custom headers (includes auth headers configured by frontend) |
| 114 | + if socai.AuthType == "custom-headers" { |
| 115 | + maps.Copy(headers, socai.CustomHeaders) |
| 116 | + } |
| 117 | + // If authType is "none", no additional headers are added |
| 118 | + |
| 119 | + // Test connection with GET request (most APIs return error but validate auth) |
| 120 | + response, status, err := utils.DoReq[map[string]any](socai.URL, nil, "GET", headers, false) |
| 121 | + |
| 122 | + // Handle response |
| 123 | + switch status { |
| 124 | + case http.StatusOK, http.StatusMethodNotAllowed, http.StatusBadRequest: |
| 125 | + // These are acceptable - means we reached the API and auth worked |
| 126 | + // 405 = endpoint doesn't accept GET but auth passed |
| 127 | + // 400 = bad request but auth passed |
| 128 | + return nil |
| 129 | + case http.StatusUnauthorized: |
| 130 | + return fmt.Errorf("SOC_AI API Key is invalid (401 Unauthorized)") |
| 131 | + case http.StatusForbidden: |
| 132 | + return fmt.Errorf("SOC_AI API Key does not have permission (403 Forbidden)") |
| 133 | + case http.StatusRequestTimeout: |
| 134 | + return fmt.Errorf("SOC_AI connection timed out") |
| 135 | + case http.StatusNotFound: |
| 136 | + return fmt.Errorf("SOC_AI URL not found (404) - check the URL is correct") |
| 137 | + default: |
| 138 | + if err != nil { |
| 139 | + return fmt.Errorf("SOC_AI connection failed: %v", err) |
| 140 | + } |
| 141 | + fmt.Printf("SOC_AI validation: status %d, response: %v\n", status, response) |
| 142 | + return nil // Accept other status codes as potentially valid |
| 143 | + } |
| 144 | +} |
0 commit comments