Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
8 changes: 7 additions & 1 deletion backend/modules/eventprocessing/domain/correlation_rule.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package domain

import "time"
import (
"time"
)

type UtmCorrelationRules struct {
ID int64 `gorm:"column:id;primaryKey"`
Expand Down Expand Up @@ -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"`

Expand All @@ -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" }
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 kept 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
4 changes: 3 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,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,
Expand All @@ -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)),
}
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