Skip to content

Commit 0baab17

Browse files
committed
feat(soc-ai): add multi-provider LLM support and HTTP API for manual analysis
- Add support for multiple LLM providers (OpenAI, Anthropic) with URL-based detection - Implement generic authentication via customHeaders configuration - Add HTTP API server on port 8090 for manual alert submission: - POST /api/v1/analyze - Submit alert for async LLM analysis - GET /api/v1/metrics - API request metrics - GET /health - Health check (unauthenticated) - Add X-Internal-Key authentication middleware for protected endpoints - Add AutoAnalyze config flag to enable/disable automatic processing - Add AnthropicRequest/Response schema types for Claude API format - Add ANTHROPIC_API_VERSION constant for required header - Clean up unused constants (GPT_API_ENDPOINT, AllowedGPTModels) - Fix silent JSON parsing errors with proper logging
1 parent cd79a09 commit 0baab17

File tree

30 files changed

+1521
-1063
lines changed

30 files changed

+1521
-1063
lines changed

agent/docs/tasks.md

Lines changed: 0 additions & 133 deletions
This file was deleted.

agent/logs/utmstack_agent.log

Lines changed: 0 additions & 1 deletion
This file was deleted.

agent/templates/filebeat.yml

Lines changed: 0 additions & 33 deletions
This file was deleted.

agent/templates/winlogbeat.yml

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 119 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,144 @@
11
package validations
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"maps"
57
"net/http"
8+
"strings"
69

710
"github.com/threatwinds/go-sdk/utils"
811
"github.com/utmstack/UTMStack/plugins/modules-config/config"
912
)
1013

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+
}
1330

14-
if config == nil {
31+
func ValidateSOCAIConfig(cfg *config.ModuleGroup) error {
32+
if cfg == nil {
1533
return fmt.Errorf("SOC_AI configuration is nil")
1634
}
1735

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")
2544
}
2645

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"
2949
}
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)
3452
}
3553

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'")
4057
}
4158

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
5267
}
5368

5469
return nil
5570
}
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

Comments
 (0)