diff --git a/backend/modules/eventprocessing/domain/correlation_rule.go b/backend/modules/eventprocessing/domain/correlation_rule.go index 60d0fec76..3632743ea 100644 --- a/backend/modules/eventprocessing/domain/correlation_rule.go +++ b/backend/modules/eventprocessing/domain/correlation_rule.go @@ -1,6 +1,8 @@ package domain -import "time" +import ( + "time" +) type UtmCorrelationRules struct { ID int64 `gorm:"column:id;primaryKey"` @@ -28,7 +30,10 @@ type UtmCorrelationRules struct { SystemOwner bool `gorm:"column:system_owner;not null"` // Nullable JSON TEXT columns for advanced rule configuration. + //[deprecated] only kept for compatibility AfterEventsDef string `gorm:"column:rule_after_events_def"` + // + RuleGroupByDef string `gorm:"column:rule_group_by_def"` DeduplicateByDef string `gorm:"column:rule_deduplicate_by_def"` @@ -38,4 +43,5 @@ type UtmCorrelationRules struct { DataTypes []UtmDataTypes `gorm:"many2many:utm_group_rules_data_type;joinForeignKey:rule_id;joinReferences:data_type_id"` } + func (UtmCorrelationRules) TableName() string { return "utm_correlation_rules" } diff --git a/backend/modules/eventprocessing/dto/correlation_rule.go b/backend/modules/eventprocessing/dto/correlation_rule.go index 22d6349ca..0d58af5a2 100644 --- a/backend/modules/eventprocessing/dto/correlation_rule.go +++ b/backend/modules/eventprocessing/dto/correlation_rule.go @@ -23,6 +23,7 @@ type RuleDataTypeResponse struct { SystemOwner bool `json:"systemOwner"` } + type CreateCorrelationRuleRequest struct { // JSON tags match Java UtmCorrelationRulesDTO field names for wire compatibility. RuleName string `json:"name"` @@ -38,15 +39,17 @@ type CreateCorrelationRuleRequest struct { RuleReferencesDef json.RawMessage `json:"references"` RuleDefinitionDef json.RawMessage `json:"definition"` - AfterEventsDef json.RawMessage `json:"afterEvents"` RuleGroupByDef json.RawMessage `json:"groupBy"` DeduplicateByDef json.RawMessage `json:"deduplicateBy"` RuleActive bool `json:"ruleActive"` + CorrelationDef json.RawMessage `json:"correlation"` DataTypes []DataTypeRef `json:"dataTypes"` } + + type UpdateCorrelationRuleRequest struct { // RelPath identifies the rule to update (the YAML-direct identity). RelPath string `json:"relPath"` @@ -64,15 +67,16 @@ type UpdateCorrelationRuleRequest struct { RuleReferencesDef json.RawMessage `json:"references"` RuleDefinitionDef json.RawMessage `json:"definition"` - AfterEventsDef json.RawMessage `json:"afterEvents"` RuleGroupByDef json.RawMessage `json:"groupBy"` DeduplicateByDef json.RawMessage `json:"deduplicateBy"` RuleActive bool `json:"ruleActive"` + CorrelationDef json.RawMessage `json:"correlation"` DataTypes []DataTypeRef `json:"dataTypes"` } + type CorrelationRuleResponse struct { // RelPath is the rule identity (replaces the legacy numeric id). RelPath string `json:"relPath"` @@ -90,7 +94,7 @@ type CorrelationRuleResponse struct { RuleReferencesDef json.RawMessage `json:"references"` RuleDefinitionDef json.RawMessage `json:"definition"` - AfterEventsDef json.RawMessage `json:"afterEvents"` + CorrelationDef json.RawMessage `json:"correlation"` RuleGroupByDef json.RawMessage `json:"groupBy"` DeduplicateByDef json.RawMessage `json:"deduplicateBy"` diff --git a/backend/modules/eventprocessing/module.go b/backend/modules/eventprocessing/module.go index 538349033..0089afed8 100644 --- a/backend/modules/eventprocessing/module.go +++ b/backend/modules/eventprocessing/module.go @@ -126,5 +126,5 @@ func (m *Module) GetIngestionStatsUsecase() connectors.IngestionStatsUsecase { } func (m *Module) AfterEventsByRuleName(name string) (json.RawMessage, bool) { - return m.ruleStore.AfterEventsByName(name) + return m.ruleStore.CorrelationsByName(name) } diff --git a/backend/modules/eventprocessing/usecase/correlation_rule.go b/backend/modules/eventprocessing/usecase/correlation_rule.go index bfbbcf792..24f4ae46c 100644 --- a/backend/modules/eventprocessing/usecase/correlation_rule.go +++ b/backend/modules/eventprocessing/usecase/correlation_rule.go @@ -24,16 +24,21 @@ func NewCorrelationRuleUsecase(store *RuleStore) connectors.CorrelationRuleUseca return &correlationRuleUsecase{store: store} } + + func (u *correlationRuleUsecase) Create(_ context.Context, req dto.CreateCorrelationRuleRequest) error { if len(req.DataTypes) == 0 { return domain.ErrDataTypesRequired } - if err := validateRuleContent(req.RuleDefinitionDef, req.AfterEventsDef); err != nil { + + correlate:= req.CorrelationDef + + if err := validateRuleContent(req.RuleDefinitionDef,correlate ); err != nil { return err } rule := buildRule(req.RuleName, req.RuleAdversary, req.RuleConfidentiality, req.RuleIntegrity, req.RuleAvailability, req.RuleCategory, req.RuleTechnique, req.RuleDescription, - req.RuleReferencesDef, req.RuleDefinitionDef, req.AfterEventsDef, req.RuleGroupByDef, + req.RuleReferencesDef, req.RuleDefinitionDef, correlate, req.RuleGroupByDef, req.DeduplicateByDef, req.DataTypes) created, err := u.store.Create(rule) @@ -105,10 +110,10 @@ func validateImportedRule(r Rule) error { if strings.TrimSpace(r.Where) == "" { return domain.ErrCorrelationRuleNullDefinition } - if r.AfterEvents == nil { + if r.Correlation == nil { return nil } - raw, err := json.Marshal(r.AfterEvents) + raw, err := json.Marshal(r.Correlation) if err != nil { return fmt.Errorf("%w: afterEvents is not serializable", domain.ErrCorrelationRuleInvalidContent) } @@ -131,12 +136,15 @@ func (u *correlationRuleUsecase) Update(_ context.Context, req dto.UpdateCorrela if len(req.DataTypes) == 0 { return domain.ErrDataTypesRequired } - if err := validateRuleContent(req.RuleDefinitionDef, req.AfterEventsDef); err != nil { + + correlate:= req.CorrelationDef + + if err := validateRuleContent(req.RuleDefinitionDef, correlate); err != nil { return err } rule := buildRule(req.RuleName, req.RuleAdversary, req.RuleConfidentiality, req.RuleIntegrity, req.RuleAvailability, req.RuleCategory, req.RuleTechnique, req.RuleDescription, - req.RuleReferencesDef, req.RuleDefinitionDef, req.AfterEventsDef, req.RuleGroupByDef, + req.RuleReferencesDef, req.RuleDefinitionDef, correlate, req.RuleGroupByDef, req.DeduplicateByDef, req.DataTypes) if _, err := u.store.Update(req.RelPath, rule); err != nil { @@ -210,7 +218,7 @@ func buildRule(name, adversary string, conf, integ, avail int, category, techniq Impact: Impact{Confidentiality: conf, Integrity: integ, Availability: avail}, Where: rawToWhere(def), References: rawToAnySlice(refs), - AfterEvents: rawToAny(after), + Correlation: rawToAny(after), GroupBy: rawToStrSlice(groupBy), DeduplicateBy: rawToStrSlice(dedup), } @@ -237,7 +245,7 @@ func storedToResponse(sr *StoredRule) *dto.CorrelationRuleResponse { RuleDescription: sr.Description, RuleReferencesDef: anyToRaw(sr.References), RuleDefinitionDef: anyToRaw(sr.Where), - AfterEventsDef: anyToRaw(sr.AfterEvents), + CorrelationDef: anyToRaw(sr.Correlation), RuleGroupByDef: anyToRaw(sr.GroupBy), DeduplicateByDef: anyToRaw(sr.DeduplicateBy), RuleLastUpdate: &mod, diff --git a/backend/modules/eventprocessing/usecase/rule_bootstrap.go b/backend/modules/eventprocessing/usecase/rule_bootstrap.go index 258480d9d..0d80a2fd5 100644 --- a/backend/modules/eventprocessing/usecase/rule_bootstrap.go +++ b/backend/modules/eventprocessing/usecase/rule_bootstrap.go @@ -264,6 +264,8 @@ func legacyToRule(row *domain.UtmCorrelationRules) Rule { for _, dt := range row.DataTypes { names = append(names, dt.DataType) } + + return Rule{ Name: row.RuleName, Adversary: row.RuleAdversary, @@ -274,7 +276,7 @@ func legacyToRule(row *domain.UtmCorrelationRules) Rule { Impact: Impact{Confidentiality: row.RuleConfidentiality, Integrity: row.RuleIntegrity, Availability: row.RuleAvailability}, Where: rawToWhere(toRaw(row.RuleDefinitionDef)), References: rawToAnySlice(toRaw(row.RuleReferencesDef)), - AfterEvents: rawToAny(toRaw(row.AfterEventsDef)), + Correlation: rawToAny(toRaw(row.AfterEventsDef)), GroupBy: rawToStrSlice(toRaw(row.RuleGroupByDef)), DeduplicateBy: rawToStrSlice(toRaw(row.DeduplicateByDef)), } diff --git a/backend/modules/eventprocessing/usecase/rule_model.go b/backend/modules/eventprocessing/usecase/rule_model.go index e2a88dd12..b48bcd2d1 100644 --- a/backend/modules/eventprocessing/usecase/rule_model.go +++ b/backend/modules/eventprocessing/usecase/rule_model.go @@ -38,7 +38,7 @@ type Rule struct { References []any `yaml:"references,omitempty"` Description string `yaml:"description,omitempty"` Where string `yaml:"where"` - AfterEvents any `yaml:"afterEvents,omitempty"` + Correlation any `yaml:"correlation,omitempty"` GroupBy []string `yaml:"groupBy,omitempty"` DeduplicateBy []string `yaml:"deduplicateBy,omitempty"` } diff --git a/backend/modules/eventprocessing/usecase/rule_store.go b/backend/modules/eventprocessing/usecase/rule_store.go index 3ff949ca9..fa24be62e 100644 --- a/backend/modules/eventprocessing/usecase/rule_store.go +++ b/backend/modules/eventprocessing/usecase/rule_store.go @@ -162,12 +162,12 @@ func (s *RuleStore) FindByName(name string) *StoredRule { return nil } -func (s *RuleStore) AfterEventsByName(name string) (json.RawMessage, bool) { +func (s *RuleStore) CorrelationsByName(name string) (json.RawMessage, bool) { sr := s.FindByName(name) if sr == nil { return nil, false } - return anyToRaw(sr.AfterEvents), true + return anyToRaw(sr.Correlation), true } // List applies the filter, sorts by name and paginates. It returns the page diff --git a/frontend/src/features/alerting-rules/components/rule-form.tsx b/frontend/src/features/alerting-rules/components/rule-form.tsx index 1de2d0c70..8f933bba0 100644 --- a/frontend/src/features/alerting-rules/components/rule-form.tsx +++ b/frontend/src/features/alerting-rules/components/rule-form.tsx @@ -35,7 +35,7 @@ export interface RuleFormState { ruleActive: boolean dataTypes: string[] definition: string - afterEvents: AfterStep[] + correlation: AfterStep[] groupBy: string[] deduplicateBy: string[] references: string[] @@ -59,9 +59,9 @@ function parseConditions(w: unknown): Condition[] { return { field: String(o.field ?? ''), operator: String(o.operator ?? 'filter_term'), value: valueToStr(o.value) } }) } -function parseSteps(after: unknown): AfterStep[] { - if (!Array.isArray(after)) return [] - return after.map((s) => { +function parseSteps(raw: unknown): AfterStep[] { + if (!Array.isArray(raw)) return [] + return raw.map((s) => { const o = (s ?? {}) as Record return { indexPattern: String(o.indexPattern ?? ''), @@ -86,7 +86,7 @@ export function ruleToForm(r?: CorrelationRule): RuleFormState { ruleActive: r?.ruleActive ?? true, dataTypes: (r?.dataTypes ?? []).filter((d) => d.included).map((d) => d.dataType), definition: r?.definition ?? '', - afterEvents: parseSteps(r?.afterEvents), + correlation: parseSteps(r?.correlation ?? r?.afterEvents), groupBy: strArray(r?.groupBy), deduplicateBy: strArray(r?.deduplicateBy), references: strArray(r?.references), @@ -122,7 +122,7 @@ export function formToInput(f: RuleFormState, relPath?: string): SaveRuleInput { description: f.description.trim(), references: f.references.filter(Boolean), definition: f.definition, - afterEvents: stepsToJson(f.afterEvents), + correlation: stepsToJson(f.correlation), groupBy: f.groupBy.filter(Boolean), deduplicateBy: f.deduplicateBy.filter(Boolean), ruleActive: f.ruleActive, @@ -134,7 +134,7 @@ export function formToInput(f: RuleFormState, relPath?: string): SaveRuleInput { export function RuleForm({ form, setForm, dataTypeOptions, t }: { form: RuleFormState; setForm: Dispatch>; dataTypeOptions: DataTypeOption[]; t: TFunction }) { const set = (k: K, v: RuleFormState[K]) => setForm((f) => ({ ...f, [k]: v })) - const setSteps = (steps: AfterStep[]) => set('afterEvents', steps) + const setSteps = (steps: AfterStep[]) => set('correlation', steps) // `where` condition: visual builder ⇄ raw CEL. Visual is the source when it // parses; otherwise we fall back to code for hand-written/advanced CEL. @@ -221,21 +221,21 @@ export function RuleForm({ form, setForm, dataTypeOptions, t }: { form: RuleForm )} -
-

{t('alertingRules.editor.afterEventsHint')}

+
+

{t('alertingRules.editor.correlationStepsHint')}

- {form.afterEvents.map((step, i) => ( + {form.correlation.map((step, i) => ( setSteps(form.afterEvents.map((x, idx) => (idx === i ? s : x)))} - onRemove={() => setSteps(form.afterEvents.filter((_, idx) => idx !== i))} + onChange={(s) => setSteps(form.correlation.map((x, idx) => (idx === i ? s : x)))} + onRemove={() => setSteps(form.correlation.filter((_, idx) => idx !== i))} /> ))}
{steps.length > 0 && ( -
+
)} @@ -607,7 +607,7 @@ function CelNodeView({ node, t, depth }: { node: CelNode; t: TFunction; depth: n } /** Read-only cards for the correlation (after-events) steps. */ -function AfterEventsView({ steps, t }: { steps: ReturnType['afterEvents']; t: TFunction }) { +function AfterEventsView({ steps, t }: { steps: ReturnType['correlation']; t: TFunction }) { return (
{steps.map((s, i) => ( diff --git a/frontend/src/features/alerting-rules/services/alerting-rules-http.service.ts b/frontend/src/features/alerting-rules/services/alerting-rules-http.service.ts index 4ff6257f9..c5a8588af 100644 --- a/frontend/src/features/alerting-rules/services/alerting-rules-http.service.ts +++ b/frontend/src/features/alerting-rules/services/alerting-rules-http.service.ts @@ -46,8 +46,10 @@ export interface ImportRulesResponse { /** * A correlation (alerting) rule. Identity is `relPath` (YAML-direct). `definition` - * is the CEL `where` condition (a JSON-encoded string); `afterEvents`/`references`/ + * is the CEL `where` condition (a JSON-encoded string); `correlation`/`references`/ * `groupBy`/`deduplicateBy` are arbitrary JSON the backend stores verbatim. + * `afterEvents` is the legacy name for `correlation` — still populated by older + * backends/rule files, read as a fallback. */ export interface CorrelationRule { relPath: string @@ -61,7 +63,8 @@ export interface CorrelationRule { description: string references: unknown definition: string - afterEvents: unknown + correlation?: unknown + afterEvents?: unknown // legacy: pre-rename field name groupBy: unknown deduplicateBy: unknown ruleLastUpdate?: string @@ -82,7 +85,7 @@ export interface SaveRuleInput { description: string references: unknown definition: string - afterEvents: unknown + correlation: unknown groupBy: unknown deduplicateBy: unknown ruleActive: boolean diff --git a/frontend/src/shared/i18n/locales/de.json b/frontend/src/shared/i18n/locales/de.json index ab072ba04..95c1cfc0c 100644 --- a/frontend/src/shared/i18n/locales/de.json +++ b/frontend/src/shared/i18n/locales/de.json @@ -4213,7 +4213,7 @@ "updated": "Aktualisiert", "relPath": "Pfad", "definition": "Bedingung (CEL)", - "afterEvents": "Korrelationsschritte", + "correlationSteps": "Korrelationsschritte", "correlation": "Korrelation", "groupBy": "Gruppieren nach", "deduplicateBy": "Deduplizieren nach", @@ -4236,8 +4236,8 @@ "definitionSection": "Definition", "where": "Bedingung (CEL)", "whereHint": "CEL-Ausdruck je Ereignis ausgewertet", - "afterEvents": "Korrelationsschritte (afterEvents)", - "afterEventsHint": "JSON-Array", + "correlationSteps": "Korrelationsschritte", + "correlationStepsHint": "JSON-Array", "jsonArray": "JSON-Array", "nameRequired": "Name ist erforderlich", "definitionRequired": "Die Bedingung ist erforderlich", diff --git a/frontend/src/shared/i18n/locales/en.json b/frontend/src/shared/i18n/locales/en.json index 907ba36bf..4106196e5 100644 --- a/frontend/src/shared/i18n/locales/en.json +++ b/frontend/src/shared/i18n/locales/en.json @@ -4465,7 +4465,7 @@ "updated": "Updated", "relPath": "Path", "definition": "Condition (CEL)", - "afterEvents": "Correlation steps", + "correlationSteps": "Correlation steps", "correlation": "Correlation", "groupBy": "Group by", "deduplicateBy": "Deduplicate by", @@ -4488,8 +4488,8 @@ "definitionSection": "Definition", "where": "Condition (CEL)", "whereHint": "CEL expression evaluated per event", - "afterEvents": "Correlation steps (afterEvents)", - "afterEventsHint": "JSON array", + "correlationSteps": "Correlation steps", + "correlationStepsHint": "JSON array", "jsonArray": "JSON array", "nameRequired": "Name is required", "definitionRequired": "The condition is required", diff --git a/frontend/src/shared/i18n/locales/es.json b/frontend/src/shared/i18n/locales/es.json index 54345a314..84e0aef53 100644 --- a/frontend/src/shared/i18n/locales/es.json +++ b/frontend/src/shared/i18n/locales/es.json @@ -4389,7 +4389,7 @@ "updated": "Actualizada", "relPath": "Ruta", "definition": "Condición (CEL)", - "afterEvents": "Pasos de correlación", + "correlationSteps": "Pasos de correlación", "correlation": "Correlación", "groupBy": "Agrupar por", "deduplicateBy": "Deduplicar por", @@ -4412,8 +4412,8 @@ "definitionSection": "Definición", "where": "Condición (CEL)", "whereHint": "Expresión CEL evaluada por evento", - "afterEvents": "Pasos de correlación (afterEvents)", - "afterEventsHint": "array JSON", + "correlationSteps": "Pasos de correlación", + "correlationStepsHint": "array JSON", "jsonArray": "array JSON", "nameRequired": "El nombre es obligatorio", "definitionRequired": "La condición es obligatoria", diff --git a/frontend/src/shared/i18n/locales/fr.json b/frontend/src/shared/i18n/locales/fr.json index 78ed500ac..9db62b374 100644 --- a/frontend/src/shared/i18n/locales/fr.json +++ b/frontend/src/shared/i18n/locales/fr.json @@ -4213,7 +4213,7 @@ "updated": "Mise à jour", "relPath": "Chemin", "definition": "Condition (CEL)", - "afterEvents": "Étapes de corrélation", + "correlationSteps": "Étapes de corrélation", "correlation": "Corrélation", "groupBy": "Grouper par", "deduplicateBy": "Dédupliquer par", @@ -4236,8 +4236,8 @@ "definitionSection": "Définition", "where": "Condition (CEL)", "whereHint": "Expression CEL évaluée par événement", - "afterEvents": "Étapes de corrélation (afterEvents)", - "afterEventsHint": "tableau JSON", + "correlationSteps": "Étapes de corrélation", + "correlationStepsHint": "tableau JSON", "jsonArray": "tableau JSON", "nameRequired": "Le nom est requis", "definitionRequired": "La condition est requise", diff --git a/frontend/src/shared/i18n/locales/it.json b/frontend/src/shared/i18n/locales/it.json index e154f2591..66b78436a 100644 --- a/frontend/src/shared/i18n/locales/it.json +++ b/frontend/src/shared/i18n/locales/it.json @@ -4213,7 +4213,7 @@ "updated": "Aggiornata", "relPath": "Percorso", "definition": "Condizione (CEL)", - "afterEvents": "Passi di correlazione", + "correlationSteps": "Passi di correlazione", "correlation": "Correlazione", "groupBy": "Raggruppa per", "deduplicateBy": "Deduplica per", @@ -4236,8 +4236,8 @@ "definitionSection": "Definizione", "where": "Condizione (CEL)", "whereHint": "Espressione CEL valutata per evento", - "afterEvents": "Passi di correlazione (afterEvents)", - "afterEventsHint": "array JSON", + "correlationSteps": "Passi di correlazione", + "correlationStepsHint": "array JSON", "jsonArray": "array JSON", "nameRequired": "Il nome è obbligatorio", "definitionRequired": "La condizione è obbligatoria", diff --git a/frontend/src/shared/i18n/locales/pt.json b/frontend/src/shared/i18n/locales/pt.json index 9d74d6531..e5037cd1c 100644 --- a/frontend/src/shared/i18n/locales/pt.json +++ b/frontend/src/shared/i18n/locales/pt.json @@ -4389,7 +4389,7 @@ "updated": "Atualizada", "relPath": "Caminho", "definition": "Condição (CEL)", - "afterEvents": "Passos de correlação", + "correlationSteps": "Passos de correlação", "correlation": "Correlação", "groupBy": "Agrupar por", "deduplicateBy": "Deduplicar por", @@ -4412,8 +4412,8 @@ "definitionSection": "Definição", "where": "Condição (CEL)", "whereHint": "Expressão CEL avaliada por evento", - "afterEvents": "Passos de correlação (afterEvents)", - "afterEventsHint": "array JSON", + "correlationSteps": "Passos de correlação", + "correlationStepsHint": "array JSON", "jsonArray": "array JSON", "nameRequired": "O nome é obrigatório", "definitionRequired": "A condição é obrigatória", diff --git a/frontend/src/shared/i18n/locales/ru.json b/frontend/src/shared/i18n/locales/ru.json index a7788d675..3f062dc26 100644 --- a/frontend/src/shared/i18n/locales/ru.json +++ b/frontend/src/shared/i18n/locales/ru.json @@ -4023,7 +4023,7 @@ "updated": "Обновлено", "relPath": "Путь", "definition": "Условие (CEL)", - "afterEvents": "Шаги корреляции", + "correlationSteps": "Шаги корреляции", "correlation": "Корреляция", "groupBy": "Группировать по", "deduplicateBy": "Дедупликация по", @@ -4046,8 +4046,8 @@ "definitionSection": "Определение", "where": "Условие (CEL)", "whereHint": "Выражение CEL, вычисляемое для каждого события", - "afterEvents": "Шаги корреляции (afterEvents)", - "afterEventsHint": "JSON-массив", + "correlationSteps": "Шаги корреляции", + "correlationStepsHint": "JSON-массив", "jsonArray": "JSON-массив", "nameRequired": "Название обязательно", "definitionRequired": "Условие обязательно",