diff --git a/.github/workflows/principal-multi-env.yml b/.github/workflows/principal-multi-env.yml
index 3561be85a..0c7e934b3 100644
--- a/.github/workflows/principal-multi-env.yml
+++ b/.github/workflows/principal-multi-env.yml
@@ -88,7 +88,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- service: ['aws', 'backend', 'correlation', 'frontend', 'bitdefender', 'mutate', 'office365', 'log-auth-proxy', 'sophos', 'user-auditor', 'web-pdf']
+ service: ['aws', 'backend', 'correlation', 'frontend', 'bitdefender', 'mutate', 'office365', 'log-auth-proxy', 'soc-ai', 'sophos', 'user-auditor', 'web-pdf']
uses: ./.github/workflows/used-runner.yml
with:
microservice: ${{ matrix.service }}
diff --git a/.github/workflows/used-runner.yml b/.github/workflows/used-runner.yml
index cea5e73f8..18d6e730c 100644
--- a/.github/workflows/used-runner.yml
+++ b/.github/workflows/used-runner.yml
@@ -22,7 +22,7 @@ jobs:
id: get_tech
run: |
folder_changed="${{inputs.microservice}}"
- if [[ "$folder_changed" == "aws" || "$folder_changed" == "correlation" || "$folder_changed" == "bitdefender" || "$folder_changed" == "office365" || "$folder_changed" == "sophos" || "$folder_changed" == "log-auth-proxy" ]]; then
+ if [[ "$folder_changed" == "aws" || "$folder_changed" == "correlation" || "$folder_changed" == "bitdefender" || "$folder_changed" == "office365" || "$folder_changed" == "soc-ai" || "$folder_changed" == "sophos" || "$folder_changed" == "log-auth-proxy" ]]; then
tech="golang"
elif [[ "$folder_changed" == "backend" ]]; then
tech="java-11"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa1c862da..b786f2d72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,8 @@
-# UTMStack 10.7.3 Release Notes
--- Implemented backend support for filtering compliance reports based on active integrations, optimizing query performance and data retrieval.
--- Introduced new compliance reports aligned with the PCI DSS standard to expand auditing capabilities.
--- Added support for creating and updating tag-based rules with dynamic conditions.
-
-### Bug Fixes
--- Improved exception handling in `automaticReview` to prevent the process from stopping due to errors, ensuring the system continues evaluating alerts even if a specific rule fails.
--- Improved operator selection for more accurate and consistent filtering.
\ No newline at end of file
+# UTMStack 10.8.0 Release Notes
+- Updated Soc-AI models and released the code as open source.
+- Added the ability for users to choose which model to use with Soc-AI.
+- Enhanced the prompt sent to OpenAI by including additional contextual details.
+- Added support for RedHat; UTMStack can now be installed on both Ubuntu and RedHat.
+- Improved log delivery from ARM-based agents on Windows, now sending native system logs.
+- Added support for macOS ARM64; agents can now be installed on that platform.
+- Improved agent information displayed in the Sources panel, providing more accurate OS details and agent versions.
\ No newline at end of file
diff --git a/backend/src/main/resources/config/liquibase/changelog/20250418001_add_options_module_group_config.xml b/backend/src/main/resources/config/liquibase/changelog/20250418001_add_options_module_group_config.xml
index 7ff458451..b1831ff2d 100644
--- a/backend/src/main/resources/config/liquibase/changelog/20250418001_add_options_module_group_config.xml
+++ b/backend/src/main/resources/config/liquibase/changelog/20250418001_add_options_module_group_config.xml
@@ -27,20 +27,22 @@
'select',
true,
'[
- { "value": "gpt-4", "label": "GPT-4 (Default)" },
- { "value": "gpt-4-0613", "label": "GPT-4 (0613)" },
- { "value": "gpt-4-32k", "label": "GPT-4 32K" },
- { "value": "gpt-4-32k-0613", "label": "GPT-4 32K (0613)" },
- { "value": "gpt-4-turbo", "label": "GPT-4 Turbo" },
- { "value": "gpt-4o", "label": "GPT-4 Omni" },
- { "value": "gpt-4o-mini", "label": "GPT-4 Omni Mini" },
- { "value": "gpt-4.1", "label": "GPT-4.1" },
- { "value": "gpt-4.1-mini", "label": "GPT-4.1 Mini" },
- { "value": "gpt-4.1-nano", "label": "GPT-4.1 Nano" },
- { "value": "gpt-3.5-turbo", "label": "GPT-3.5 Turbo" },
- { "value": "gpt-3.5-turbo-0613", "label": "GPT-3.5 Turbo (0613)" },
- { "value": "gpt-3.5-turbo-16k", "label": "GPT-3.5 Turbo 16K" },
- { "value": "gpt-3.5-turbo-16k-0613", "label": "GPT-3.5 Turbo 16K (0613)" }
+ { "value": "gpt-4.1", "label": "GPT-4.1 Model" },
+ { "value": "gpt-4.1-mini", "label": "GPT-4.1 Mini Model" },
+ { "value": "gpt-4.1-nano", "label": "GPT-4.1 Nano Model" },
+ { "value": "gpt-4o", "label": "GPT-4 Omni Model" },
+ { "value": "gpt-4o-mini", "label": "GPT-4 Omni Mini Model" },
+ { "value": "gpt-4-turbo", "label": "GPT-4 Turbo Model" },
+ { "value": "gpt-4-0614", "label": "GPT-4 Model (0614)" },
+ { "value": "gpt-4-0125-preview", "label": "GPT-4 Model (0125 Preview)" },
+ { "value": "gpt-3.5-turbo", "label": "GPT-3.5 Turbo Model" },
+ { "value": "gpt-3.5-turbo-instruct", "label": "GPT-3.5 Turbo Instruct Model" },
+ { "value": "gpt-3.5-turbo-1106", "label": "GPT-3.5 Turbo Model (1106)" },
+ { "value": "o1", "label": "O1 Model" },
+ { "value": "o1-pro", "label": "O1 Pro Model" },
+ { "value": "o3", "label": "O3 Model" },
+ { "value": "o3-mini", "label": "O3 Mini Model" },
+ { "value": "o4-mini", "label": "O4 Mini Model" }
]'
);
diff --git a/correlation/cache/operators.go b/correlation/cache/operators.go
index c0322a14c..2c9e2634b 100644
--- a/correlation/cache/operators.go
+++ b/correlation/cache/operators.go
@@ -88,7 +88,7 @@ func compare(operator, val1, val2 string) bool {
return !lowerEqual(val1, val2)
case "contains":
return contain(val1, val2)
- case "not contain":
+ case "not contain", "not contains":
return !contain(val1, val2)
case "in":
return in(val1, val2)
diff --git a/frontend/src/app/shared/components/utm/util/utm-agent-detail/utm-agent-detail.component.html b/frontend/src/app/shared/components/utm/util/utm-agent-detail/utm-agent-detail.component.html
index 9fda0af24..8b4b26264 100644
--- a/frontend/src/app/shared/components/utm/util/utm-agent-detail/utm-agent-detail.component.html
+++ b/frontend/src/app/shared/components/utm/util/utm-agent-detail/utm-agent-detail.component.html
@@ -56,9 +56,13 @@
- OS Version:
+ Agent Version:
{{agent.version}}
+
+ OS Version:
+ {{agent.osMajorVersion + '.' + agent.osMinorVersion}}
+
Last seen:
{{agent.lastSeen}}
diff --git a/installer/types/compose.go b/installer/types/compose.go
index 5dbd6a0a3..28b1d33c0 100644
--- a/installer/types/compose.go
+++ b/installer/types/compose.go
@@ -507,7 +507,7 @@ func (c *Compose) Populate(conf *Config, stack *StackConfig) *Compose {
socAIMem := stack.ServiceResources["socai"].AssignedMemory
c.Services["socai"] = Service{
- Image: utils.Str("ghcr.io/utmstack/soc-ai/soc-ai:" + conf.Branch),
+ Image: utils.Str("ghcr.io/utmstack/utmstack/soc-ai:" + conf.Branch),
DependsOn: []string{
"node1",
"backend",
diff --git a/installer/utils/os.go b/installer/utils/os.go
index d46d26f6d..272ab89fc 100644
--- a/installer/utils/os.go
+++ b/installer/utils/os.go
@@ -49,6 +49,11 @@ func CreatePathIfNotExist(path string) error {
}
func WriteToFile(fileName string, body string) error {
+ filePath := filepath.Dir(fileName)
+ if err := CreatePathIfNotExist(filePath); err != nil {
+ return fmt.Errorf("error creating directory for file %s: %v", fileName, err)
+ }
+
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm)
if err != nil {
diff --git a/soc-ai/Dockerfile b/soc-ai/Dockerfile
new file mode 100644
index 000000000..bb8839947
--- /dev/null
+++ b/soc-ai/Dockerfile
@@ -0,0 +1,13 @@
+FROM ubuntu:24.04
+
+COPY soc-ai /app/
+
+RUN apt-get update && \
+ apt-get install -y ca-certificates jq wget && \
+ update-ca-certificates && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
+EXPOSE 8080
+
+CMD ["/app/soc-ai"]
\ No newline at end of file
diff --git a/soc-ai/configurations/config.go b/soc-ai/configurations/config.go
new file mode 100644
index 000000000..cbb66b05f
--- /dev/null
+++ b/soc-ai/configurations/config.go
@@ -0,0 +1,71 @@
+package configurations
+
+import (
+ "time"
+
+ UTMStackConfigurationClient "github.com/utmstack/config-client-go"
+ "github.com/utmstack/config-client-go/enum"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+var (
+ gptConfig GPTConfig
+)
+
+type GPTConfig struct {
+ APIKey string
+ ChangeAlertStatus bool
+ AutomaticIncidentCreation bool
+ Model string
+ ModuleActive bool
+}
+
+func GetGPTConfig() *GPTConfig {
+ return &gptConfig
+}
+
+func UpdateGPTConfigurations() {
+ intKey := GetInternalKey()
+ panelServ := GetPanelServiceName()
+ client := UTMStackConfigurationClient.NewUTMClient(intKey, panelServ)
+
+ for {
+ if err := utils.ConnectionChecker(GPT_API_ENDPOINT); err != nil {
+ utils.Logger.ErrorF("Failed to establish internet connection: %v", err)
+ }
+
+ tempModuleConfig, err := client.GetUTMConfig(enum.SOCAI)
+ if err != nil && err.Error() != "" && err.Error() != " " {
+ utils.Logger.LogF(100, "Error while getting GPT configuration: %v", err)
+ time.Sleep(TIME_FOR_GET_CONFIG * time.Second)
+ continue
+ }
+
+ gptConfig.ModuleActive = tempModuleConfig.ModuleActive
+
+ if gptConfig.ModuleActive && tempModuleConfig != nil && len(tempModuleConfig.ConfigurationGroups) > 0 {
+ for _, config := range tempModuleConfig.ConfigurationGroups[0].Configurations {
+ switch config.ConfKey {
+ case "utmstack.socai.key":
+ if config.ConfValue != "" && config.ConfValue != " " {
+ gptConfig.APIKey = config.ConfValue
+ }
+ case "utmstack.socai.incidentCreation":
+ if config.ConfValue != "" && config.ConfValue != " " {
+ gptConfig.AutomaticIncidentCreation = config.ConfValue == "true"
+ }
+ case "utmstack.socai.changeAlertStatus":
+ if config.ConfValue != "" && config.ConfValue != " " {
+ gptConfig.ChangeAlertStatus = config.ConfValue == "true"
+ }
+ case "utmstack.socai.model":
+ if config.ConfValue != "" && config.ConfValue != " " {
+ gptConfig.Model = config.ConfValue
+ }
+ }
+ }
+ }
+
+ time.Sleep(TIME_FOR_GET_CONFIG * time.Second)
+ }
+}
diff --git a/soc-ai/configurations/const.go b/soc-ai/configurations/const.go
new file mode 100644
index 000000000..842acfec3
--- /dev/null
+++ b/soc-ai/configurations/const.go
@@ -0,0 +1,89 @@
+package configurations
+
+import (
+ "path/filepath"
+
+ "github.com/utmstack/soc-ai/utils"
+)
+
+const (
+ SOC_AI_SERVER_PORT = "8080"
+ SOC_AI_SERVER_ENDPOINT = "/process"
+ API_ALERT_ENDPOINT = "/api/elasticsearch/search"
+ API_ALERT_STATUS_ENDPOINT = "/api/utm-alerts/status"
+ API_INCIDENT_ENDPOINT = "/api/utm-incidents"
+ API_INCIDENT_ADD_NEW_ALERT_ENDPOINT = "/api/utm-incidents/add-alerts"
+ API_ALERT_COMPLETED_STATUS_CODE = 5
+ API_ALERT_INFO_PARAMS = "?page=0&size=25&top=10000&indexPattern="
+ ELASTIC_DOC_ENDPOINT = "/_doc/"
+ ELASTIC_UPDATE_BY_QUERY_ENDPOINT = "/_update_by_query"
+ ALERT_INDEX_PATTERN = "alert-*"
+ LOGS_INDEX_PATTERN = "log-*"
+ SOC_AI_INDEX = "soc-ai"
+ GPT_API_ENDPOINT = "https://api.openai.com/v1/chat/completions"
+ TIME_FOR_GET_CONFIG = 10
+ CLEANER_DELAY = 10
+ MAX_ATTEMPS_TO_GPT = 3
+ GPT_RESPONSE_TOKENS = 10
+ HTTP_GPT_TIMEOUT = 90
+ HTTP_TIMEOUT = 30
+ LOGS_SEPARATOR = "[utm-logs-separator]"
+)
+
+var (
+ AllowedGPTModels = map[string]int{
+ "gpt-4.1": 1047576,
+ "gpt-4.1-mini": 1047576,
+ "gpt-4.1-nano": 1047576,
+ "gpt-4o": 128000,
+ "gpt-4o-mini": 128000,
+ "gpt-4-turbo": 128000,
+ "gpt-4-0614": 8192,
+ "gpt-4-0125-preview": 128000,
+ "gpt-3.5-turbo": 16385,
+ "gpt-3.5-turbo-instruct": 4096,
+ "gpt-3.5-turbo-1106": 16385,
+ "o1": 200000,
+ "o1-pro": 200000,
+ "o3": 200000,
+ "o3-mini": 200000,
+ "o4-mini": 200000,
+ // "gpt-4-0314": 8192, // Removed 2024-06-13
+ // "gpt-4-1106-preview": 128000, // Removed 2024-12-06
+ }
+)
+
+type SensitivePattern struct {
+ Regexp string
+ FakeValue string
+}
+
+var (
+ SensitivePatterns = map[string]SensitivePattern{
+ "email": {Regexp: `([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})`, FakeValue: "jhondoe@gmail.com"},
+ //"ipv4": `(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`,
+ }
+ GPT_INSTRUCTION = "You are an expert security engineer. Perform a deep analysis of an alert created by a SIEM and the logs related to it. Determine if the alert could be an actual potential threat or not and explain why. Provide a description that shows a deep understanding of the alert based on a deep analysis of its logs and estimate the risk to the systems affected. Classify the alert in the following manner: if the alert information is sufficient to determine that the security, availability, confidentiality, or integrity of the systems has being compromised, then classify it as \"possible incident\". If the alert does not pose a security risk to the organization or has no security relevance, classify it as \"possible false positive\". If the alert does not pose an imminent risk to the systems, requires no urgent action from an administrator, or requires not urgent review by an administrator, it should be classified as a \"standard alert\". You will also provide context-specific instructions for remediation, mitigation, or further investigation, related to the alert and logs analyzed. Your answer should be provided using the following JSON format and the total number of characters in your answer must not exceed 1500 words. Your entire answer must be inside this json format. {\"activity_id\":\"
\",\"classification\":\"\",\"reasoning\":[\"\"],\"nextSteps\":[{\"step\":1,\"action\":\"\",\"details\":\"\"},{\"step\":2,\"action\":\"\",\"details\":\"\"},{\"step\":3,\"action\":\"\"]}Ensure that your entire answer adheres to the provided JSON format. The response should be valid JSON syntax and schema."
+ GPT_FALSE_POSITIVE = "This alert is categorized as a potential false positive due to two key factors. Firstly, it originates from an automated system, which may occasionally produce alerts without direct human validation. Additionally, the absence of any correlated logs further raises suspicion, as a genuine incident typically leaves a trail of relevant log entries. Hence, the combination of its system-generated nature and the lack of associated logs suggests a likelihood of being a false positive rather than a genuine security incident."
+)
+
+func GetInternalKey() string {
+ return utils.Getenv("INTERNAL_KEY", true)
+}
+
+func GetPanelServiceName() string {
+ return utils.Getenv("PANEL_SERV_NAME", true)
+}
+
+func GetOpenSearchHost() string {
+ return "http://" + utils.Getenv("OPENSEARCH_HOST", true)
+}
+
+func GetOpenSearchPort() string {
+ return utils.Getenv("OPENSEARCH_PORT", true)
+}
+
+func GetAlertsDBPath() string {
+ path, _ := utils.GetMyPath()
+ return filepath.Join(path, "database", "alerts.sqlite3")
+}
diff --git a/soc-ai/elastic/alerts.go b/soc-ai/elastic/alerts.go
new file mode 100644
index 000000000..678608e8a
--- /dev/null
+++ b/soc-ai/elastic/alerts.go
@@ -0,0 +1,67 @@
+package elastic
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/schema"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+func GetAlertsInfo(id string) (schema.Alert, error) {
+ result, err := ElasticSearch(configurations.ALERT_INDEX_PATTERN, "id", id)
+ if err != nil {
+ return schema.Alert{}, fmt.Errorf("error while getting alert %s info: %v", id, err)
+ }
+
+ var alertDetails schema.AlertDetails
+ err = json.Unmarshal(result, &alertDetails)
+ if err != nil {
+ return schema.Alert{}, fmt.Errorf("error decoding response: %v", err)
+ }
+
+ if len(alertDetails) == 0 {
+ return schema.Alert{}, fmt.Errorf("no alert found for id")
+ }
+
+ if len(alertDetails[0].Logs) > 0 {
+ var logs []string
+ if len(alertDetails[0].Logs) > 3 {
+ logs = alertDetails[0].Logs[:3]
+ } else {
+ logs = alertDetails[0].Logs
+ }
+
+ for i, log := range logs {
+ resp, err := ElasticSearch(configurations.LOGS_INDEX_PATTERN, "id", log)
+ if err != nil {
+ continue
+ }
+ alertDetails[0].Logs[i] = string(resp)
+ }
+ }
+ return alertDetails[0], nil
+}
+
+func ChangeAlertStatus(id string, status int, observations string) error {
+ url := configurations.GetPanelServiceName() + configurations.API_ALERT_STATUS_ENDPOINT
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ "Utm-Internal-Key": configurations.GetInternalKey(),
+ }
+
+ body := schema.ChangeAlertStatus{AlertIDs: []string{id}, Status: status, StatusObservation: observations}
+ bodyBytes, err := json.Marshal(body)
+ if err != nil {
+ return fmt.Errorf("error marshalling body: %v", err)
+ }
+
+ resp, statusCode, err := utils.DoReq(url, bodyBytes, "POST", headers, configurations.HTTP_TIMEOUT)
+ if err != nil || statusCode != http.StatusOK {
+ return fmt.Errorf("error while doing request: %v, status: %d, response: %v", err, statusCode, string(resp))
+ }
+
+ return nil
+}
diff --git a/soc-ai/elastic/incidents.go b/soc-ai/elastic/incidents.go
new file mode 100644
index 000000000..f385b024a
--- /dev/null
+++ b/soc-ai/elastic/incidents.go
@@ -0,0 +1,102 @@
+package elastic
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/schema"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+func CreateNewIncident(alertDetails schema.AlertGPTDetails) error {
+ url := configurations.GetPanelServiceName() + configurations.API_INCIDENT_ENDPOINT
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ "Utm-Internal-Key": configurations.GetInternalKey(),
+ }
+
+ t := time.Now()
+ body := schema.CreateNewIncidentRequest{
+ IncidentName: fmt.Sprintf("INC-%s%s Incident in %s", t.Format("2006010215"), t.Format("04"), alertDetails.DataSource),
+ IncidentDescription: fmt.Sprintf("AI GENERATED ANALYSIS: Multiple related alerts were detected and grouped in the %s datasource during the last 24 hours. Artificial intelligence classified this grouping as a possible incident", alertDetails.DataSource),
+ IncidentAssignedTo: "None",
+ AlertList: schema.AlertList{{
+ AlertID: alertDetails.AlertID,
+ AlertName: alertDetails.Name,
+ AlertStatus: alertDetails.Status,
+ AlertSeverity: alertDetails.Severity,
+ }},
+ }
+
+ bodyBytes, err := json.Marshal(body)
+ if err != nil {
+ return fmt.Errorf("error marshalling body: %v", err)
+ }
+
+ resp, statusCode, err := utils.DoReq(url, bodyBytes, "POST", headers, configurations.HTTP_TIMEOUT)
+ if err != nil || statusCode != http.StatusOK {
+ return fmt.Errorf("error while doing request: %v, status: %d, response: %v", err, statusCode, string(resp))
+ }
+
+ return nil
+}
+
+func AddAlertToIncident(incidentId int, alertDetails schema.AlertGPTDetails) error {
+ url := configurations.GetPanelServiceName() + configurations.API_INCIDENT_ADD_NEW_ALERT_ENDPOINT
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ "Utm-Internal-Key": configurations.GetInternalKey(),
+ }
+
+ body := schema.AddNewAlertToIncidentRequest{
+ IncidenId: incidentId,
+ AlertList: schema.AlertList{{
+ AlertID: alertDetails.AlertID,
+ AlertName: alertDetails.Name,
+ AlertStatus: alertDetails.Status,
+ AlertSeverity: alertDetails.Severity,
+ }},
+ }
+
+ bodyBytes, err := json.Marshal(body)
+ if err != nil {
+ return fmt.Errorf("error marshalling body: %v", err)
+ }
+
+ resp, statusCode, err := utils.DoReq(url, bodyBytes, "POST", headers, configurations.HTTP_TIMEOUT)
+ if err != nil || (statusCode != http.StatusOK && statusCode != http.StatusCreated) {
+ return fmt.Errorf("error while doing request: %v, status: %d, response: %v", err, statusCode, string(resp))
+ }
+
+ return nil
+}
+
+func GetIncidentsByPattern(pattern string) ([]schema.IncidentResp, error) {
+ pattern = strings.ReplaceAll(pattern, " ", "%20")
+ tnow := time.Now().UTC()
+ t24hAfter := tnow.Add(24 * time.Hour)
+ t24hBefore := tnow.Add(-24 * time.Hour)
+
+ url := configurations.GetPanelServiceName() + configurations.API_INCIDENT_ENDPOINT + "?incidentName.contains=" + pattern + "&incidentCreatedDate.greaterThanOrEqual=" + t24hBefore.Format(time.RFC3339) + "&incidentCreatedDate.lessThanOrEqual=" + t24hAfter.Format(time.RFC3339) + "&incidentStatus.in=IN_REVIEW,OPEN&page=0&size=100"
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ "Utm-Internal-Key": configurations.GetInternalKey(),
+ }
+
+ resp, statusCode, err := utils.DoReq(url, nil, "GET", headers, configurations.HTTP_TIMEOUT)
+ if err != nil || statusCode != http.StatusOK {
+ return nil, fmt.Errorf("error while doing request: %v, status: %d, response: %v", err, statusCode, string(resp))
+ }
+
+ var incidents []schema.IncidentResp
+ err = json.Unmarshal(resp, &incidents)
+ if err != nil {
+ return nil, fmt.Errorf("error while unmarshalling response: %v", err)
+ }
+
+ return incidents, nil
+}
diff --git a/soc-ai/elastic/index.go b/soc-ai/elastic/index.go
new file mode 100644
index 000000000..d1cee0c98
--- /dev/null
+++ b/soc-ai/elastic/index.go
@@ -0,0 +1,98 @@
+package elastic
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/schema"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+func ElasticQuery(index string, query interface{}, op string) error {
+ var endp string
+ switch op {
+ case "create":
+ endp = configurations.ELASTIC_DOC_ENDPOINT
+ case "update":
+ endp = configurations.ELASTIC_UPDATE_BY_QUERY_ENDPOINT
+ }
+ url := configurations.GetOpenSearchHost() + ":" + configurations.GetOpenSearchPort() + "/" + index + endp
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ }
+
+ queryBytes, err := json.Marshal(query)
+ if err != nil {
+ return fmt.Errorf("error marshalling query: %v", err)
+ }
+
+ resp, statusCode, err := utils.DoReq(url, queryBytes, "POST", headers, configurations.HTTP_TIMEOUT)
+ if err != nil || (statusCode != http.StatusOK && statusCode != http.StatusCreated) {
+ return fmt.Errorf("error while doing request: %v, status: %d, response: %v", err, statusCode, string(resp))
+ }
+
+ return nil
+}
+
+func ElasticSearch(index, field, value string) ([]byte, error) {
+ url := configurations.GetPanelServiceName() + configurations.API_ALERT_ENDPOINT + configurations.API_ALERT_INFO_PARAMS + index
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ "Utm-Internal-Key": configurations.GetInternalKey(),
+ }
+
+ body := schema.SearchDetailsRequest{{Field: field, Operator: "IS", Value: value}}
+ bodyBytes, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("error marshalling body: %v", err)
+ }
+
+ resp, statusCode, err := utils.DoReq(url, bodyBytes, "POST", headers, configurations.HTTP_TIMEOUT)
+ if err != nil || statusCode != http.StatusOK {
+ return nil, fmt.Errorf("error while doing request for get Alert Details: %v: %s", err, string(resp))
+ }
+
+ return resp, nil
+}
+
+func IndexStatus(id, status, op string) error {
+ doc := schema.GPTAlertResponse{
+ Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05.999999Z07:00"),
+ Status: status,
+ ActivityID: id,
+ }
+
+ if op == "update" {
+ query, err := schema.ConvertGPTResponseToUpdateQuery(doc)
+ if err != nil {
+ return fmt.Errorf("error while converting response to update query: %v", err)
+ }
+ return ElasticQuery(configurations.SOC_AI_INDEX, query, op)
+ } else {
+ return ElasticQuery(configurations.SOC_AI_INDEX, doc, op)
+ }
+}
+
+func CreateIndexIfNotExist(index string) error {
+ url := configurations.GetOpenSearchHost() + ":" + configurations.GetOpenSearchPort() + "/" + index
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ }
+
+ resp, statusCode, err := utils.DoReq(url, nil, "HEAD", headers, configurations.HTTP_TIMEOUT)
+ if err != nil {
+ return fmt.Errorf("error while doing request: %v, status: %d, response: %v", err, statusCode, string(resp))
+ }
+
+ if statusCode == 404 {
+ resp, statusCode, err = utils.DoReq(url, nil, "PUT", headers, configurations.HTTP_TIMEOUT)
+ if err != nil || (statusCode != http.StatusOK && statusCode != http.StatusCreated) {
+ return fmt.Errorf("error while doing request: %v, status: %d, response: %v", err, statusCode, string(resp))
+ }
+ }
+
+ return nil
+}
diff --git a/soc-ai/go.mod b/soc-ai/go.mod
new file mode 100644
index 000000000..f5af25a6d
--- /dev/null
+++ b/soc-ai/go.mod
@@ -0,0 +1,46 @@
+module github.com/utmstack/soc-ai
+
+go 1.22.4
+
+toolchain go1.23.4
+
+require (
+ github.com/gin-contrib/gzip v0.0.6
+ github.com/gin-gonic/gin v1.10.0
+ github.com/pkoukk/tiktoken-go v0.1.6
+ github.com/threatwinds/logger v1.2.1
+ github.com/utmstack/config-client-go v1.2.4
+)
+
+require (
+ github.com/bytedance/sonic v1.12.1 // indirect
+ github.com/bytedance/sonic/loader v0.2.0 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/dlclark/regexp2 v1.10.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.5 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.22.0 // indirect
+ github.com/goccy/go-json v0.10.3 // indirect
+ github.com/google/go-cmp v0.5.8 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.8 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ golang.org/x/arch v0.9.0 // indirect
+ golang.org/x/crypto v0.32.0 // indirect
+ golang.org/x/net v0.34.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/soc-ai/go.sum b/soc-ai/go.sum
new file mode 100644
index 000000000..64651d345
--- /dev/null
+++ b/soc-ai/go.sum
@@ -0,0 +1,145 @@
+github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
+github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
+github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
+github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
+github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
+github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
+github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
+github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
+github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
+github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/threatwinds/logger v1.2.1 h1:uN7efZaHobMX3DRi6GOPtxESPxt5xj0bNflnmgklwII=
+github.com/threatwinds/logger v1.2.1/go.mod h1:eevkhjP9wSpRekRIgi4ZAq7DMdlUMy+Shwx/QNDvOHg=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/utmstack/config-client-go v1.2.4 h1:q/if6a/qGMkUj53MGHmF6ZucixfoqHuM5CG2pHNpWcY=
+github.com/utmstack/config-client-go v1.2.4/go.mod h1:5B1phBmxHZmnggujCsNVAHK2oFXIy89mLCc9ixrDcCo=
+golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
+golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
diff --git a/soc-ai/gpt/client.go b/soc-ai/gpt/client.go
new file mode 100644
index 000000000..edff874ee
--- /dev/null
+++ b/soc-ai/gpt/client.go
@@ -0,0 +1,81 @@
+package gpt
+
+import (
+ "encoding/json"
+ "fmt"
+ "sync"
+
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/schema"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+type GPTClient struct{}
+
+var (
+ client *GPTClient
+ clientOnce sync.Once
+)
+
+func GetGPTClient() *GPTClient {
+ clientOnce.Do(func() {
+ client = &GPTClient{}
+ })
+ return client
+}
+
+func (c *GPTClient) Request(alert schema.AlertGPTDetails) (string, error) {
+ content := configurations.GPT_INSTRUCTION
+ if alert.Logs == "" || alert.Logs == " " {
+ content += content + ". " + configurations.GPT_FALSE_POSITIVE
+ }
+ jsonContent, err := json.Marshal(alert)
+ if err != nil {
+ return "", fmt.Errorf("error marshalling alert: %v", err)
+ }
+
+ req := schema.GPTRequest{
+ Model: configurations.GetGPTConfig().Model,
+ Messages: []schema.GPTMessage{
+ {
+ Role: "system",
+ Content: content,
+ },
+ {
+ Role: "user",
+ Content: string(jsonContent),
+ },
+ },
+ }
+
+ requestJson, error := json.Marshal(req)
+ if error != nil {
+ return "", fmt.Errorf("error marshalling request: %v", error)
+ }
+
+ headers := map[string]string{
+ "Authorization": "Bearer " + configurations.GetGPTConfig().APIKey,
+ "Content-Type": "application/json",
+ }
+
+ response, status, err := utils.DoParseReq[schema.GPTResponse](configurations.GPT_API_ENDPOINT, requestJson, "POST", headers, configurations.HTTP_GPT_TIMEOUT)
+ if err != nil {
+ if status == 401 {
+ return "", fmt.Errorf("invalid api-key")
+ }
+ return "", fmt.Errorf("error making request to GPT: %v", err)
+ }
+
+ return response.Choices[0].Message.Content, nil
+}
+
+func (c *GPTClient) ProcessResponse(response string) (schema.GPTAlertResponse, error) {
+ alertRespProcessed := schema.GPTAlertResponse{}
+
+ err := json.Unmarshal([]byte(response), &alertRespProcessed)
+ if err != nil {
+ return schema.GPTAlertResponse{}, fmt.Errorf("error while unmarshalling response: %v", err)
+ }
+
+ return alertRespProcessed, nil
+}
diff --git a/soc-ai/gpt/counter.go b/soc-ai/gpt/counter.go
new file mode 100644
index 000000000..606c72c44
--- /dev/null
+++ b/soc-ai/gpt/counter.go
@@ -0,0 +1,44 @@
+package gpt
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/pkoukk/tiktoken-go"
+)
+
+func NumTokensFromMessage(message string, model string) (numTokens int, err error) {
+ tkm, err := tiktoken.EncodingForModel(model)
+ if err != nil {
+ return -1, fmt.Errorf("encoding for model: %v", err)
+ }
+
+ var tokensPerMessage, tokensPerName int
+ switch model {
+ case "gpt-3.5-turbo-0613",
+ "gpt-3.5-turbo-16k-0613",
+ "gpt-4-0314",
+ "gpt-4-32k-0314",
+ "gpt-4-0613",
+ "gpt-4-32k-0613":
+ tokensPerMessage = 3
+ tokensPerName = 1
+ case "gpt-3.5-turbo-0301":
+ tokensPerMessage = 4
+ tokensPerName = -1
+ default:
+ if strings.Contains(model, "gpt-3.5-turbo") {
+ return NumTokensFromMessage(message, "gpt-3.5-turbo-0613")
+ } else if strings.Contains(model, "gpt-4") {
+ return NumTokensFromMessage(message, "gpt-4-0613")
+ } else {
+ return -1, fmt.Errorf("num_tokens_from_messages() is not implemented for model %s. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens", model)
+ }
+ }
+
+ numTokens += tokensPerMessage
+ numTokens += len(tkm.Encode(message, nil, nil))
+ numTokens += tokensPerName
+
+ return numTokens, nil
+}
diff --git a/soc-ai/main.go b/soc-ai/main.go
new file mode 100644
index 000000000..66e51900e
--- /dev/null
+++ b/soc-ai/main.go
@@ -0,0 +1,10 @@
+package main
+
+import (
+ "github.com/utmstack/soc-ai/processor"
+)
+
+func main() {
+ processor := processor.NewProcessor()
+ processor.ProcessData()
+}
diff --git a/soc-ai/processor/alertProcessor.go b/soc-ai/processor/alertProcessor.go
new file mode 100644
index 000000000..2a4146c0d
--- /dev/null
+++ b/soc-ai/processor/alertProcessor.go
@@ -0,0 +1,21 @@
+package processor
+
+import (
+ "fmt"
+
+ "github.com/utmstack/soc-ai/elastic"
+ "github.com/utmstack/soc-ai/schema"
+)
+
+func (p *Processor) processAlertsInfo() {
+ for alert := range p.AlertInfoQueue {
+ alertInfo, err := elastic.GetAlertsInfo(alert.AlertID)
+ if err != nil {
+ p.RegisterError(fmt.Sprintf("error while getting alert %s info: %v", alert.AlertID, err), alert.AlertID)
+ continue
+ }
+
+ details := schema.ConvertFromAlertToAlertDB(alertInfo)
+ p.GPTQueue <- cleanAlerts(&details)
+ }
+}
diff --git a/soc-ai/processor/cleaner.go b/soc-ai/processor/cleaner.go
new file mode 100644
index 000000000..6dbd89f2d
--- /dev/null
+++ b/soc-ai/processor/cleaner.go
@@ -0,0 +1,27 @@
+package processor
+
+import (
+ "reflect"
+ "regexp"
+
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/schema"
+)
+
+func cleanAlerts(alertDetails *schema.AlertGPTDetails) schema.AlertGPTDetails {
+ v := reflect.ValueOf(alertDetails).Elem()
+ for i := 0; i < v.NumField(); i++ {
+ field := v.Field(i)
+ if field.Kind() == reflect.String {
+ str := field.String()
+ for _, pattern := range configurations.SensitivePatterns {
+ re := regexp.MustCompile(pattern.Regexp)
+ if re.MatchString(str) {
+ str = re.ReplaceAllString(str, pattern.FakeValue)
+ }
+ }
+ field.SetString(str)
+ }
+ }
+ return *alertDetails
+}
diff --git a/soc-ai/processor/elastic.go b/soc-ai/processor/elastic.go
new file mode 100644
index 000000000..1b19974b4
--- /dev/null
+++ b/soc-ai/processor/elastic.go
@@ -0,0 +1,73 @@
+package processor
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/elastic"
+ "github.com/utmstack/soc-ai/schema"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+func (p *Processor) processAlertToElastic() {
+ for alert := range p.ElasticQueue {
+ gptConfig := configurations.GetGPTConfig()
+
+ resp := schema.ConvertFromAlertDBToGPTResponse(alert)
+ resp.Status = "Completed"
+ query, err := schema.ConvertGPTResponseToUpdateQuery(resp)
+ if err != nil {
+ p.RegisterError(fmt.Sprintf("error converting gpt response to update query: %v", err), alert.AlertID)
+ continue
+ }
+ err = elastic.ElasticQuery(configurations.SOC_AI_INDEX, query, "update")
+ if err != nil {
+ p.RegisterError(fmt.Sprintf("error indexing gpt response in elastic: %v", err), alert.AlertID)
+ continue
+ }
+
+ if gptConfig.ChangeAlertStatus && alert.GPTClassification == "possible false positive" {
+ err = elastic.ChangeAlertStatus(alert.AlertID, configurations.API_ALERT_COMPLETED_STATUS_CODE, alert.GPTClassification+" - "+alert.GPTReasoning)
+ if err != nil {
+ utils.Logger.ErrorF("error while changing alert status in elastic: %v", err)
+ continue
+ }
+ utils.Logger.Info("alert %s status changed to COMPLETED in Panel", alert.AlertID)
+ }
+
+ if gptConfig.AutomaticIncidentCreation && alert.GPTClassification == "possible incident" {
+ incidentsDetails, err := elastic.GetIncidentsByPattern("Incident in " + alert.DataSource)
+ if err != nil {
+ utils.Logger.ErrorF("error while getting incidents by pattern: %v", err)
+ continue
+ }
+
+ incidentExists := false
+ if len(incidentsDetails) != 0 {
+ for _, incident := range incidentsDetails {
+ if strings.HasSuffix(incident.IncidentName, "Incident in "+alert.DataSource) {
+ incidentExists = true
+ err = elastic.AddAlertToIncident(incident.ID, alert)
+ if err != nil {
+ utils.Logger.ErrorF("error while adding alert to incident: %v", err)
+ continue
+ }
+ }
+ }
+ }
+
+ if !incidentExists {
+ err = elastic.CreateNewIncident(alert)
+ if err != nil {
+ utils.Logger.ErrorF("error while creating incident: %v", err)
+ continue
+ }
+ }
+ utils.Logger.Info("alert %s added to incident in Panel", alert.AlertID)
+ }
+
+ utils.Logger.Info("alert %s processed correctly", alert.AlertID)
+
+ }
+}
diff --git a/soc-ai/processor/gpt.go b/soc-ai/processor/gpt.go
new file mode 100644
index 000000000..298095cd2
--- /dev/null
+++ b/soc-ai/processor/gpt.go
@@ -0,0 +1,57 @@
+package processor
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/gpt"
+ "github.com/utmstack/soc-ai/schema"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+func (p *Processor) processGPTRequests() {
+ for alert := range p.GPTQueue {
+ response, err := gpt.GetGPTClient().Request(alert)
+ if err != nil {
+ p.RegisterError(fmt.Sprintf("error while making request to GPT: %v", err), alert.AlertID)
+ continue
+ }
+
+ response, err = clearJson(response)
+ if err != nil {
+ p.RegisterError(fmt.Sprintf("error while cleaning json response: %v", err), alert.AlertID)
+ continue
+ }
+
+ alertResponse, err := utils.ConvertFromJsonToStruct[schema.GPTAlertResponse](response)
+ if err != nil {
+ p.RegisterError(fmt.Sprintf("error while converting response to struct: %v", err), alert.AlertID)
+ continue
+ }
+
+ nextSteps := []string{}
+ for _, step := range alertResponse.NextSteps {
+ nextSteps = append(nextSteps, fmt.Sprintf("%s:: %s", step.Action, step.Details))
+ }
+
+ alert.GPTTimestamp = time.Now().UTC().Format("2006-01-02T15:04:05.999999Z07:00")
+ alert.GPTClassification = alertResponse.Classification
+ alert.GPTReasoning = strings.Join(alertResponse.Reasoning, configurations.LOGS_SEPARATOR)
+ alert.GPTNextSteps = strings.Join(nextSteps, "\n")
+
+ p.ElasticQueue <- alert
+ }
+}
+
+func clearJson(s string) (string, error) {
+ start := strings.Index(s, "{")
+ end := strings.LastIndex(s, "}")
+
+ if start == -1 || end == -1 {
+ return "", fmt.Errorf("no valid json found in gpt response")
+ }
+
+ return s[start : end+1], nil
+}
diff --git a/soc-ai/processor/listener.go b/soc-ai/processor/listener.go
new file mode 100644
index 000000000..9ccf244c5
--- /dev/null
+++ b/soc-ai/processor/listener.go
@@ -0,0 +1,98 @@
+package processor
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/elastic"
+ "github.com/utmstack/soc-ai/schema"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+func (p *Processor) restRouter() {
+ gin.SetMode(gin.ReleaseMode)
+ r := gin.New()
+ r.Use(
+ gin.Recovery(),
+ gzip.Gzip(gzip.DefaultCompression),
+ )
+
+ r.NoRoute(notFound)
+
+ r.POST("/process", p.handleAlerts)
+
+ r.Run(":" + configurations.SOC_AI_SERVER_PORT)
+}
+
+func notFound(c *gin.Context) {
+ c.JSON(http.StatusBadRequest, "url not found")
+}
+
+func (p *Processor) handleAlerts(c *gin.Context) {
+ err := elastic.CreateIndexIfNotExist(configurations.SOC_AI_INDEX)
+ if err != nil {
+ utils.Logger.ErrorF("error creating index %s: %v", configurations.SOC_AI_INDEX, err)
+ c.JSON(http.StatusInternalServerError, fmt.Sprintf("error creating index %s", configurations.SOC_AI_INDEX))
+ return
+ }
+
+ if !configurations.GetGPTConfig().ModuleActive {
+ utils.Logger.LogF(100, "Droping request to /process, GPT module is not active")
+ c.JSON(http.StatusOK, "GPT module is not active")
+ return
+ }
+
+ var ids []string
+ if err := c.BindJSON(&ids); err != nil {
+ utils.Logger.ErrorF("error binding JSON: %v", err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ for _, id := range ids {
+ result, err := elastic.ElasticSearch(configurations.SOC_AI_INDEX, "activityId", id)
+ if err != nil {
+ if !strings.Contains(err.Error(), "no such host") {
+ utils.Logger.ErrorF("error while searching alert %s in elastic: %v", id, err)
+ c.JSON(http.StatusInternalServerError, fmt.Sprintf("error while searching alert %s in elastic: %v", id, err))
+ return
+ }
+ c.JSON(http.StatusInternalServerError, fmt.Sprintf("error while searching alert %s in elastic", id))
+ return
+ }
+
+ var gptResponses []schema.GPTAlertResponse
+ err = json.Unmarshal(result, &gptResponses)
+ if err != nil {
+ utils.Logger.ErrorF("error decoding response from elastic: %v", err)
+ c.JSON(http.StatusInternalServerError, fmt.Sprintf("error decoding response: %v", err))
+ return
+ }
+
+ if len(gptResponses) == 0 {
+ err = elastic.IndexStatus(id, "Processing", "create")
+ if err != nil {
+ utils.Logger.ErrorF("error creating doc in index: %v", err)
+ c.JSON(http.StatusInternalServerError, fmt.Sprintf("error creating doc in index: %v", err))
+ return
+ }
+ } else {
+ err = elastic.IndexStatus(id, "Processing", "update")
+ if err != nil {
+ utils.Logger.ErrorF("error updating doc in index: %v", err)
+ c.JSON(http.StatusInternalServerError, fmt.Sprintf("error updating doc in index: %v", err))
+ return
+ }
+ }
+
+ utils.Logger.Info("alert %s received", id)
+ p.AlertInfoQueue <- schema.AlertGPTDetails{AlertID: id}
+ }
+
+ c.JSON(http.StatusOK, "alerts received successfully")
+}
diff --git a/soc-ai/processor/processor.go b/soc-ai/processor/processor.go
new file mode 100644
index 000000000..518c804a0
--- /dev/null
+++ b/soc-ai/processor/processor.go
@@ -0,0 +1,61 @@
+package processor
+
+import (
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+
+ "github.com/utmstack/soc-ai/configurations"
+ "github.com/utmstack/soc-ai/elastic"
+ "github.com/utmstack/soc-ai/schema"
+ "github.com/utmstack/soc-ai/utils"
+)
+
+var (
+ processor *Processor
+ processorOnce sync.Once
+)
+
+type Processor struct {
+ AlertInfoQueue chan schema.AlertGPTDetails
+ GPTQueue chan schema.AlertGPTDetails
+ ElasticQueue chan schema.AlertGPTDetails
+}
+
+func NewProcessor() *Processor {
+ processorOnce.Do(func() {
+ processor = &Processor{
+ AlertInfoQueue: make(chan schema.AlertGPTDetails, 1000),
+ GPTQueue: make(chan schema.AlertGPTDetails, 1000),
+ ElasticQueue: make(chan schema.AlertGPTDetails, 1000),
+ }
+ })
+ return processor
+}
+
+func (p *Processor) ProcessData() {
+ utils.Logger.Info("Starting SOC-AI Processor...")
+
+ go configurations.UpdateGPTConfigurations()
+ go p.restRouter()
+ go p.processAlertsInfo()
+
+ for i := 0; i < 10; i++ {
+ go p.processGPTRequests()
+ }
+
+ go p.processAlertToElastic()
+
+ signals := make(chan os.Signal, 1)
+ signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
+ <-signals
+}
+
+func (p *Processor) RegisterError(message, id string) {
+ err := elastic.IndexStatus(id, "Error", "update")
+ if err != nil {
+ utils.Logger.ErrorF("error while indexing error in elastic: %v", err)
+ }
+ utils.Logger.ErrorF("%s", message)
+}
diff --git a/soc-ai/schema/convert.go b/soc-ai/schema/convert.go
new file mode 100644
index 000000000..a97ed08ef
--- /dev/null
+++ b/soc-ai/schema/convert.go
@@ -0,0 +1,161 @@
+package schema
+
+import (
+ "fmt"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/utmstack/soc-ai/configurations"
+)
+
+func ConvertFromAlertToAlertDB(alert Alert) AlertGPTDetails {
+ return AlertGPTDetails{
+ AlertID: alert.ID,
+ Severity: alert.Severity,
+ TagRulesApplied: alert.TagRulesApplied,
+ SeverityLabel: alert.SeverityLabel,
+ Notes: alert.Notes,
+ DataType: alert.DataType,
+ Description: alert.Description,
+ StatusLabel: alert.StatusLabel,
+ Tactic: alert.Tactic,
+ Tags: strings.Join(alert.Tags, ","),
+ Reference: strings.Join(alert.Reference, ","),
+ Protocol: alert.Protocol,
+ Timestamp: alert.Timestamp,
+ Solution: alert.Solution,
+ StatusObservation: alert.StatusObservation,
+ Name: alert.Name,
+ IsIncident: alert.IsIncident,
+ Category: alert.Category,
+ DataSource: alert.DataSource,
+ Logs: strings.Join(alert.Logs, configurations.LOGS_SEPARATOR),
+ Status: alert.Status,
+ DestinationCountry: alert.Destination.Country,
+ DestinationAccuracyRadius: alert.Destination.AccuracyRadius,
+ DestinationCity: alert.Destination.City,
+ DestinationIP: alert.Destination.IP,
+ DestinationPort: alert.Destination.Port,
+ DestinationCountryCode: alert.Destination.CountryCode,
+ DestinationIsAnonymousProxy: alert.Destination.IsAnonymousProxy,
+ DestinationHost: alert.Destination.Host,
+ DestinationCoordinates: strings.Join(convertFloatSliceToStringSlice(alert.Destination.Coordinates), ","),
+ DestinationIsSatelliteProvider: alert.Destination.IsSatelliteProvider,
+ DestinationAso: alert.Destination.Aso,
+ DestinationAsn: alert.Destination.Asn,
+ DestinationUser: alert.Destination.User,
+ SourceCountry: alert.Source.Country,
+ SourceAccuracyRadius: alert.Source.AccuracyRadius,
+ SourceCity: alert.Source.City,
+ SourceIP: alert.Source.IP,
+ SourceCoordinates: strings.Join(convertFloatSliceToStringSlice(alert.Source.Coordinates), ","),
+ SourcePort: alert.Source.Port,
+ SourceCountryCode: alert.Source.CountryCode,
+ SourceIsAnonymousProxy: alert.Source.IsAnonymousProxy,
+ SourceIsSatelliteProvider: alert.Source.IsSatelliteProvider,
+ SourceHost: alert.Source.Host,
+ SourceAso: alert.Source.Aso,
+ SourceAsn: alert.Source.Asn,
+ SourceUser: alert.Source.User,
+ IncidentCreatedBy: alert.IncidentDetails.CreatedBy,
+ IncidentObservation: alert.IncidentDetails.Observation,
+ IncidentSource: alert.IncidentDetails.Source,
+ IncidentCreationDate: alert.IncidentDetails.CreationDate,
+ IncidentName: alert.IncidentDetails.IncidentName,
+ IncidentID: alert.IncidentDetails.IncidentID,
+ GPTTimestamp: time.Now().UTC().Format("2006-01-02T15:04:05.999999Z07:00"),
+ GPTClassification: "",
+ GPTReasoning: "",
+ GPTNextSteps: "",
+ }
+}
+
+func convertFloatSliceToStringSlice(floatSlice []float64) []string {
+ strSlice := make([]string, len(floatSlice))
+ for i, num := range floatSlice {
+ strSlice[i] = strconv.FormatFloat(num, 'f', -1, 64)
+ }
+ return strSlice
+}
+
+func ConvertFromAlertDBToGPTResponse(alertDetails AlertGPTDetails) GPTAlertResponse {
+ resp := GPTAlertResponse{
+ Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05.999999Z07:00"),
+ Severity: alertDetails.Severity,
+ Category: alertDetails.Category,
+ AlertName: alertDetails.Name,
+ ActivityID: alertDetails.AlertID,
+ Classification: alertDetails.GPTClassification,
+ Reasoning: strings.Split(alertDetails.GPTReasoning, configurations.LOGS_SEPARATOR),
+ NextSteps: []NextStep{},
+ }
+
+ nextSteps := strings.Split(alertDetails.GPTNextSteps, "\n")
+ for i, step := range nextSteps {
+ actionAndDetails := strings.Split(step, "::")
+ if len(actionAndDetails) < 2 {
+ continue
+ }
+ resp.NextSteps = append(resp.NextSteps, NextStep{
+ Step: i + 1,
+ Action: actionAndDetails[0],
+ Details: actionAndDetails[1],
+ })
+ }
+
+ return resp
+}
+
+func ConvertGPTResponseToUpdateQuery(gptResp GPTAlertResponse) (UpdateDocRequest, error) {
+ source, err := BuildScriptString(gptResp)
+ if err != nil {
+ return UpdateDocRequest{}, err
+ }
+
+ return UpdateDocRequest{
+ Query: Query{
+ Bool: Bool{
+ Must: []Must{
+ {Match{
+ ActivityID: gptResp.ActivityID,
+ }},
+ },
+ },
+ },
+ Script: Script{
+ Source: source,
+ Lang: "painless",
+ Params: gptResp,
+ },
+ }, nil
+}
+
+func BuildScriptString(alert GPTAlertResponse) (string, error) {
+ v := reflect.ValueOf(alert)
+ typeOfAlert := v.Type()
+
+ source := ""
+ for i := 0; i < v.NumField(); i++ {
+ jsonTag := typeOfAlert.Field(i).Tag.Get("json")
+ jsonFieldName := strings.Split(jsonTag, ",")[0]
+ fieldValue := v.Field(i).Interface()
+
+ switch reflect.TypeOf(fieldValue).Kind() {
+ case reflect.String, reflect.Int, reflect.Struct:
+ if fieldValue != reflect.Zero(reflect.TypeOf(fieldValue)).Interface() {
+ source += fmt.Sprintf("ctx._source['%s'] = params['%s']; ", jsonFieldName, jsonFieldName)
+ }
+ case reflect.Slice:
+ s := reflect.ValueOf(fieldValue)
+ if s.Len() > 0 {
+ source += fmt.Sprintf("ctx._source['%s'] = params.%s; ", jsonFieldName, jsonFieldName)
+ }
+ default:
+ return "", fmt.Errorf("unsupported type: %v", reflect.TypeOf(fieldValue).Kind())
+ }
+ }
+
+ return source, nil
+}
diff --git a/soc-ai/schema/models.go b/soc-ai/schema/models.go
new file mode 100644
index 000000000..98bc3f4fd
--- /dev/null
+++ b/soc-ai/schema/models.go
@@ -0,0 +1,61 @@
+package schema
+
+type AlertGPTDetails struct {
+ AlertID string `json:"alert_id,omitempty"`
+ Severity int `json:"severity,omitempty"`
+ TagRulesApplied string `json:"tag_rules_applied,omitempty"`
+ SeverityLabel string `json:"severity_label,omitempty"`
+ Notes string `json:"notes,omitempty"`
+ DataType string `json:"data_type,omitempty"`
+ Description string `json:"description,omitempty"`
+ StatusLabel string `json:"status_label,omitempty"`
+ Tactic string `json:"tactic,omitempty"`
+ Tags string `json:"tags,omitempty"`
+ Reference string `json:"reference,omitempty"`
+ Protocol string `json:"protocol,omitempty"`
+ Timestamp string `json:"timestamp,omitempty"`
+ Solution string `json:"solution,omitempty"`
+ StatusObservation string `json:"status_observation,omitempty"`
+ Name string `json:"name,omitempty"`
+ IsIncident bool `json:"is_incident,omitempty"`
+ Category string `json:"category,omitempty"`
+ DataSource string `json:"data_source,omitempty"`
+ Logs string `json:"logs,omitempty"`
+ Status int `json:"status,omitempty"`
+ DestinationCountry string `json:"destination_country,omitempty"`
+ DestinationAccuracyRadius int `json:"destination_accuracy_radius,omitempty"`
+ DestinationCity string `json:"destination_city,omitempty"`
+ DestinationIP string `json:"-"`
+ DestinationPort int `json:"destination_port,omitempty"`
+ DestinationCountryCode string `json:"destination_country_code,omitempty"`
+ DestinationIsAnonymousProxy bool `json:"destination_is_anonymous_proxy,omitempty"`
+ DestinationHost string `json:"destination_host,omitempty"`
+ DestinationCoordinates string `json:"destination_coordinates,omitempty"`
+ DestinationIsSatelliteProvider bool `json:"destination_is_satellite_provider,omitempty"`
+ DestinationAso string `json:"destination_aso,omitempty"`
+ DestinationAsn int `json:"destination_asn,omitempty"`
+ DestinationUser string `json:"-"`
+ SourceCountry string `json:"source_country,omitempty"`
+ SourceAccuracyRadius int `json:"source_accuracy_radius,omitempty"`
+ SourceCity string `json:"source_city,omitempty"`
+ SourceIP string `json:"-"`
+ SourceCoordinates string `json:"source_coordinates,omitempty"`
+ SourcePort int `json:"source_port,omitempty"`
+ SourceCountryCode string `json:"source_country_code,omitempty"`
+ SourceIsAnonymousProxy bool `json:"source_is_anonymous_proxy,omitempty"`
+ SourceIsSatelliteProvider bool `json:"source_is_satellite_provider,omitempty"`
+ SourceHost string `json:"source_host,omitempty"`
+ SourceAso string `json:"source_aso,omitempty"`
+ SourceAsn int `json:"source_asn,omitempty"`
+ SourceUser string `json:"-"`
+ IncidentCreatedBy string `json:"incident_created_by,omitempty"`
+ IncidentObservation string `json:"incident_observation,omitempty"`
+ IncidentSource string `json:"incident_source,omitempty"`
+ IncidentCreationDate string `json:"incident_creation_date,omitempty"`
+ IncidentName string `json:"incident_name,omitempty"`
+ IncidentID string `json:"incident_id,omitempty"`
+ GPTTimestamp string `json:"gpt_timestamp,omitempty"`
+ GPTClassification string `json:"gpt_classification,omitempty"`
+ GPTReasoning string `json:"gpt_reasoning,omitempty"`
+ GPTNextSteps string `json:"gpt_next_steps,omitempty"`
+}
diff --git a/soc-ai/schema/schema.go b/soc-ai/schema/schema.go
new file mode 100644
index 000000000..08f59b9a4
--- /dev/null
+++ b/soc-ai/schema/schema.go
@@ -0,0 +1,195 @@
+package schema
+
+type SearchResult struct{}
+
+type SeachRequest struct{}
+
+type SearchDetailsRequest []struct {
+ Field string `json:"field"`
+ Operator string `json:"operator"`
+ Value string `json:"value"`
+}
+
+type GPTRequest struct {
+ Model string `json:"model"`
+ Messages []GPTMessage `json:"messages"`
+}
+
+type GPTMessage struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+}
+
+type GPTResponse struct {
+ ID string `json:"id"`
+ Object string `json:"object"`
+ Created int `json:"created"`
+ Model string `json:"model"`
+ Choices []GPTChoice `json:"choices"`
+ Usage GPTUsage `json:"usage"`
+ SystemFingerprint string `json:"system_fingerprint"`
+}
+
+type GPTChoice struct {
+ Index int `json:"index"`
+ Message GPTMessage `json:"message"`
+ LogProbs string `json:"logprobs"`
+ FinishReason string `json:"finish_reason"`
+}
+
+type GPTUsage struct {
+ PromptTokens int `json:"prompt_tokens"`
+ CompletionTokens int `json:"completion_tokens"`
+ TotalTokens int `json:"total_tokens"`
+}
+
+type GPTAlertResponse struct {
+ Timestamp string `json:"@timestamp,omitempty"`
+ Status string `json:"status,omitempty"`
+ Severity int `json:"severity,omitempty"`
+ Category string `json:"category,omitempty"`
+ AlertName string `json:"alertName,omitempty"`
+ ActivityID string `json:"activityId,omitempty"`
+ Classification string `json:"classification,omitempty"`
+ Reasoning []string `json:"reasoning,omitempty"`
+ NextSteps []NextStep `json:"nextSteps,omitempty"`
+}
+
+type NextStep struct {
+ Step int `json:"step"`
+ Action string `json:"action"`
+ Details string `json:"details"`
+}
+
+type ChangeAlertStatus struct {
+ AlertIDs []string `json:"alertIds"`
+ StatusObservation string `json:"statusObservation"`
+ Status int `json:"status"`
+}
+
+type CreateNewIncidentRequest struct {
+ IncidentName string `json:"incidentName"`
+ IncidentDescription string `json:"incidentDescription"`
+ IncidentAssignedTo string `json:"incidentAssignedTo"`
+ AlertList AlertList `json:"alertList"`
+}
+
+type AlertList []struct {
+ AlertID string `json:"alertId"`
+ AlertName string `json:"alertName"`
+ AlertStatus int `json:"alertStatus"`
+ AlertSeverity int `json:"alertSeverity"`
+}
+
+type IncidentResp struct {
+ ID int `json:"id"`
+ IncidentName string `json:"incidentName"`
+ IncidentDescription string `json:"incidentDescription"`
+ IncidentStatus string `json:"incidentStatus"`
+ IncidentAssignedTo string `json:"incidentAssignedTo"`
+ IncidentSeverity int `json:"incidentSeverity"`
+ IncidentCreatedDate string `json:"incidentCreatedDate"`
+ IncidentSolution string `json:"incidentSolution"`
+}
+
+type AddNewAlertToIncidentRequest struct {
+ IncidenId int `json:"incidentId"`
+ AlertList AlertList `json:"alertList"`
+}
+
+type AlertDetails []Alert
+
+type Alert struct {
+ ID string `json:"id"`
+ Severity int `json:"severity"`
+ TagRulesApplied string `json:"TagRulesApplied,omitempty"`
+ SeverityLabel string `json:"severityLabel"`
+ Notes string `json:"notes"`
+ DataType string `json:"dataType"`
+ Description string `json:"description"`
+ StatusLabel string `json:"statusLabel"`
+ Tactic string `json:"tactic"`
+ Tags []string `json:"tags"`
+ Reference []string `json:"reference"`
+ Protocol string `json:"protocol"`
+ Timestamp string `json:"@timestamp"`
+ Solution string `json:"solution"`
+ StatusObservation string `json:"statusObservation"`
+ Name string `json:"name"`
+ IsIncident bool `json:"isIncident"`
+ Category string `json:"category"`
+ DataSource string `json:"dataSource"`
+ Logs []string `json:"logs"`
+ Status int `json:"status"`
+ Destination Destination `json:"destination"`
+ Source Source `json:"source"`
+ IncidentDetails IncidentDetail `json:"incidentDetail"`
+}
+
+type Destination struct {
+ Country string `json:"country"`
+ AccuracyRadius int `json:"accuracyRadius"`
+ City string `json:"city"`
+ IP string `json:"ip,omitempty"`
+ Port int `json:"port"`
+ CountryCode string `json:"countryCode"`
+ IsAnonymousProxy bool `json:"isAnonymousProxy"`
+ Host string `json:"host,omitempty"`
+ Coordinates []float64 `json:"coordinates"`
+ IsSatelliteProvider bool `json:"isSatelliteProvider"`
+ Aso string `json:"aso"`
+ Asn int `json:"asn"`
+ User string `json:"user"`
+}
+
+type Source struct {
+ Country string `json:"country"`
+ AccuracyRadius int `json:"accuracyRadius"`
+ City string `json:"city"`
+ IP string `json:"ip,omitempty"`
+ Coordinates []float64 `json:"coordinates"`
+ Port int `json:"port"`
+ CountryCode string `json:"countryCode"`
+ IsAnonymousProxy bool `json:"isAnonymousProxy"`
+ IsSatelliteProvider bool `json:"isSatelliteProvider"`
+ Host string `json:"host,omitempty"`
+ Aso string `json:"aso"`
+ Asn int `json:"asn"`
+ User string `json:"user"`
+}
+
+type IncidentDetail struct {
+ CreatedBy string `json:"createdBy"`
+ Observation string `json:"observation"`
+ Source string `json:"source"`
+ CreationDate string `json:"creationDate"`
+ IncidentName string `json:"incidentName,omitempty"`
+ IncidentID string `json:"incidentId,omitempty"`
+}
+
+type UpdateDocRequest struct {
+ Query Query `json:"query"`
+ Script Script `json:"script"`
+}
+
+type Query struct {
+ Bool `json:"bool"`
+}
+
+type Bool struct {
+ Must []Must `json:"must"`
+}
+
+type Must struct {
+ Match Match `json:"match"`
+}
+
+type Match struct {
+ ActivityID string `json:"activityId"`
+}
+
+type Script struct {
+ Source string `json:"source"`
+ Lang string `json:"lang"`
+ Params GPTAlertResponse `json:"params"`
+}
diff --git a/soc-ai/utils/check.go b/soc-ai/utils/check.go
new file mode 100644
index 000000000..dd0f48632
--- /dev/null
+++ b/soc-ai/utils/check.go
@@ -0,0 +1,41 @@
+package utils
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+)
+
+func ConnectionChecker(url string) error {
+ checkConn := func() error {
+ if err := checkConnection(url); err != nil {
+ return fmt.Errorf("connection failed: %v", err)
+ }
+ return nil
+ }
+
+ if err := Logger.InfiniteRetryIfXError(checkConn, "connection failed"); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func checkConnection(url string) error {
+ client := &http.Client{
+ Timeout: 30 * time.Second,
+ }
+
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/soc-ai/utils/convert.go b/soc-ai/utils/convert.go
new file mode 100644
index 000000000..caf07fc16
--- /dev/null
+++ b/soc-ai/utils/convert.go
@@ -0,0 +1,25 @@
+package utils
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+func ConvertFromStructToJsonString(alert interface{}) (string, error) {
+ bytes, err := json.Marshal(alert)
+ if err != nil {
+ return "", fmt.Errorf("error marshalling alert: %v", err)
+ }
+
+ return string(bytes), nil
+}
+
+func ConvertFromJsonToStruct[responseType any](jsonString string) (responseType, error) {
+ var response responseType
+ err := json.Unmarshal([]byte(jsonString), &response)
+ if err != nil {
+ return *new(responseType), fmt.Errorf("error unmarshalling GPT response: %v", err)
+ }
+
+ return response, nil
+}
diff --git a/soc-ai/utils/env.go b/soc-ai/utils/env.go
new file mode 100644
index 000000000..99979fcfd
--- /dev/null
+++ b/soc-ai/utils/env.go
@@ -0,0 +1,21 @@
+package utils
+
+import (
+ "log"
+ "os"
+)
+
+func Getenv(key string, isMandatory bool) string {
+ value, defined := os.LookupEnv(key)
+ if !defined {
+ if isMandatory {
+ log.Fatalf("Error loading environment variable: %s: environment variable does not exist\n", key)
+ } else {
+ return ""
+ }
+ }
+ if (value == "" || value == " ") && isMandatory {
+ log.Fatalf("Error loading environment variable: %s: empty environment variable\n", key)
+ }
+ return value
+}
diff --git a/soc-ai/utils/files.go b/soc-ai/utils/files.go
new file mode 100644
index 000000000..d483f886d
--- /dev/null
+++ b/soc-ai/utils/files.go
@@ -0,0 +1,35 @@
+package utils
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+func GetMyPath() (string, error) {
+ ex, err := os.Executable()
+ if err != nil {
+ return "", err
+ }
+ exPath := filepath.Dir(ex)
+ return exPath, nil
+}
+
+// CreatePathIfNotExist creates a specific path if not exist
+func CreatePathIfNotExist(path string) error {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ if err := os.Mkdir(path, 0755); err != nil {
+ return fmt.Errorf("error creating path: %v", err)
+ }
+ } else if err != nil {
+ return fmt.Errorf("error checking path: %v", err)
+ }
+ return nil
+}
+
+func CheckIfPathExist(path string) bool {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return false
+ }
+ return true
+}
diff --git a/soc-ai/utils/logger.go b/soc-ai/utils/logger.go
new file mode 100644
index 000000000..4332e7f5a
--- /dev/null
+++ b/soc-ai/utils/logger.go
@@ -0,0 +1,31 @@
+package utils
+
+import (
+ "log"
+ "os"
+ "strconv"
+
+ "github.com/threatwinds/logger"
+)
+
+var Logger *logger.Logger
+
+func init() {
+ lenv := os.Getenv("LOG_LEVEL")
+ var level int
+ var err error
+
+ if lenv != "" && lenv != " " {
+ level, err = strconv.Atoi(lenv)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ } else {
+ level = 200
+ }
+
+ Logger = logger.NewLogger(&logger.Config{
+ Format: "text",
+ Level: level,
+ })
+}
diff --git a/soc-ai/utils/request.go b/soc-ai/utils/request.go
new file mode 100644
index 000000000..8ae06dc58
--- /dev/null
+++ b/soc-ai/utils/request.go
@@ -0,0 +1,63 @@
+package utils
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "time"
+)
+
+func DoParseReq[response any](url string, data []byte, method string, headers map[string]string, timeoutInSec int) (response, int, error) {
+ body, status, err := DoReq(url, data, method, headers, timeoutInSec)
+ if err != nil {
+ return *new(response), status, fmt.Errorf("error reading response body: %v", err)
+ }
+
+ var result response
+
+ err = json.Unmarshal(body, &result)
+ if err != nil {
+ return *new(response), http.StatusInternalServerError, fmt.Errorf("error decoding response: %v", err)
+ }
+
+ if status != http.StatusAccepted && status != http.StatusOK {
+ return result, status, fmt.Errorf("status code '%d' received '%s' while sending '%s'", status, body, data)
+ }
+
+ return result, status, nil
+}
+
+func DoReq(url string, data []byte, method string, headers map[string]string, timeoutInSec int) ([]byte, int, error) {
+ req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
+ if err != nil {
+ return nil, http.StatusInternalServerError, fmt.Errorf("error creating request: %v", err)
+ }
+
+ for k, v := range headers {
+ req.Header.Add(k, v)
+ }
+
+ client := &http.Client{
+ Timeout: time.Duration(timeoutInSec) * time.Second,
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+ return nil, http.StatusInternalServerError, fmt.Errorf("request timed out: %v: %s", err, data)
+ }
+ return nil, http.StatusInternalServerError, fmt.Errorf("error performing request: %v: %s", err, data)
+ }
+
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, http.StatusInternalServerError, fmt.Errorf("error reading response body: %v", err)
+ }
+
+ return body, resp.StatusCode, nil
+}
diff --git a/version.yml b/version.yml
index 21c159ed9..40b613fa8 100644
--- a/version.yml
+++ b/version.yml
@@ -1 +1 @@
-version: 10.7.3
\ No newline at end of file
+version: 10.8.0
\ No newline at end of file