Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion backend/modules/eventprocessing/domain/correlation_rule.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package domain

import "time"
import (
"fmt"
"time"
)

type UtmCorrelationRules struct {
ID int64 `gorm:"column:id;primaryKey"`
Expand Down Expand Up @@ -28,7 +31,10 @@ type UtmCorrelationRules struct {
SystemOwner bool `gorm:"column:system_owner;not null"`

// Nullable JSON TEXT columns for advanced rule configuration.
//[deprecated] only keeped for compatibility
AfterEventsDef string `gorm:"column:rule_after_events_def"`
//
CorrelationDef string `gorm:"column:rule_correlation_def"`
RuleGroupByDef string `gorm:"column:rule_group_by_def"`
DeduplicateByDef string `gorm:"column:rule_deduplicate_by_def"`

Expand All @@ -38,4 +44,16 @@ type UtmCorrelationRules struct {
DataTypes []UtmDataTypes `gorm:"many2many:utm_group_rules_data_type;joinForeignKey:rule_id;joinReferences:data_type_id"`
}

func (self *UtmCorrelationRules) GetCorrelationDef() (string,error){
var correlate string
if self.AfterEventsDef!="" && self.CorrelationDef !=""{
return "",fmt.Errorf("only one afterEvents or correlation allowed")
}else if self.AfterEventsDef!="" {
correlate = self.AfterEventsDef
}else{
correlate= self.CorrelationDef
}
return correlate,nil
}

func (UtmCorrelationRules) TableName() string { return "utm_correlation_rules" }
31 changes: 28 additions & 3 deletions backend/modules/eventprocessing/dto/correlation_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dto

import (
"encoding/json"
"fmt"
"time"
)

Expand All @@ -23,7 +24,29 @@ type RuleDataTypeResponse struct {
SystemOwner bool `json:"systemOwner"`
}


type CorrelationOwner struct{
//[deprecated] only keeped for compatibility
AfterEventsDef json.RawMessage `json:"afterEvents"`
//
CorrelationDef json.RawMessage `json:"correlation"`
}

func (self *CorrelationOwner) GetCorrelationDef() (json.RawMessage,error){
var correlate json.RawMessage
if self.AfterEventsDef!=nil && self.CorrelationDef !=nil{
return nil,fmt.Errorf("only one afterEvents or correlation allowed")
}else if self.AfterEventsDef!=nil {
correlate = self.AfterEventsDef
}else{
correlate= self.CorrelationDef
}
return correlate,nil
}


type CreateCorrelationRuleRequest struct {
CorrelationOwner
// JSON tags match Java UtmCorrelationRulesDTO field names for wire compatibility.
RuleName string `json:"name"`
RuleAdversary string `json:"adversary"`
Expand All @@ -38,7 +61,6 @@ 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"`

Expand All @@ -47,7 +69,10 @@ type CreateCorrelationRuleRequest struct {
DataTypes []DataTypeRef `json:"dataTypes"`
}



type UpdateCorrelationRuleRequest struct {
CorrelationOwner
// RelPath identifies the rule to update (the YAML-direct identity).
RelPath string `json:"relPath"`

Expand All @@ -64,7 +89,6 @@ 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"`

Expand All @@ -73,6 +97,7 @@ type UpdateCorrelationRuleRequest struct {
DataTypes []DataTypeRef `json:"dataTypes"`
}


type CorrelationRuleResponse struct {
// RelPath is the rule identity (replaces the legacy numeric id).
RelPath string `json:"relPath"`
Expand All @@ -90,7 +115,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"`

Expand Down
2 changes: 1 addition & 1 deletion backend/modules/eventprocessing/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
30 changes: 22 additions & 8 deletions backend/modules/eventprocessing/usecase/correlation_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,24 @@ 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,err:= req.GetCorrelationDef()
if err!=nil{
return err
}

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)
Expand Down Expand Up @@ -105,10 +113,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)
}
Expand All @@ -131,12 +139,18 @@ 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,err:= req.GetCorrelationDef()
if err!=nil{
return err
}

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 {
Expand Down Expand Up @@ -210,7 +224,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),
}
Expand All @@ -237,7 +251,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,
Expand Down
12 changes: 11 additions & 1 deletion backend/modules/eventprocessing/usecase/rule_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,16 @@ func legacyToRule(row *domain.UtmCorrelationRules) Rule {
for _, dt := range row.DataTypes {
names = append(names, dt.DataType)
}

correlation,err:=row.GetCorrelationDef()
if err!=nil{
catcher.Error("skipping invalid after events of rule, %s",err,map[string]any{
"rule":row.RuleName,
"afterEvents":row.AfterEventsDef,
"correlation":row.CorrelationDef,
})
}

return Rule{
Name: row.RuleName,
Adversary: row.RuleAdversary,
Expand All @@ -274,7 +284,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(correlation)),
GroupBy: rawToStrSlice(toRaw(row.RuleGroupByDef)),
DeduplicateBy: rawToStrSlice(toRaw(row.DeduplicateByDef)),
}
Expand Down
2 changes: 1 addition & 1 deletion backend/modules/eventprocessing/usecase/rule_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
4 changes: 2 additions & 2 deletions backend/modules/eventprocessing/usecase/rule_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 14 additions & 14 deletions frontend/src/features/alerting-rules/components/rule-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface RuleFormState {
ruleActive: boolean
dataTypes: string[]
definition: string
afterEvents: AfterStep[]
correlation: AfterStep[]
groupBy: string[]
deduplicateBy: string[]
references: string[]
Expand All @@ -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<string, unknown>
return {
indexPattern: String(o.indexPattern ?? ''),
Expand All @@ -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),
Expand Down Expand Up @@ -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,
Expand All @@ -134,7 +134,7 @@ export function formToInput(f: RuleFormState, relPath?: string): SaveRuleInput {

export function RuleForm({ form, setForm, dataTypeOptions, t }: { form: RuleFormState; setForm: Dispatch<SetStateAction<RuleFormState>>; dataTypeOptions: DataTypeOption[]; t: TFunction }) {
const set = <K extends keyof RuleFormState>(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.
Expand Down Expand Up @@ -221,21 +221,21 @@ export function RuleForm({ form, setForm, dataTypeOptions, t }: { form: RuleForm
)}
</div>

<Section title={t('alertingRules.editor.afterEvents')}>
<p className="mb-3 text-[11px] text-muted-foreground">{t('alertingRules.editor.afterEventsHint')}</p>
<Section title={t('alertingRules.editor.correlationSteps')}>
<p className="mb-3 text-[11px] text-muted-foreground">{t('alertingRules.editor.correlationStepsHint')}</p>
<div className="space-y-3">
{form.afterEvents.map((step, i) => (
{form.correlation.map((step, i) => (
<StepCard
key={i}
step={step}
index={i}
t={t}
onChange={(s) => 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))}
/>
))}
<button
onClick={() => setSteps([...form.afterEvents, { indexPattern: 'v11-log-*', within: '2m', count: 1, with: [], or: [] }])}
onClick={() => setSteps([...form.correlation, { indexPattern: 'v11-log-*', within: '2m', count: 1, with: [], or: [] }])}
className="flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed border-border py-2 text-xs text-muted-foreground hover:border-primary/40 hover:text-foreground"
>
<Plus size={13} /> {t('alertingRules.editor.addStep')}
Expand All @@ -256,7 +256,7 @@ export function RuleForm({ form, setForm, dataTypeOptions, t }: { form: RuleForm
)
}

/* ─── afterEvents builder pieces ───────────────────────────────────────── */
/* ─── correlation steps builder pieces ────────────────────────────────── */

function StepCard({ step, index, onChange, onRemove, t }: { step: AfterStep; index: number; onChange: (s: AfterStep) => void; onRemove: () => void; t: TFunction }) {
const patch = (p: Partial<AfterStep>) => onChange({ ...step, ...p })
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/features/alerting-rules/lib/rule-yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function ruleFormToYaml(f: RuleFormState): string {
if (f.description) doc.description = f.description
doc.where = f.definition

const steps = f.afterEvents
const steps = f.correlation
.filter((s) => s.indexPattern.trim())
.map((s) => {
const step: Record<string, unknown> = {
Expand All @@ -88,7 +88,7 @@ export function ruleFormToYaml(f: RuleFormState): string {
if (ors.length) step.or = ors.map((g) => ({ with: condsToYaml(g.with) }))
return step
})
if (steps.length) doc.afterEvents = steps
if (steps.length) doc.correlation = steps
if (f.groupBy.length) doc.groupBy = f.groupBy
if (f.deduplicateBy.length) doc.deduplicateBy = f.deduplicateBy

Expand Down Expand Up @@ -126,7 +126,7 @@ export function yamlToRuleForm(content: string): RuleParseResult {
ruleActive: doc.active !== false,
dataTypes: strArray(doc.dataTypes),
definition: str(where),
afterEvents: parseSteps(doc.afterEvents),
correlation: parseSteps(doc.correlation ?? doc.afterEvents),
groupBy: strArray(doc.groupBy),
deduplicateBy: strArray(doc.deduplicateBy),
references: strArray(doc.references),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ function RuleDrawer({
}

function RuleView({ rule, df, t }: { rule: CorrelationRule; df: ReturnType<typeof useDateFormat>; t: TFunction }) {
const steps = ruleToForm(rule).afterEvents
const steps = ruleToForm(rule).correlation
return (
<div className="space-y-4">
{rule.description && <Section title={t('alertingRules.view.description')}><RuleDescription text={rule.description} /></Section>}
Expand All @@ -547,7 +547,7 @@ function RuleView({ rule, df, t }: { rule: CorrelationRule; df: ReturnType<typeo
<DefinitionView definition={rule.definition} t={t} />
</Section>
{steps.length > 0 && (
<Section title={t('alertingRules.view.afterEvents')}>
<Section title={t('alertingRules.view.correlationSteps')}>
<AfterEventsView steps={steps} t={t} />
</Section>
)}
Expand Down Expand Up @@ -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<typeof ruleToForm>['afterEvents']; t: TFunction }) {
function AfterEventsView({ steps, t }: { steps: ReturnType<typeof ruleToForm>['correlation']; t: TFunction }) {
return (
<div className="space-y-2">
{steps.map((s, i) => (
Expand Down
Loading
Loading