Skip to content
Open
Show file tree
Hide file tree
Changes from all 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" }
10 changes: 7 additions & 3 deletions backend/modules/eventprocessing/dto/correlation_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand All @@ -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"`
Expand All @@ -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"`

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)
}
24 changes: 16 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,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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
}
Expand All @@ -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,
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -82,7 +85,7 @@ export interface SaveRuleInput {
description: string
references: unknown
definition: string
afterEvents: unknown
correlation: unknown
groupBy: unknown
deduplicateBy: unknown
ruleActive: boolean
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/shared/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -4213,7 +4213,7 @@
"updated": "Aktualisiert",
"relPath": "Pfad",
"definition": "Bedingung (CEL)",
"afterEvents": "Korrelationsschritte",
"correlationSteps": "Korrelationsschritte",
"correlation": "Korrelation",
"groupBy": "Gruppieren nach",
"deduplicateBy": "Deduplizieren nach",
Expand All @@ -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",
Expand Down
Loading
Loading