From 0289434535984970d283db908d463fc464aa0451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Thu, 23 Jul 2026 13:07:44 -0600 Subject: [PATCH] fix[backend](socai/mcp): fixed create/update rule schemas --- backend/modules/mcp/catalog.json | 4 +- backend/modules/mcp/tools_eventprocessing.go | 108 +++++++++++++++++-- 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/backend/modules/mcp/catalog.json b/backend/modules/mcp/catalog.json index 07c7d62b7..fb04401a0 100644 --- a/backend/modules/mcp/catalog.json +++ b/backend/modules/mcp/catalog.json @@ -406,8 +406,8 @@ {"name": "tenant_config.list", "status": "planned", "module": "eventprocessing", "title": "List tenant configs", "permission": "eventprocessing.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"asset_name": {"type": "string"}, "page": {"type": "integer"}, "size": {"type": "integer"}}}, "output": "{ items: TenantConfigResponse[], total: int64 }", "maps_to": "eventprocessing.TenantConfigUsecase.List", "rest_equivalent": "GET /api/v1/eventprocessing/tenant-config"}, {"name": "tenant_config.get", "status": "planned", "module": "eventprocessing", "title": "Get tenant config", "permission": "eventprocessing.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"asset_name": {"type": "string"}}, "required": ["asset_name"]}, "output": "TenantConfigResponse", "maps_to": "eventprocessing.TenantConfigUsecase.GetByID", "rest_equivalent": "GET /api/v1/eventprocessing/tenant-config/:id"}, {"name": "tenant_config.delete", "status": "planned", "module": "eventprocessing", "title": "Delete tenant config", "permission": "eventprocessing.write", "annotations": {"destructiveHint": true}, "input": {"type": "object", "properties": {"asset_name": {"type": "string"}}, "required": ["asset_name"]}, "output": "{ asset_name: string, deleted: true }", "maps_to": "eventprocessing.TenantConfigUsecase.Delete", "rest_equivalent": "DELETE /api/v1/eventprocessing/tenant-config/:id"}, - {"name": "correlation_rule.create", "status": "planned", "module": "eventprocessing", "title": "Create correlation rule", "permission": "eventprocessing.write", "input": {"type": "object", "properties": {"rule_name": {"type": "string"}, "adversary": {"type": "string"}, "category": {"type": "string"}, "technique": {"type": "string"}, "description": {"type": "string"}, "definition": {"type": "object"}, "confidentiality": {"type": "integer"}, "integrity": {"type": "integer"}, "availability": {"type": "integer"}}, "required": ["rule_name", "definition"]}, "output": "CorrelationRuleResponse", "maps_to": "eventprocessing.CorrelationRuleUsecase.Create", "rest_equivalent": "POST /api/v1/eventprocessing/correlation-rule"}, - {"name": "correlation_rule.update", "status": "planned", "module": "eventprocessing", "title": "Update correlation rule", "permission": "eventprocessing.write", "input": {"type": "object", "properties": {"rel_path": {"type": "string"}, "rule_name": {"type": "string"}, "definition": {"type": "object"}}, "required": ["rel_path"]}, "output": "CorrelationRuleResponse", "maps_to": "eventprocessing.CorrelationRuleUsecase.Update", "rest_equivalent": "PUT /api/v1/eventprocessing/correlation-rule"}, + {"name": "correlation_rule.create", "status": "planned", "module": "eventprocessing", "title": "Create correlation rule", "permission": "eventprocessing.write", "input": {"type": "object", "properties": {"name": {"type": "string"}, "adversary": {"type": "string"}, "confidentiality": {"type": "integer"}, "integrity": {"type": "integer"}, "availability": {"type": "integer"}, "category": {"type": "string"}, "technique": {"type": "string"}, "description": {"type": "string"}, "references": {"type": "array"}, "definition": {"type": "string", "description": "CEL expression"}, "groupBy": {"type": "array", "items": {"type": "string"}}, "deduplicateBy": {"type": "array", "items": {"type": "string"}}, "correlation": {"type": "object"}, "ruleActive": {"type": "boolean"}, "dataTypes": {"type": "array"}}, "required": ["name", "definition", "dataTypes"]}, "output": "CorrelationRuleResponse", "maps_to": "eventprocessing.CorrelationRuleUsecase.Create", "rest_equivalent": "POST /api/v1/eventprocessing/correlation-rule"}, + {"name": "correlation_rule.update", "status": "planned", "module": "eventprocessing", "title": "Update correlation rule", "permission": "eventprocessing.write", "input": {"type": "object", "properties": {"relPath": {"type": "string"}, "name": {"type": "string"}, "adversary": {"type": "string"}, "confidentiality": {"type": "integer"}, "integrity": {"type": "integer"}, "availability": {"type": "integer"}, "category": {"type": "string"}, "technique": {"type": "string"}, "description": {"type": "string"}, "references": {"type": "array"}, "definition": {"type": "string", "description": "CEL expression"}, "groupBy": {"type": "array", "items": {"type": "string"}}, "deduplicateBy": {"type": "array", "items": {"type": "string"}}, "correlation": {"type": "object"}, "ruleActive": {"type": "boolean"}, "dataTypes": {"type": "array"}}, "required": ["relPath"]}, "output": "CorrelationRuleResponse", "maps_to": "eventprocessing.CorrelationRuleUsecase.Update", "rest_equivalent": "PUT /api/v1/eventprocessing/correlation-rule"}, {"name": "correlation_rule.list", "status": "planned", "module": "eventprocessing", "title": "List correlation rules", "permission": "eventprocessing.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"rule_name": {"type": "string"}, "adversary": {"type": "string"}, "active": {"type": "boolean"}, "page": {"type": "integer"}, "size": {"type": "integer"}}}, "output": "{ items: CorrelationRuleResponse[], total: int64 }", "maps_to": "eventprocessing.CorrelationRuleUsecase.List", "rest_equivalent": "GET /api/v1/eventprocessing/correlation-rule/search-by-filters"}, {"name": "correlation_rule.get", "status": "planned", "module": "eventprocessing", "title": "Get correlation rule", "permission": "eventprocessing.read", "annotations": {"readOnlyHint": true}, "input": {"type": "object", "properties": {"rel_path": {"type": "string"}}, "required": ["rel_path"]}, "output": "CorrelationRuleResponse", "maps_to": "eventprocessing.CorrelationRuleUsecase.GetByRelPath", "rest_equivalent": "GET /api/v1/eventprocessing/correlation-rule/find"}, {"name": "correlation_rule.delete", "status": "planned", "module": "eventprocessing", "title": "Delete correlation rule", "permission": "eventprocessing.write", "annotations": {"destructiveHint": true}, "input": {"type": "object", "properties": {"rel_path": {"type": "string"}}, "required": ["rel_path"]}, "output": "{ rel_path: string, deleted: true }", "maps_to": "eventprocessing.CorrelationRuleUsecase.Delete", "rest_equivalent": "DELETE /api/v1/eventprocessing/correlation-rule"}, diff --git a/backend/modules/mcp/tools_eventprocessing.go b/backend/modules/mcp/tools_eventprocessing.go index 76a5cbdba..1ea2c9ece 100644 --- a/backend/modules/mcp/tools_eventprocessing.go +++ b/backend/modules/mcp/tools_eventprocessing.go @@ -2,6 +2,8 @@ package mcp import ( "context" + "encoding/json" + "reflect" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/utmstack/utmstack/backend/modules/eventprocessing/dto" @@ -188,27 +190,121 @@ type epRulePropertyValuesInput struct { Value string `json:"value,omitempty"` } +// MCP-only input shapes: the underlying DTO uses json.RawMessage for these +// fields, which the SDK reflects as []byte → array — wrong for the +// automation client. These wrappers carry the real per-field shape. +type epCorrelationRuleCreateInput struct { + RuleName string `json:"name" jsonschema:"Rule name"` + RuleAdversary string `json:"adversary" jsonschema:"origin | target"` + RuleConfidentiality int `json:"confidentiality" jsonschema:"0-3"` + RuleIntegrity int `json:"integrity" jsonschema:"0-3"` + RuleAvailability int `json:"availability" jsonschema:"0-3"` + RuleCategory string `json:"category"` + RuleTechnique string `json:"technique"` + RuleDescription string `json:"description"` + RuleReferences []any `json:"references,omitempty" jsonschema:"Reference URLs or objects"` + RuleDefinition string `json:"definition" jsonschema:"CEL expression, e.g. equals(\"log.channel\",\"Security\")"` + RuleGroupBy []string `json:"groupBy,omitempty"` + DeduplicateBy []string `json:"deduplicateBy,omitempty"` + Correlation any `json:"correlation,omitempty" jsonschema:"Optional multi-step correlation object"` + RuleActive bool `json:"ruleActive"` + DataTypes []dto.DataTypeRef `json:"dataTypes"` +} + +type epCorrelationRuleUpdateInput struct { + RelPath string `json:"relPath" jsonschema:"Rule identity (YAML relative path)"` + RuleName string `json:"name"` + RuleAdversary string `json:"adversary" jsonschema:"origin | target"` + RuleConfidentiality int `json:"confidentiality" jsonschema:"0-3"` + RuleIntegrity int `json:"integrity" jsonschema:"0-3"` + RuleAvailability int `json:"availability" jsonschema:"0-3"` + RuleCategory string `json:"category"` + RuleTechnique string `json:"technique"` + RuleDescription string `json:"description"` + RuleReferences []any `json:"references,omitempty"` + RuleDefinition string `json:"definition" jsonschema:"CEL expression"` + RuleGroupBy []string `json:"groupBy,omitempty"` + DeduplicateBy []string `json:"deduplicateBy,omitempty"` + Correlation any `json:"correlation,omitempty"` + RuleActive bool `json:"ruleActive"` + DataTypes []dto.DataTypeRef `json:"dataTypes"` +} + +func mustRaw(v any) json.RawMessage { + if v == nil || reflect.ValueOf(v).IsZero() { + return nil + } + b, err := json.Marshal(v) + if err != nil { + return nil + } + return b +} + +func (in epCorrelationRuleCreateInput) toDTO() dto.CreateCorrelationRuleRequest { + return dto.CreateCorrelationRuleRequest{ + RuleName: in.RuleName, + RuleAdversary: in.RuleAdversary, + RuleConfidentiality: in.RuleConfidentiality, + RuleIntegrity: in.RuleIntegrity, + RuleAvailability: in.RuleAvailability, + RuleCategory: in.RuleCategory, + RuleTechnique: in.RuleTechnique, + RuleDescription: in.RuleDescription, + RuleReferencesDef: mustRaw(in.RuleReferences), + RuleDefinitionDef: mustRaw(in.RuleDefinition), + RuleGroupByDef: mustRaw(in.RuleGroupBy), + DeduplicateByDef: mustRaw(in.DeduplicateBy), + CorrelationDef: mustRaw(in.Correlation), + RuleActive: in.RuleActive, + DataTypes: in.DataTypes, + } +} + +func (in epCorrelationRuleUpdateInput) toDTO() dto.UpdateCorrelationRuleRequest { + return dto.UpdateCorrelationRuleRequest{ + RelPath: in.RelPath, + RuleName: in.RuleName, + RuleAdversary: in.RuleAdversary, + RuleConfidentiality: in.RuleConfidentiality, + RuleIntegrity: in.RuleIntegrity, + RuleAvailability: in.RuleAvailability, + RuleCategory: in.RuleCategory, + RuleTechnique: in.RuleTechnique, + RuleDescription: in.RuleDescription, + RuleReferencesDef: mustRaw(in.RuleReferences), + RuleDefinitionDef: mustRaw(in.RuleDefinition), + RuleGroupByDef: mustRaw(in.RuleGroupBy), + DeduplicateByDef: mustRaw(in.DeduplicateBy), + CorrelationDef: mustRaw(in.Correlation), + RuleActive: in.RuleActive, + DataTypes: in.DataTypes, + } +} + func registerEPCorrelationRules(m *Module) { uc := m.deps.EventProcessing.GetCorrelationRuleUsecase() Add(m, &mcp.Tool{ Name: "correlation_rule.create", Title: "Create correlation rule", }, Gate{Permission: "eventprocessing.write"}, - func(ctx context.Context, _ *authz.Actor, in dto.CreateCorrelationRuleRequest) (any, error) { - if err := uc.Create(ctx, in); err != nil { + func(ctx context.Context, _ *authz.Actor, in epCorrelationRuleCreateInput) (any, error) { + req := in.toDTO() + if err := uc.Create(ctx, req); err != nil { return nil, err } - return map[string]any{"name": in.RuleName, "created": true}, nil + return map[string]any{"name": req.RuleName, "created": true}, nil }) Add(m, &mcp.Tool{ Name: "correlation_rule.update", Title: "Update correlation rule", }, Gate{Permission: "eventprocessing.write"}, - func(ctx context.Context, _ *authz.Actor, in dto.UpdateCorrelationRuleRequest) (any, error) { - if err := uc.Update(ctx, in); err != nil { + func(ctx context.Context, _ *authz.Actor, in epCorrelationRuleUpdateInput) (any, error) { + req := in.toDTO() + if err := uc.Update(ctx, req); err != nil { return nil, err } - return map[string]any{"rel_path": in.RelPath, "updated": true}, nil + return map[string]any{"rel_path": req.RelPath, "updated": true}, nil }) Add(m, &mcp.Tool{