diff --git a/model/detection.go b/model/detection.go index ea33ce6e..5214c589 100644 --- a/model/detection.go +++ b/model/detection.go @@ -102,6 +102,7 @@ type Detection struct { Title string `json:"title"` Severity Severity `json:"severity"` Author string `json:"author"` + Category string `json:"category,omitempty"` Description string `json:"description"` Content string `json:"content"` IsEnabled bool `json:"isEnabled"` @@ -113,6 +114,10 @@ type Detection struct { Tags []string `json:"tags"` Ruleset *string `json:"ruleset"` License string `json:"license"` + + // elastalert - sigma only + Product string `json:"product,omitempty"` + Service string `json:"service,omitempty"` } type DetectionComment struct { diff --git a/server/modules/elastalert/elastalert.go b/server/modules/elastalert/elastalert.go index 5c010fd1..097a3e01 100644 --- a/server/modules/elastalert/elastalert.go +++ b/server/modules/elastalert/elastalert.go @@ -224,6 +224,18 @@ func (e *ElastAlertEngine) ExtractDetails(detect *model.Detection) error { detect.Description = *rule.Description } + if rule.LogSource.Category != nil { + detect.Category = *rule.LogSource.Category + } + + if rule.LogSource.Product != nil { + detect.Product = *rule.LogSource.Product + } + + if rule.LogSource.Service != nil { + detect.Service = *rule.LogSource.Service + } + if rule.Level != nil { switch strings.ToLower(string(*rule.Level)) { case "informational": diff --git a/server/modules/elastalert/elastalert_test.go b/server/modules/elastalert/elastalert_test.go index 30ef3113..f6224519 100644 --- a/server/modules/elastalert/elastalert_test.go +++ b/server/modules/elastalert/elastalert_test.go @@ -404,6 +404,7 @@ level: high Severity: model.SeverityHigh, Content: data, Description: "Always Alerts", + Product: "windows", IsCommunity: true, Engine: model.EngineNameElastAlert, Language: model.SigLangSigma, @@ -470,6 +471,8 @@ level: high Content: data, Description: "Detects when a user fails to login to the Security Onion Console (Web UI). Review associated logs for target username and source IP.", IsCommunity: true, + Product: "kratos", + Service: "audit", Engine: model.EngineNameElastAlert, Language: model.SigLangSigma, Ruleset: util.Ptr("repo-path"), diff --git a/server/modules/elastalert/validate.go b/server/modules/elastalert/validate.go index 5355f95d..85cdb07d 100644 --- a/server/modules/elastalert/validate.go +++ b/server/modules/elastalert/validate.go @@ -162,5 +162,17 @@ func (r *SigmaRule) ToDetection(content string, ruleset string, license string) det.Description = *r.Description } + if r.LogSource.Category != nil && *r.LogSource.Category != "" { + det.Category = *r.LogSource.Category + } + + if r.LogSource.Product != nil && *r.LogSource.Product != "" { + det.Product = *r.LogSource.Product + } + + if r.LogSource.Service != nil && *r.LogSource.Service != "" { + det.Service = *r.LogSource.Service + } + return det } diff --git a/server/modules/suricata/suricata.go b/server/modules/suricata/suricata.go index 4fdf524e..aa68ebda 100644 --- a/server/modules/suricata/suricata.go +++ b/server/modules/suricata/suricata.go @@ -147,6 +147,28 @@ func (e *SuricataEngine) resetInterrupt() { } } +func checkAndExtractCategory(title string) string { + // Regex to extract the first two words from the title + regex, err := regexp.Compile(`^(\w+)\s+(\w+)`) + if err != nil { + log.WithError(err).Error("unable to compile suricata category extraction regex") + } + + matches := regex.FindStringSubmatch(title) + if len(matches) > 1 { + firstWord := matches[1] + secondWord := matches[2] + + // Check if the first word is one of the keywords + switch firstWord { + case "ET", "ETPRO", "GPL": + return firstWord + " " + secondWord // Return both words if the first is a keyword + } + } + + return "" // Return empty string if no matches or keyword doesn't match +} + func (e *SuricataEngine) IsRunning() bool { return e.isRunning } @@ -169,6 +191,8 @@ func (e *SuricataEngine) ExtractDetails(detect *model.Detection) error { if strings.EqualFold(opt.Name, "msg") && opt.Value != nil { detect.Title = util.Unquote(*opt.Value) + detect.Category = checkAndExtractCategory(detect.Title) + continue } } @@ -516,6 +540,8 @@ func (e *SuricataEngine) ParseRules(content string, ruleset *string) ([]*model.D title = strings.ReplaceAll(title, `\"`, `"`) title = strings.ReplaceAll(title, `\\`, `\`) + category := checkAndExtractCategory(title) + severity := model.SeverityUnknown // TODO: Default severity? md := parsed.ParseMetaData() @@ -539,6 +565,7 @@ func (e *SuricataEngine) ParseRules(content string, ruleset *string) ([]*model.D d := &model.Detection{ Author: socAuthor, + Category: category, PublicID: sid, Title: title, Severity: severity, diff --git a/server/modules/suricata/suricata_test.go b/server/modules/suricata/suricata_test.go index b278f62a..21b83941 100644 --- a/server/modules/suricata/suricata_test.go +++ b/server/modules/suricata/suricata_test.go @@ -320,6 +320,7 @@ func TestParse(t *testing.T) { Author: "__soc_import__", PublicID: SimpleRuleSID, Title: `GPL ATTACK_RESPONSE id check returned root`, + Category: `GPL ATTACK_RESPONSE`, Severity: model.SeverityUnknown, Content: SimpleRule, Engine: model.EngineNameSuricata, @@ -331,6 +332,7 @@ func TestParse(t *testing.T) { Author: "__soc_import__", PublicID: "20000", Title: `a "tricky";\ msg`, + Category: ``, Severity: model.SeverityInformational, Content: `alert http any any <> any any (metadata:signature_severity Informational; sid:"20000"; msg:"a \"tricky\"\;\\ msg";)`, Engine: model.EngineNameSuricata,