diff --git a/x-pack/platform/plugins/private/translations/translations/de-DE.json b/x-pack/platform/plugins/private/translations/translations/de-DE.json index c1c95eef837ca..5b7d88bde4736 100644 --- a/x-pack/platform/plugins/private/translations/translations/de-DE.json +++ b/x-pack/platform/plugins/private/translations/translations/de-DE.json @@ -34554,9 +34554,6 @@ "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.eventTypeColumn": "Typ", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.fetchErrorDescription": "Fehler beim Abrufen der Regelausführungsereignisse", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.logLevelColumn": "Kategorie", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.messageColumn": "Nachricht", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.jsonTitle": "Vollständiges JSON", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.messageTitle": "Nachricht", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableSubtitle": "Ein detailliertes Log der Regelausführungsereignisse", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableTitle": "Ausführungslog", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.timestampColumn": "Zeitstempel", diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index a8fd2685fe22f..8d8ea8d29896c 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -34910,9 +34910,6 @@ "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.eventTypeColumn": "Type", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.fetchErrorDescription": "Impossible de récupérer les événements d'exécution de règle", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.logLevelColumn": "Niveau", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.messageColumn": "Message", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.jsonTitle": "Full JSON", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.messageTitle": "Message", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableSubtitle": "Un log détaillé des événements d’exécution de la règle", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableTitle": "Log d'exécution", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.timestampColumn": "Horodatage", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index ad0c9f49daf96..c4aa274738584 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -34962,9 +34962,6 @@ "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.eventTypeColumn": "型", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.fetchErrorDescription": "ルール実行イベントを取得できませんでした", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.logLevelColumn": "レベル", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.messageColumn": "メッセージ", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.jsonTitle": "JSON全体", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.messageTitle": "メッセージ", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableSubtitle": "ルール実行イベントの詳細ログ", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableTitle": "実行ログ", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.timestampColumn": "タイムスタンプ", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index bb839608520ad..0d755b8da0185 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -34946,9 +34946,6 @@ "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.eventTypeColumn": "类型", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.fetchErrorDescription": "无法提取规则执行事件", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.logLevelColumn": "级别", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.messageColumn": "消息", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.jsonTitle": "完整 JSON", - "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.messageTitle": "消息", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableSubtitle": "规则执行事件的详细日志", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableTitle": "执行日志", "xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.timestampColumn": "时间戳", diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_event.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_event.gen.ts index eafbf651f9f9e..88cc3d9354a21 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_event.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_event.gen.ts @@ -45,4 +45,8 @@ export const RuleExecutionEvent = z.object({ type: RuleExecutionEventType, execution_id: z.string().min(1), message: z.string(), + /** + * Event details. The details vary per event type. + */ + details: z.object({}).catchall(z.unknown()).optional(), }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_event.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_event.schema.yaml index d49a49d222401..06a0865c2d7e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_event.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_event.schema.yaml @@ -36,6 +36,10 @@ components: minLength: 1 message: type: string + details: + type: object + additionalProperties: true + description: Event details. The details vary per event type. required: - timestamp - sequence diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/health_truncate_text/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/health_truncate_text/index.tsx index 1778951f6458c..0783ec13a8867 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/health_truncate_text/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/health_truncate_text/index.tsx @@ -32,14 +32,18 @@ export const HealthTruncateText: React.FC ( - +}) => { + const content = ( {children} - -); + ); + if (!tooltipContent) { + return content; + } + return {content}; +}; HealthTruncateText.displayName = 'HealthTruncateText'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/utils.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/utils.ts index 9e86215228078..a487598976315 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/utils.ts @@ -15,11 +15,11 @@ import * as i18n from './translations'; export const getBadgeIcon = (type: RuleExecutionEventType): IconType => { switch (type) { case RuleExecutionEventTypeEnum.message: - return 'console'; + return 'comment'; case RuleExecutionEventTypeEnum['status-change']: return 'dot'; case RuleExecutionEventTypeEnum['execution-metrics']: - return 'gear'; + return 'stats'; default: return assertUnreachable(type, 'Unknown rule execution event type'); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/event_message_filter/event_message_filter.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/event_message_filter/event_message_filter.tsx index b8d45ed5fb1a3..d49008be5a27c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/event_message_filter/event_message_filter.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/event_message_filter/event_message_filter.tsx @@ -5,30 +5,22 @@ * 2.0. */ -import type { ChangeEvent } from 'react'; -import React, { useCallback } from 'react'; +import React from 'react'; import { EuiFieldSearch } from '@elastic/eui'; import * as i18n from './translations'; interface EventMessageFilterProps { - value: string; - onChange: (value: string) => void; + onSearch: (value: string) => void; } -export function EventMessageFilter({ value, onChange }: EventMessageFilterProps): JSX.Element { - const handleChange = useCallback( - (e: ChangeEvent) => onChange(e.target.value), - [onChange] - ); - +export function EventMessageFilter({ onSearch }: EventMessageFilterProps): JSX.Element { return ( ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table.tsx index 85870f7cda4e3..94c983991bf12 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table.tsx @@ -20,7 +20,7 @@ import type { RuleExecutionEvent } from '../../../../../common/api/detection_eng import { HeaderSection } from '../../../../common/components/header_section'; import { EventTypeFilter } from '../basic/filters/event_type_filter'; import { LogLevelFilter } from '../basic/filters/log_level_filter'; -import { ExecutionEventsTableRowDetails } from './execution_events_table_row_details'; +import { ExecutionEventsTableDetailsCell } from './execution_events_table_row_details'; import { useFilters } from './use_filters'; import { useSorting } from '../basic/tables/use_sorting'; @@ -34,6 +34,10 @@ import * as i18n from './translations'; const PAGE_SIZE_OPTIONS = [10, 20, 50, 100, 200]; +const tableRowProps = { + 'data-test-subj': 'executionEventsTableRow', +}; + interface ExecutionEventsTableProps { ruleId: string; } @@ -44,7 +48,7 @@ const ExecutionEventsTableComponent: React.FC = ({ ru }, []); const renderExpandedItem = useCallback((item: RuleExecutionEvent) => { - return ; + return ; }, []); const rows = useExpandableRows({ @@ -96,7 +100,7 @@ const ExecutionEventsTableComponent: React.FC = ({ ru - + @@ -113,12 +117,14 @@ const ExecutionEventsTableComponent: React.FC = ({ ru end={filters.state.dateRange.end} onTimeChange={filters.setDateRange} showUpdateButton={false} + data-test-subj="executionEventsTable-datePicker" /> {/* Table with items */} = ({ ru sorting={sorting.state} pagination={pagination.state} onChange={handleTableChange} + tableCaption={i18n.TABLE_SUBTITLE} + rowProps={tableRowProps} /> ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_details.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_details.tsx index 9bedc720a4a1f..7006a3641fa41 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_details.tsx @@ -6,35 +6,64 @@ */ import React from 'react'; -import { EuiDescriptionList } from '@elastic/eui'; -import type { RuleExecutionEvent } from '../../../../../common/api/detection_engine/rule_monitoring'; -import { TextBlock } from '../basic/text/text_block'; +import { EuiCodeBlock, EuiDescriptionList } from '@elastic/eui'; +import { type RuleExecutionEvent } from '../../../../../common/api/detection_engine/rule_monitoring'; import * as i18n from './translations'; -interface ExecutionEventsTableRowDetailsProps { +interface ExecutionEventsTableDetailsCellProps { item: RuleExecutionEvent; } -const ExecutionEventsTableRowDetailsComponent: React.FC = ({ +const ExecutionEventsTableDetailsCellComponent: React.FC = ({ item, }) => { + const listItems = []; + + if (item.message) { + listItems.push({ + title: i18n.EVENT_MESSAGE, + description: ( + + {item.message} + + ), + }); + } + + if (item?.details?.metrics) { + listItems.push({ + title: i18n.METRICS, + description: ( + + {JSON.stringify(item.details.metrics, null, 2)} + + ), + }); + } + + listItems.push({ + title: i18n.RULE_EXECUTION_ID, + description: ( + + {item.execution_id} + + ), + }); return ( , - }, - { - title: i18n.ROW_DETAILS_JSON, - description: , - }, - ]} + listItems={listItems} /> ); }; -export const ExecutionEventsTableRowDetails = React.memo(ExecutionEventsTableRowDetailsComponent); -ExecutionEventsTableRowDetails.displayName = 'ExecutionEventsTableRowDetails'; +export const ExecutionEventsTableDetailsCell = React.memo(ExecutionEventsTableDetailsCellComponent); +ExecutionEventsTableDetailsCell.displayName = 'ExecutionEventsTableDetailsCell'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_summary.tsx new file mode 100644 index 0000000000000..7a47154a8e834 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_summary.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import type { + RuleExecutionEvent, + RuleExecutionMetrics, + RuleExecutionStatus, +} from '../../../../../common/api/detection_engine/rule_monitoring'; +import { RuleExecutionEventTypeEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; + +import { assertUnreachable } from '../../../../../common/utility_types'; +import { TruncatedText } from '../basic/text/truncated_text'; +import { RuleStatusBadge } from '../../../common/components/rule_execution_status/rule_status_badge'; + +import * as i18n from './translations'; + +interface ExecutionEventsTableSummaryCellProps { + event: RuleExecutionEvent; +} + +export function ExecutionEventsTableSummaryCell({ event }: ExecutionEventsTableSummaryCellProps) { + switch (event.type) { + case RuleExecutionEventTypeEnum.message: + return ; + case RuleExecutionEventTypeEnum['status-change']: + return ; + case RuleExecutionEventTypeEnum['execution-metrics']: + return ; + default: + assertUnreachable(event.type, 'Unknown rule execution event type'); + } +} + +function MessageSummary({ message }: { message: string }) { + const firstLineOnly = message.split('\n')[0]; + return ; +} + +interface StatusChangeSummaryProps { + details?: { status?: RuleExecutionStatus }; +} + +function StatusChangeSummary({ details }: StatusChangeSummaryProps) { + const status = details?.status; + + if (!status) { + return null; + } + + return ( + + {i18n.STATUS_CHANGED_TO} + + + + + ); +} + +interface ExecutionMetricsSummaryProps { + details?: { metrics?: RuleExecutionMetrics }; +} + +function ExecutionMetricsSummary({ details }: ExecutionMetricsSummaryProps) { + const metrics = details?.metrics; + + const summaryMetrics: string[] = []; + + if (metrics?.execution_gap_duration_s) { + summaryMetrics.push(i18n.GAP_DURATION(metrics.execution_gap_duration_s)); + } + + summaryMetrics.push(i18n.SEARCH_DURATION(metrics?.total_search_duration_ms ?? 0)); + + if (metrics?.total_indexing_duration_ms) { + summaryMetrics.push(i18n.INDEXING_DURATION(metrics.total_indexing_duration_ms)); + } + + return ; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/translations.ts index 0cc671d7b732c..c4896a01fb9e7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const TABLE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableTitle', { - defaultMessage: 'Execution log', + defaultMessage: 'Execution events', } ); @@ -42,10 +42,10 @@ export const COLUMN_EVENT_TYPE = i18n.translate( } ); -export const COLUMN_MESSAGE = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.messageColumn', +export const COLUMN_SUMMARY = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.summaryColumn', { - defaultMessage: 'Message', + defaultMessage: 'Summary', } ); @@ -56,16 +56,57 @@ export const FETCH_ERROR = i18n.translate( } ); -export const ROW_DETAILS_MESSAGE = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.messageTitle', +export const EVENT_MESSAGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.eventMessageTitle', { - defaultMessage: 'Message', + defaultMessage: 'Event message', } ); -export const ROW_DETAILS_JSON = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.jsonTitle', +export const STATUS_CHANGED_TO = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.statusChangedTo', { - defaultMessage: 'Full JSON', + defaultMessage: 'Status changed to', + } +); + +export const GAP_DURATION = (durationSeconds: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.gapDuration', + { + defaultMessage: 'Gap duration: {durationSeconds}s', + values: { durationSeconds }, + } + ); + +export const SEARCH_DURATION = (durationMs: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.searchDuration', + { + defaultMessage: 'Search duration: {durationMs}ms', + values: { durationMs }, + } + ); + +export const INDEXING_DURATION = (durationMs: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.indexingDuration', + { + defaultMessage: 'Indexing duration: {durationMs}ms', + values: { durationMs }, + } + ); + +export const METRICS = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.metricsTitle', + { + defaultMessage: 'Metrics', + } +); + +export const RULE_EXECUTION_ID = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.ruleExecutionIdTitle', + { + defaultMessage: 'Rule execution ID', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_columns.tsx index 5ce74f014f809..c9c87754d1a7c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_columns.tsx @@ -18,9 +18,9 @@ import type { import { FormattedDate } from '../../../../common/components/formatted_date'; import { EventTypeIndicator } from '../basic/indicators/event_type_indicator'; import { LogLevelIndicator } from '../basic/indicators/log_level_indicator'; -import { TruncatedText } from '../basic/text/truncated_text'; import * as i18n from './translations'; +import { ExecutionEventsTableSummaryCell } from './execution_events_table_row_summary'; type TableColumn = EuiBasicTableColumn; @@ -37,7 +37,7 @@ export const useColumns = (args: UseColumnsArgs): TableColumn[] => { timestampColumn, logLevelColumn, eventTypeColumn, - messageColumn, + summaryColumn, expanderColumn({ toggleRowExpanded, isRowExpanded }), ]; }, [toggleRowExpanded, isRowExpanded]); @@ -50,6 +50,7 @@ const timestampColumn: TableColumn = { sortable: true, truncateText: false, width: '20%', + 'data-test-subj': 'executionEventsTable-timestampColumn', }; const logLevelColumn: TableColumn = { @@ -59,6 +60,7 @@ const logLevelColumn: TableColumn = { sortable: false, truncateText: false, width: '8%', + 'data-test-subj': 'executionEventsTable-logLevelColumn', }; const eventTypeColumn: TableColumn = { @@ -68,13 +70,12 @@ const eventTypeColumn: TableColumn = { sortable: false, truncateText: false, width: '8%', + 'data-test-subj': 'executionEventsTable-eventTypeColumn', }; -const messageColumn: TableColumn = { - field: 'message', - name: i18n.COLUMN_MESSAGE, - render: (value: string) => , - sortable: false, +const summaryColumn: TableColumn = { + name: i18n.COLUMN_SUMMARY, + render: (item: RuleExecutionEvent) => , truncateText: true, width: '64%', }; @@ -94,6 +95,11 @@ const expanderColumn = ({ toggleRowExpanded, isRowExpanded }: UseColumnsArgs): T onClick={() => toggleRowExpanded(item)} aria-label={isRowExpanded(item) ? 'Collapse' : 'Expand'} iconType={isRowExpanded(item) ? 'arrowUp' : 'arrowDown'} + data-test-subj={ + isRowExpanded(item) + ? 'executionEventsTable-collapseButton' + : 'executionEventsTable-expandButton' + } /> ), }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_reader.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_reader.ts index ba877fa9ec091..f0341931d9193 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_reader.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_reader.ts @@ -27,7 +27,6 @@ import { import { prepareKQLStringParam } from '../../../../../../../common/utils/kql'; -import { assertUnreachable } from '../../../../../../../common/utility_types'; import { invariant } from '../../../../../../../common/utils/invariant'; import { withSecuritySpan } from '../../../../../../utils/with_security_span'; import { kqlAnd, kqlOr } from '../../utils/kql'; @@ -219,9 +218,10 @@ const normalizeEvent = (rawEvent: IValidatedEvent): RuleExecutionEvent => { const level = normalizeLogLevel(rawEvent); const type = normalizeEventType(rawEvent); const executionId = normalizeExecutionId(rawEvent); - const message = normalizeEventMessage(rawEvent, type); + const message = rawEvent.message || ''; + const details = normalizeEventDetails(rawEvent, type); - return { timestamp, sequence, level, type, message, execution_id: executionId }; + return { timestamp, sequence, level, type, message, details, execution_id: executionId }; }; type RawEvent = NonNullable; @@ -260,21 +260,16 @@ const normalizeEventType = (event: RawEvent): RuleExecutionEventType => { return result.success ? result.data : RuleExecutionEventTypeEnum.message; }; -const normalizeEventMessage = (event: RawEvent, type: RuleExecutionEventType): string => { - if (type === RuleExecutionEventTypeEnum.message) { - return event.message || ''; - } - +const normalizeEventDetails = ( + event: RawEvent, + type: RuleExecutionEventType +): Record | undefined => { if (type === RuleExecutionEventTypeEnum['status-change']) { invariant( event.kibana?.alert?.rule?.execution?.status, 'Required "kibana.alert.rule.execution.status" field is not found' ); - - const status = event.kibana?.alert?.rule?.execution?.status; - const message = event.message || ''; - - return `Rule changed status to "${status}". ${message}`; + return { status: event.kibana.alert.rule.execution.status }; } if (type === RuleExecutionEventTypeEnum['execution-metrics']) { @@ -282,12 +277,8 @@ const normalizeEventMessage = (event: RawEvent, type: RuleExecutionEventType): s event.kibana?.alert?.rule?.execution?.metrics, 'Required "kibana.alert.rule.execution.metrics" field is not found' ); - - return JSON.stringify(event.kibana.alert.rule.execution.metrics); + return { metrics: event.kibana.alert.rule.execution.metrics }; } - - assertUnreachable(type); - return ''; }; const normalizeExecutionId = (event: RawEvent): string => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 5a50ed815a842..b45c47a729df3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -217,7 +217,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const refresh = isPreview ? false : true; - ruleExecutionLogger.debug(`Starting Security Rule execution (interval: ${interval})`); + ruleExecutionLogger.debug(`Starting execution with interval: ${interval}`); await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatusEnum.running, @@ -279,13 +279,13 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = if (SavedObjectsErrorHelpers.isNotFoundError(exc)) { await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatusEnum.failed, - message: `Data View not found ${exc}`, + message: `Data view is not found.\nError: ${exc}`, userError: true, }); } else { await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatusEnum.failed, - message: `Check for indices to search failed ${exc}`, + message: `Check for indices to search failed.\nError: ${exc}`, }); } @@ -589,12 +589,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }); } else if (!(result.warningMessages.length > 0) && !(wrapperWarnings.length > 0)) { ruleExecutionLogger.debug('Security Rule execution completed'); - ruleExecutionLogger.debug( - `Finished indexing ${createdSignalsCount} alerts into ${ruleDataClient.indexNameWithNamespace( + ruleExecutionLogger.info( + `Alerts created: ${createdSignalsCount}\nFinished indexing ${createdSignalsCount} alerts into "${ruleDataClient.indexNameWithNamespace( spaceId - )} ${ + )}".${ !isEmpty(tuples) - ? `searched between date ranges ${JSON.stringify(tuples, null, 2)}` + ? ` Searched between date ranges: ${JSON.stringify(tuples, null, 2)}.` : '' }` ); @@ -614,7 +614,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatusEnum.failed, - message: `An error occurred during rule execution: message: "${errorMessage}"`, + message: `An error occurred during rule execution. ${errorMessage}`, userError: checkErrorDetails(errorMessage).isUserError, metrics: { searchDurations: result.searchAfterTimes, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/eql/build_alert_group_from_sequence.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/eql/build_alert_group_from_sequence.ts index 851635bf86bed..61c91560426e7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/eql/build_alert_group_from_sequence.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/eql/build_alert_group_from_sequence.ts @@ -106,7 +106,7 @@ export const buildAlertGroupFromSequence = ({ }) ); } catch (error) { - ruleExecutionLogger.error(error); + ruleExecutionLogger.debug(`Error building alert group from sequence\nError: ${error}`); return { shellAlert: undefined, buildingBlocks: [] }; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts index 87496501ce356..d4543d443ef76 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts @@ -98,7 +98,7 @@ export const eqlExecutor = async ({ tiebreakerField: ruleParams.tiebreakerField, }); - ruleExecutionLogger.debug(`EQL query request: ${JSON.stringify(request)}`); + ruleExecutionLogger.trace(`EQL query to execute\n${JSON.stringify(request)}`); const exceptionsWarning = getUnprocessedExceptionsWarnings(sharedParams.unprocessedExceptions); if (exceptionsWarning) { result.warningMessages.push(exceptionsWarning); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts index cd72345409595..64c2fff7939dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts @@ -145,7 +145,7 @@ export const esqlExecutor = async ({ }; const hasLoggedRequestsReachedLimit = iteration >= 2; - ruleExecutionLogger.debug(`ES|QL query request: ${JSON.stringify(esqlRequest)}`); + ruleExecutionLogger.trace(`ES|QL query to execute\n${JSON.stringify(esqlRequest)}`); const exceptionsWarning = getUnprocessedExceptionsWarnings(unprocessedExceptions); if (exceptionsWarning) { result.warningMessages.push(exceptionsWarning); @@ -166,8 +166,8 @@ export const esqlExecutor = async ({ const esqlSearchDuration = performance.now() - esqlSignalSearchStart; result.searchAfterTimes.push(makeFloatString(esqlSearchDuration)); - ruleExecutionLogger.debug( - `ES|QL query request for ${iteration} iteration took: ${esqlSearchDuration}ms` + ruleExecutionLogger.trace( + `ES|QL query iteration\nIteration: ${iteration}. Search took: ${esqlSearchDuration}ms.` ); const results = response.values.map((row) => rowToDocument(response.columns, row)); @@ -235,8 +235,8 @@ export const esqlExecutor = async ({ maxNumberOfAlertsMultiplier: 1, }); - ruleExecutionLogger.debug( - `Created ${bulkCreateResult.createdItemsCount} alerts. Suppressed ${bulkCreateResult.suppressedItemsCount} alerts` + ruleExecutionLogger.info( + `Alerts created: ${bulkCreateResult.createdItemsCount}. Alerts suppressed: ${bulkCreateResult.suppressedItemsCount}.` ); updateExcludedDocuments({ @@ -268,7 +268,7 @@ export const esqlExecutor = async ({ }); addToSearchAfterReturn({ current: result, next: bulkCreateResult }); - ruleExecutionLogger.debug(`Created ${bulkCreateResult.createdItemsCount} alerts`); + ruleExecutionLogger.info(`Alerts created: ${bulkCreateResult.createdItemsCount}.`); updateExcludedDocuments({ excludedDocuments, @@ -293,8 +293,8 @@ export const esqlExecutor = async ({ // no more results will be found if (response.values.length < size) { - ruleExecutionLogger.debug( - `End of search: Found ${response.values.length} results with page size ${size}` + ruleExecutionLogger.trace( + `End of search. Found ${response.values.length} results\nPage size ${size}.` ); break; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index 524671096230f..6604973aa08e1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -71,7 +71,7 @@ export const bulkCreate = async ({ }); return enrichedAlerts; } catch (error) { - ruleExecutionLogger.error(`Alerts enrichment failed: ${error}`); + ruleExecutionLogger.error(`Error enriching alerts\nError: ${error}`); throw error; } finally { enrichmentsTimeFinish = performance.now(); @@ -91,10 +91,10 @@ export const bulkCreate = async ({ const end = performance.now(); - ruleExecutionLogger.debug(`Alerts bulk process took ${makeFloatString(end - start)} ms`); + ruleExecutionLogger.debug(`Bulk processing alerts took ${makeFloatString(end - start)}ms.`); if (!isEmpty(errors)) { - ruleExecutionLogger.warn(`Alerts bulk process finished with errors: ${JSON.stringify(errors)}`); + ruleExecutionLogger.warn(`Error bulk processing alerts\nError: ${JSON.stringify(errors)}`); return { errors: Object.keys(errors), success: false, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts index eeb79fdd24ee5..0343aaedc688d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts @@ -106,7 +106,7 @@ export const createEventSignal = async ({ loadFields: true, }); - ruleExecutionLogger.debug(`${ids?.length} matched signals found`); + ruleExecutionLogger.debug(`Matched signals found: ${ids?.length}`); const enrichment = threatEnrichmentFactory({ signalIdToMatchedQueriesMap, @@ -138,12 +138,12 @@ export const createEventSignal = async ({ } else { createResult = await searchAfterAndBulkCreate(searchAfterBulkCreateParams); } - ruleExecutionLogger.debug( - `${ + ruleExecutionLogger.trace( + `Match checks completed\n${ currentEventList.length - } items have completed match checks and the total times to search were ${ + } items have completed match checks. Search times (ms): ${ createResult.searchAfterTimes.length !== 0 ? createResult.searchAfterTimes : '(unknown) ' - }ms` + }.` ); return createResult; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts index e5b20455e488f..d510e616c4e92 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts @@ -56,7 +56,7 @@ export const createThreatSignal = async ({ if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) { // empty threat list and we do not want to return everything as being // a hit so opt to return the existing result. - ruleExecutionLogger.debug( + ruleExecutionLogger.trace( 'Indicator items are empty after filtering for missing data, returning without attempting a match' ); return currentResult; @@ -74,7 +74,7 @@ export const createThreatSignal = async ({ loadFields: true, }); - ruleExecutionLogger.debug( + ruleExecutionLogger.trace( `${threatFilter.query?.bool.should.length} indicator items are being checked for existence of matches` ); @@ -115,12 +115,12 @@ export const createThreatSignal = async ({ result = await searchAfterAndBulkCreate(searchAfterBulkCreateParams); } - ruleExecutionLogger.debug( - `${ + ruleExecutionLogger.trace( + `Match checks completed\n${ threatFilter.query?.bool.should.length - } items have completed match checks and the total times to search were ${ + } items have completed match checks. Search times (ms): ${ result.searchAfterTimes.length !== 0 ? result.searchAfterTimes : '(unknown) ' - }ms` + }.` ); return result; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts index f04996132e0dc..7b60279d0ad2e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts @@ -77,7 +77,7 @@ export const createThreatSignals = async ({ }); const params = completeRule.ruleParams; - ruleExecutionLogger.debug('Indicator matching rule starting'); + ruleExecutionLogger.trace('Indicator matching rule starting'); const perPage = concurrentSearches * itemsPerSearch; const verifyExecutionCanProceed = buildExecutionIntervalValidator( completeRule.ruleConfig.schedule.interval @@ -185,7 +185,7 @@ export const createThreatSignals = async ({ while (list.hits.hits.length !== 0) { verifyExecutionCanProceed(); const chunks = chunk(chunkPage, list.hits.hits); - ruleExecutionLogger.debug(`${chunks.length} concurrent indicator searches are starting.`); + ruleExecutionLogger.trace(`${chunks.length} concurrent indicator searches are starting.`); const concurrentSearchesPerformed = chunks.map>(createSignal); const searchesPerformed = await Promise.all(concurrentSearchesPerformed); @@ -205,8 +205,8 @@ export const createThreatSignals = async ({ // allowed by elasticsearch. The sliced chunk is used in createSignal to generate // threat filters. chunkPage = maxClauseCountValue; - ruleExecutionLogger.warn( - `maxClauseCount error received from elasticsearch, setting IM rule page size to ${maxClauseCountValue}` + ruleExecutionLogger.debug( + `Max clause count error received from Elasticsearch. Setting rule page size to ${maxClauseCountValue}.` ); // only store results + errors that are not related to maxClauseCount @@ -232,10 +232,7 @@ export const createThreatSignals = async ({ } documentCount -= list.hits.hits.length; ruleExecutionLogger.debug( - `Concurrent indicator match searches completed with ${results.createdSignalsCount} signals found`, - `search times of ${results.searchAfterTimes}ms,`, - `bulk create times ${results.bulkCreateTimes}ms,`, - `all successes are ${results.success}` + `Alert candidates found: ${results.createdSignalsCount}.\nConcurrent indicator match searches completed. Search took: ${results.searchAfterTimes}ms. Bulk create times (ms): ${results.bulkCreateTimes}. Are all operations successful: ${results.success}.` ); // if alerts suppressed it means suppression enabled, so suppression alert limit should be applied (5 * max_signals) @@ -246,7 +243,7 @@ export const createThreatSignals = async ({ results.warningMessages.push(getMaxSignalsWarning()); } ruleExecutionLogger.debug( - `Indicator match has reached its max signals count ${params.maxSignals}. Additional documents not checked are ${documentCount}` + `Max alerts per run reached\n${params.maxSignals}. Additional ${documentCount} documents are not checked.` ); break; } else if ( @@ -257,15 +254,15 @@ export const createThreatSignals = async ({ ) { // warning should be already set ruleExecutionLogger.debug( - `Indicator match has reached its max signals count ${ + `Max alerts per run reached\nIndicator match has reached its max signals count ${ MAX_SIGNALS_SUPPRESSION_MULTIPLIER * params.maxSignals - }. Additional documents not checked are ${documentCount}` + }. Additional ${documentCount} documents are not checked.` ); break; } - ruleExecutionLogger.debug(`Documents items left to check are ${documentCount}`); + ruleExecutionLogger.trace(`Documents items left to check: ${documentCount}`); if (maxClauseCountValue > Number.NEGATIVE_INFINITY) { - ruleExecutionLogger.debug(`Re-running search since we hit max clause count error`); + ruleExecutionLogger.trace(`Re-running search due to max clause count error`); // re-run search with smaller max clause count; list = await getDocumentList({ searchAfter: undefined }); @@ -280,8 +277,8 @@ export const createThreatSignals = async ({ const hasNegativeDateSort = sortIds?.some((val) => Number(val) < 0); if (hasNegativeDateSort) { - ruleExecutionLogger.debug( - `Negative date sort id value encountered: ${sortIds}. Threat search stopped.` + ruleExecutionLogger.trace( + `Negative date sort ID encountered\nValue: ${sortIds}. Threat search stopped.` ); break; @@ -382,8 +379,8 @@ export const createThreatSignals = async ({ await services.scopedClusterClient.asCurrentUser.closePointInTime({ id: threatPitId }); } catch (error) { // Don't fail due to a bad point in time closure. We have seen failures in e2e tests during nominal operations. - ruleExecutionLogger.warn( - `Error trying to close point in time: "${threatPitId}", it will expire within "${THREAT_PIT_KEEP_ALIVE}". Error is: "${error}"` + ruleExecutionLogger.debug( + `Error trying to close point in time\nPIT ID: "${threatPitId}". It will expire within "${THREAT_PIT_KEEP_ALIVE}". Error: "${error}".` ); } scheduleNotificationResponseActionsService({ @@ -391,6 +388,6 @@ export const createThreatSignals = async ({ signalsCount: results.createdSignalsCount, responseActions: completeRule.ruleParams.responseActions, }); - ruleExecutionLogger.debug('Indicator matching rule has completed'); + ruleExecutionLogger.trace('Indicator matching rule has completed'); return results; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_allowed_fields_for_terms_query.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_allowed_fields_for_terms_query.ts index 4e1c50c72745c..c7eef7e36eb79 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_allowed_fields_for_terms_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_allowed_fields_for_terms_query.ts @@ -69,7 +69,7 @@ export const getAllowedFieldsForTermQuery = async ({ ), }; } catch (e) { - ruleExecutionLogger.debug(`Can't get allowed fields for terms query: ${e}`); + ruleExecutionLogger.debug(`Error getting allowed fields for the terms query\nError: ${e}`); return allowedFieldsForTermsQuery; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts index 990620e47d841..28c080feb944c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts @@ -40,8 +40,8 @@ export const getEventList = async ({ throw new TypeError('perPage cannot exceed the size of 10000'); } - ruleExecutionLogger.debug( - `Querying the events items from the index: "${sharedParams.inputIndex}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items` + ruleExecutionLogger.trace( + `Querying events\nIndex: "${sharedParams.inputIndex}", searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items.` ); const queryFilter = getQueryFilter({ @@ -75,7 +75,7 @@ export const getEventList = async ({ ruleExecutionLogger, }); - ruleExecutionLogger.debug(`Retrieved events items of size: ${searchResult.hits.hits.length}`); + ruleExecutionLogger.debug(`Events retrieved: ${searchResult.hits.hits.length}`); return searchResult; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_threat_list.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_threat_list.ts index ac27cd185dd5a..d2c4b21a05bc5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_threat_list.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_threat_list.ts @@ -54,7 +54,7 @@ export const getThreatList = async ({ }); ruleExecutionLogger.debug( - `Querying the indicator items from the index: "${threatIndex}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items` + `Querying indicator items\nIndex: "${threatIndex}", searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items.` ); const response = await esClient.search< @@ -74,7 +74,7 @@ export const getThreatList = async ({ pit: { id: pitId }, }); - ruleExecutionLogger.debug(`Retrieved indicator items of size: ${response.hits.hits.length}`); + ruleExecutionLogger.debug(`Indicator items retrieved: ${response.hits.hits.length}`); reassignPitId(response.pit_id); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts index fedfec6707fdf..69e2a89a92184 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts @@ -98,10 +98,8 @@ describe('ml_executor', () => { isAlertSuppressionActive: true, scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }); - expect(ruleExecutionLogger.warn).toHaveBeenCalled(); - expect(ruleExecutionLogger.warn.mock.calls[0][0]).toContain( - 'Machine learning job(s) are not started' - ); + expect(ruleExecutionLogger.debug).toHaveBeenCalled(); + expect(ruleExecutionLogger.debug.mock.calls[0][0]).toContain('ML jobs are not started'); expect(result.warningMessages.length).toEqual(1); }); @@ -122,10 +120,8 @@ describe('ml_executor', () => { isAlertSuppressionActive: true, scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }); - expect(ruleExecutionLogger.warn).toHaveBeenCalled(); - expect(ruleExecutionLogger.warn.mock.calls[0][0]).toContain( - 'Machine learning job(s) are not started' - ); + expect(ruleExecutionLogger.debug).toHaveBeenCalled(); + expect(ruleExecutionLogger.debug.mock.calls[0][0]).toContain('ML jobs are not started'); expect(result.warningMessages.length).toEqual(1); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts index a38ea441c99f8..00f6770d26b73 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts @@ -81,19 +81,19 @@ export const mlExecutor = async ({ jobSummaries.some((job) => !isJobStarted(job.jobState, job.datafeedState)) ) { const warningMessage = [ - 'Machine learning job(s) are not started:', + 'ML jobs are not started', ...jobSummaries.map((job) => [ - `job id: "${job.id}"`, - `job name: "${job?.customSettings?.security_app_display_name ?? job.id}"`, - `job status: "${job.jobState}"`, - `datafeed status: "${job.datafeedState}"`, - ].join(', ') + `Job ID: "${job.id}"`, + `Job name: "${job?.customSettings?.security_app_display_name ?? job.id}"`, + `Job status: "${job.jobState}"`, + `Datafeed status: "${job.datafeedState}"`, + ].join('. ') ), - ].join(' '); + ].join('\n'); result.warningMessages.push(warningMessage); - ruleExecutionLogger.warn(warningMessage); + ruleExecutionLogger.debug(warningMessage); result.warning = true; } @@ -139,7 +139,7 @@ export const mlExecutor = async ({ const anomalyCount = filteredAnomalyHits.length; if (anomalyCount) { - ruleExecutionLogger.debug(`Found ${anomalyCount} signals from ML anomalies`); + ruleExecutionLogger.info(`Alerts from ML anomalies: ${anomalyCount}`); } if ( diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/multi_terms_composite.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/multi_terms_composite.ts index 7750daed86073..7b5e2fae86b44 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/multi_terms_composite.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/multi_terms_composite.ts @@ -284,8 +284,8 @@ export const multiTermsComposite = async ( } retryBatchSize = retryBatchSize / 2; - ruleExecutionLogger.warn( - `New terms query for multiple fields failed due to too many clauses in query: ${e.message}. Retrying #${retryCount} with ${retryBatchSize} for composite aggregation` + ruleExecutionLogger.debug( + `New terms query failed due to too many clauses\nError: ${e.message}. Retrying #${retryCount} with ${retryBatchSize} for composite aggregation.` ); throw e; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/query/alert_suppression/group_and_bulk_create.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/query/alert_suppression/group_and_bulk_create.ts index 4ce9fff66356a..500795e90d442 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/query/alert_suppression/group_and_bulk_create.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/query/alert_suppression/group_and_bulk_create.ts @@ -282,8 +282,8 @@ export const groupAndBulkCreate = async ({ ruleType: 'query', }); addToSearchAfterReturn({ current: toReturn, next: bulkCreateResult }); - sharedParams.ruleExecutionLogger.debug( - `created ${bulkCreateResult.createdItemsCount} signals` + sharedParams.ruleExecutionLogger.info( + `Alerts created: ${bulkCreateResult.createdItemsCount}` ); } else { const bulkCreateResult = await bulkCreate({ @@ -298,8 +298,8 @@ export const groupAndBulkCreate = async ({ suppressedItemsCount: getNumberOfSuppressedAlerts(bulkCreateResult.createdItems, []), }, }); - sharedParams.ruleExecutionLogger.debug( - `created ${bulkCreateResult.createdItemsCount} signals` + sharedParams.ruleExecutionLogger.info( + `Alerts created: ${bulkCreateResult.createdItemsCount}` ); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_with_suppression.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_with_suppression.ts index 2679f7bcf6d26..d8ef3eb59e925 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_with_suppression.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_with_suppression.ts @@ -83,7 +83,7 @@ export const bulkCreateWithSuppression = async < }); return enrichedAlerts; } catch (error) { - ruleExecutionLogger.error(`Alerts enrichment failed: ${error}`); + ruleExecutionLogger.error(`Error enriching alerts\nError: ${error}.`); throw error; } finally { enrichmentsTimeFinish = performance.now(); @@ -112,7 +112,7 @@ export const bulkCreateWithSuppression = async < const end = performance.now(); - ruleExecutionLogger.debug(`Alerts bulk process took ${makeFloatString(end - start)} ms`); + ruleExecutionLogger.debug(`Bulk processing alerts took ${makeFloatString(end - start)}ms.`); // query rule type suppression does not happen in memory, so we can't just count createdAlerts and suppressedAlerts // for this rule type we need to look into alerts suppression properties, extract those values and sum up @@ -124,7 +124,7 @@ export const bulkCreateWithSuppression = async < : suppressedAlerts.length; if (!isEmpty(errors)) { - ruleExecutionLogger.warn(`Alerts bulk process finished with errors: ${JSON.stringify(errors)}`); + ruleExecutionLogger.warn(`Error bulk processing alerts\nError: ${JSON.stringify(errors)}.`); return { errors: Object.keys(errors), success: false, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/create_set_to_filter_against.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/create_set_to_filter_against.ts index e419d13589a57..e5b518a57d8e8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/create_set_to_filter_against.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/create_set_to_filter_against.ts @@ -36,8 +36,8 @@ export const createSetToFilterAgainst = async ({ return acc; }, new Set()); - ruleExecutionLogger.debug( - `number of distinct values from ${field}: ${[...valuesFromSearchResultField].length}` + ruleExecutionLogger.trace( + `Distinct values from field: ${[...valuesFromSearchResultField].length}` ); const matchedListItems = await listClient.searchListItemByValues({ @@ -47,7 +47,7 @@ export const createSetToFilterAgainst = async ({ }); ruleExecutionLogger.debug( - `number of matched items from list with id ${listId}: ${matchedListItems.length}` + `Matched items from list: ${matchedListItems.length}\nList ID: "${listId}".` ); return new Set( diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/filter_events_against_list.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/filter_events_against_list.test.ts index d5c566383ebba..ea79ac5b00a42 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/filter_events_against_list.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/filter_events_against_list.test.ts @@ -62,7 +62,7 @@ describe('filterEventsAgainstList', () => { expect(included.length).toEqual(4); expect(excluded.length).toEqual(0); expect(ruleExecutionLogger.debug.mock.calls[0][0]).toContain( - 'No exception items of type list found - return unfiltered events' + 'No exception items of type list found' ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/filter_events_against_list.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/filter_events_against_list.ts index 8a8f7d34aa3a8..f8629d8ba000b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/filter_events_against_list.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/large_list_filters/filter_events_against_list.ts @@ -47,7 +47,9 @@ export const filterEventsAgainstList = async ({ ); if (!atLeastOneLargeValueList) { - ruleExecutionLogger.debug('No exception items of type list found - return unfiltered events'); + ruleExecutionLogger.debug( + 'No exception items of type list found\nReturning unfiltered events.' + ); return [events, []]; } @@ -74,7 +76,7 @@ export const filterEventsAgainstList = async ({ fieldAndSetTuples, }); ruleExecutionLogger.debug( - `Exception with id ${exceptionItem.id} filtered out ${nextExcludedEvents.length} events` + `Events filtered by exception: ${nextExcludedEvents.length}\nException ID: "${exceptionItem.id}".` ); return [nextIncludedEvents, [...excludedEvents, ...nextExcludedEvents]]; }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/log_shard_failure.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/log_shard_failure.ts index a5110f7ea11bb..208883817813a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/log_shard_failure.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/log_shard_failure.ts @@ -20,7 +20,7 @@ export const logShardFailures = ( isSequenceQuery, JSON.stringify(shardFailures) ); - ruleExecutionLogger.error(shardFailureMessage); + ruleExecutionLogger.error(`Shard failure\nError: ${shardFailureMessage}.`); if (isSequenceQuery) { result.errors.push(shardFailureMessage); } else { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create_factory.ts index 14d6ec2931669..219197224351b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create_factory.ts @@ -97,10 +97,10 @@ export const searchAfterAndBulkCreateFactory = async ({ while (toReturn.createdSignalsCount <= maxSignals) { const cycleNum = `cycle ${searchingIteration++}`; try { - ruleExecutionLogger.debug( - `[${cycleNum}] Searching events${ - sortIds ? ` after cursor ${JSON.stringify(sortIds)}` : '' - } in index pattern "${inputIndexPattern}"` + ruleExecutionLogger.trace( + `Searching events\n${cycleNum}. Searching events after cursor ${JSON.stringify( + sortIds + )} in index pattern "${inputIndexPattern}".` ); const searchAfterQuery = buildEventsSearchQuery({ @@ -152,17 +152,19 @@ export const searchAfterAndBulkCreateFactory = async ({ ); if (totalHits === 0 || searchResult.hits.hits.length === 0) { - ruleExecutionLogger.debug( - `[${cycleNum}] Found 0 events ${ - sortIds ? ` after cursor ${JSON.stringify(sortIds)}` : '' - }` + ruleExecutionLogger.trace( + `No results found in cycle\n${cycleNum}. Found 0 events after cursor ${JSON.stringify( + sortIds + )}.` ); break; } else { - ruleExecutionLogger.debug( - `[${cycleNum}] Found ${searchResult.hits.hits.length} of total ${totalHits} events${ - sortIds ? ` after cursor ${JSON.stringify(sortIds)}` : '' - }, last cursor ${JSON.stringify(lastSortIds)}` + ruleExecutionLogger.trace( + `Results found in cycle\n${cycleNum}. Found ${ + searchResult.hits.hits.length + } of total ${totalHits} events after cursor ${JSON.stringify( + sortIds + )}. Last cursor: ${JSON.stringify(lastSortIds)}.` ); } @@ -187,8 +189,8 @@ export const searchAfterAndBulkCreateFactory = async ({ toReturn, }); - ruleExecutionLogger.debug( - `[${cycleNum}] Created ${bulkCreateResult.createdItemsCount} alerts from ${enrichedEvents.length} events` + ruleExecutionLogger.trace( + `Created alerts from enriched events\n${cycleNum}. Created ${bulkCreateResult.createdItemsCount} alerts from ${enrichedEvents.length} events.` ); sendAlertTelemetryEvents( @@ -212,13 +214,12 @@ export const searchAfterAndBulkCreateFactory = async ({ if (lastSortIds != null && lastSortIds.length !== 0 && !hasNegativeNumber) { sortIds = lastSortIds; } else { - ruleExecutionLogger.debug(`[${cycleNum}] Unable to fetch last event cursor`); + ruleExecutionLogger.trace(`Failed to fetch last event cursor\n${cycleNum}.`); break; } } catch (exc: unknown) { ruleExecutionLogger.error( - 'Unable to extract/process events or create alerts', - JSON.stringify(exc) + `Error extracting/processing events or creating alerts\nError: ${JSON.stringify(exc)}.` ); return mergeReturns([ toReturn, @@ -229,7 +230,9 @@ export const searchAfterAndBulkCreateFactory = async ({ ]); } } - ruleExecutionLogger.debug(`Completed bulk indexing of ${toReturn.createdSignalsCount} alert`); + ruleExecutionLogger.debug( + `Completed bulk indexing. Alerts created: ${toReturn.createdSignalsCount}.` + ); if (isLoggedRequestsEnabled) { toReturn.loggedRequests = loggedRequests; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/send_telemetry_events.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/send_telemetry_events.ts index c3c4f8c4d5f23..29a5160e0420e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/send_telemetry_events.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/send_telemetry_events.ts @@ -78,6 +78,6 @@ export function sendAlertTelemetryEvents( ); eventsTelemetry.sendAsync(TelemetryChannel.ENDPOINT_ALERTS, filtered); } catch (exc) { - ruleExecutionLogger.error(`Queuing telemetry events failed: ${exc}`); + ruleExecutionLogger.debug(`Error queuing telemetry events\nError: ${exc}.`); } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/single_search_after.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/single_search_after.ts index 3bd45d5ab85e8..834234fe76857 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/single_search_after.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/single_search_after.ts @@ -73,7 +73,7 @@ export const singleSearchAfter = async < loggedRequests, }; } catch (exc) { - ruleExecutionLogger.error(`Searching events operation failed: ${exc}`); + ruleExecutionLogger.error(`Error searching events\nError: ${exc}.`); throw exc; } }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts index 6389df5b5eb4b..d9587a50710d6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts @@ -434,9 +434,9 @@ export const getRuleRangeTuples = async ({ const intervalDuration = parseInterval(interval); if (intervalDuration == null) { ruleExecutionLogger.error( - `Failed to compute gap between rule runs: could not parse rule interval "${JSON.stringify( + `Error computing gap between rule runs\nError: could not parse rule interval "${JSON.stringify( interval - )}"` + )}".` ); return { tuples, diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_events.cy.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_events.cy.ts new file mode 100644 index 0000000000000..738a9ca663ded --- /dev/null +++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_events.cy.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { installMockPrebuiltRulesPackage } from '../../../../tasks/api_calls/prebuilt_rules'; +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +import { login } from '../../../../tasks/login'; +import { visit } from '../../../../tasks/navigation'; +import { ruleDetailsUrl } from '../../../../urls/rule_details'; +import { createRule } from '../../../../tasks/api_calls/rules'; +import { waitForTheRuleToBeExecuted } from '../../../../tasks/rule_details'; +import { + getExecutionEventsTableRows, + filterExecutionEventsByMessage, + filterExecutionEventsByLogLevel, + filterExecutionEventsByEventType, + clearExecutionEventsMessageFilter, + assertAllEventsHaveLogLevel, + assertAllEventsHaveType, + assertAllEventsHaveTimestamp, + assertAllEventsHaveMessageContaining, + expandAllRows, + assertEventsAreSortedByTimestamp, +} from '../../../../tasks/execution_events'; +import { getCustomQueryRuleParams } from '../../../../objects/rule'; +import { + EXECUTION_EVENTS_TABLE, + EXECUTION_EVENTS_DATE_PICKER, + EXECUTION_EVENTS_TIMESTAMP_COLUMN, +} from '../../../../screens/execution_events'; +import { enableExtendedRuleExecutionLogging } from '../../../../tasks/api_calls/kibana_advanced_settings'; +import { setEndDate, setStartDate, showStartEndDate } from '../../../../tasks/date_picker'; + +describe( + 'Execution events table', + { + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], + env: { + ftrConfig: { + kbnServerArgs: [ + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'extendedRuleExecutionLoggingEnabled', + ])}`, + ], + }, + }, + }, + function () { + before(() => { + installMockPrebuiltRulesPackage(); + }); + + beforeEach(() => { + login(); + deleteAlertsAndRules(); + enableExtendedRuleExecutionLogging(); + + createRule({ + ...getCustomQueryRuleParams({ + enabled: true, + }), + }).then((rule) => { + visit(ruleDetailsUrl(rule.body.id, 'execution_events')); + }); + + waitForTheRuleToBeExecuted(); + }); + + it('should display the execution events table', function () { + cy.get(EXECUTION_EVENTS_TABLE).should('be.visible'); + }); + + describe('Filtering', () => { + it('should filter by event message', function () { + // Search for a term that should not match any events + filterExecutionEventsByMessage('non-existent-term-12345'); + // Verify that table is empty + getExecutionEventsTableRows().should('have.length', 0); + clearExecutionEventsMessageFilter(); + + // Search for a term that should match some events + filterExecutionEventsByMessage('completed successfully'); + expandAllRows(); + // Verify that all events contain the message + assertAllEventsHaveMessageContaining('completed successfully'); + }); + + it('should filter by log level', function () { + filterExecutionEventsByLogLevel('DEBUG'); + assertAllEventsHaveLogLevel('DEBUG'); + }); + + it('should filter by event type', function () { + filterExecutionEventsByEventType('Status'); + assertAllEventsHaveType('Status'); + }); + + it('should filter by date range', function () { + // Get the timestamp of the first row and use it for filtering + getExecutionEventsTableRows() + .first() + .find(EXECUTION_EVENTS_TIMESTAMP_COLUMN) + .invoke('text') + .then((timestamp) => { + // Plug the timestamp into the date picker as both start and end date + showStartEndDate(EXECUTION_EVENTS_DATE_PICKER); + setStartDate(timestamp, EXECUTION_EVENTS_DATE_PICKER); + setEndDate(timestamp, EXECUTION_EVENTS_DATE_PICKER); + + // Check that only events with the this timestamp are displayed + assertAllEventsHaveTimestamp(timestamp); + }); + }); + }); + + describe('Sorting', () => { + it('should sort by timestamp', function () { + cy.get(`${EXECUTION_EVENTS_TABLE} [role="columnheader"]`).contains('Timestamp').click(); + assertEventsAreSortedByTimestamp('desc'); + + cy.get(`${EXECUTION_EVENTS_TABLE} [role="columnheader"]`).contains('Timestamp').click(); + assertEventsAreSortedByTimestamp('asc'); + }); + }); + } +); diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/screens/execution_events.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/screens/execution_events.ts new file mode 100644 index 0000000000000..3db3776678ffb --- /dev/null +++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/screens/execution_events.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const EXECUTION_EVENTS_TAB = 'a[data-test-subj="navigation-execution_events"]'; + +export const EXECUTION_EVENTS_TABLE = '[data-test-subj="executionEventsTable"]'; + +export const EXECUTION_EVENTS_DATE_PICKER = '[data-test-subj="executionEventsTable-datePicker"]'; + +export const EXECUTION_EVENTS_TABLE_ROW = '[data-test-subj="executionEventsTableRow"]'; + +export const EXECUTION_EVENTS_TIMESTAMP_COLUMN = + '[data-test-subj="executionEventsTable-timestampColumn"]'; + +export const EXECUTION_EVENTS_LOG_LEVEL_COLUMN = + '[data-test-subj="executionEventsTable-logLevelColumn"]'; + +export const EXECUTION_EVENTS_EVENT_TYPE_COLUMN = + '[data-test-subj="executionEventsTable-eventTypeColumn"]'; + +export const EXECUTION_EVENTS_MESSAGE_SEARCH = '[data-test-subj="ruleEventLogMessageSearchField"]'; + +export const EXECUTION_EVENTS_LOG_LEVEL_FILTER = '[data-test-subj="logLevelFilter-popoverButton"]'; + +export const EXECUTION_EVENTS_LOG_LEVEL_FILTER_ITEM = '[data-test-subj="logLevelFilter-item"]'; + +export const EXECUTION_EVENTS_EVENT_TYPE_FILTER = + '[data-test-subj="eventTypeFilter-popoverButton"]'; + +export const EXECUTION_EVENTS_EVENT_TYPE_FILTER_ITEM = '[data-test-subj="eventTypeFilter-item"]'; diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/api_calls/kibana_advanced_settings.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/api_calls/kibana_advanced_settings.ts index 589da1a34af36..2f797ef4ae2c8 100644 --- a/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/api_calls/kibana_advanced_settings.ts +++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/api_calls/kibana_advanced_settings.ts @@ -9,6 +9,7 @@ import { SECURITY_SOLUTION_SHOW_RELATED_INTEGRATIONS_ID, AI_CHAT_EXPERIENCE_TYPE, } from '@kbn/management-settings-ids'; +import { EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING } from '@kbn/security-solution-plugin/common/constants'; import { rootRequest } from './common'; export const setKibanaSetting = (key: string, value: boolean | number | string) => { @@ -30,3 +31,11 @@ export const disableRelatedIntegrations = () => { export const setPreferredChatExperienceToAgent = () => { setKibanaSetting(AI_CHAT_EXPERIENCE_TYPE, 'agent'); }; + +export const enableExtendedRuleExecutionLogging = () => { + setKibanaSetting(EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING, true); +}; + +export const disableExtendedRuleExecutionLogging = () => { + setKibanaSetting(EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING, false); +}; diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/execution_events.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/execution_events.ts new file mode 100644 index 0000000000000..518fe799a7bf9 --- /dev/null +++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/execution_events.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EXECUTION_EVENTS_EVENT_TYPE_COLUMN, + EXECUTION_EVENTS_EVENT_TYPE_FILTER, + EXECUTION_EVENTS_EVENT_TYPE_FILTER_ITEM, + EXECUTION_EVENTS_LOG_LEVEL_COLUMN, + EXECUTION_EVENTS_LOG_LEVEL_FILTER, + EXECUTION_EVENTS_LOG_LEVEL_FILTER_ITEM, + EXECUTION_EVENTS_MESSAGE_SEARCH, + EXECUTION_EVENTS_TAB, + EXECUTION_EVENTS_TABLE, + EXECUTION_EVENTS_TABLE_ROW, + EXECUTION_EVENTS_TIMESTAMP_COLUMN, +} from '../screens/execution_events'; + +export const goToExecutionEventsTab = () => { + cy.get(EXECUTION_EVENTS_TAB).click(); +}; + +export const getExecutionEventsTableRows = () => + cy.get(EXECUTION_EVENTS_TABLE).find(EXECUTION_EVENTS_TABLE_ROW); + +export const clearExecutionEventsMessageFilter = () => { + cy.get(EXECUTION_EVENTS_MESSAGE_SEARCH).clear(); + cy.get(EXECUTION_EVENTS_MESSAGE_SEARCH).trigger('search', { waitForAnimations: true }); +}; + +export const filterExecutionEventsByMessage = (searchTerm: string) => { + cy.get(EXECUTION_EVENTS_MESSAGE_SEARCH).clear(); + cy.get(EXECUTION_EVENTS_MESSAGE_SEARCH).type(searchTerm, { force: true }); + cy.get(EXECUTION_EVENTS_MESSAGE_SEARCH).trigger('search', { waitForAnimations: true }); + cy.get(EXECUTION_EVENTS_TABLE).should('have.class', 'euiBasicTable-loading'); + cy.get(EXECUTION_EVENTS_TABLE).should('not.have.class', 'euiBasicTable-loading'); +}; + +export const filterExecutionEventsByLogLevel = (logLevel: string) => { + cy.get(EXECUTION_EVENTS_LOG_LEVEL_FILTER).click(); + cy.get(EXECUTION_EVENTS_LOG_LEVEL_FILTER_ITEM).contains(logLevel).click(); + // Close the popover + cy.get(EXECUTION_EVENTS_LOG_LEVEL_FILTER).click(); +}; + +export const filterExecutionEventsByEventType = (eventType: string) => { + cy.get(EXECUTION_EVENTS_EVENT_TYPE_FILTER).click(); + cy.get(EXECUTION_EVENTS_EVENT_TYPE_FILTER_ITEM).contains(eventType).click(); + // Close the popover + cy.get(EXECUTION_EVENTS_EVENT_TYPE_FILTER).click(); +}; + +export const assertAllEventsHaveLogLevel = (logLevel: string) => { + getExecutionEventsTableRows().should(($rows) => { + expect($rows.length).to.be.at.least(1); + $rows.each((_, row) => { + const logLevelBadge = Cypress.$(row).find(EXECUTION_EVENTS_LOG_LEVEL_COLUMN); + expect(logLevelBadge.text()).to.contain(logLevel); + }); + }); +}; + +export const assertAllEventsHaveType = (status: string) => { + getExecutionEventsTableRows().should(($rows) => { + expect($rows.length).to.be.at.least(1); + $rows.each((_, row) => { + const statusBadge = Cypress.$(row).find(EXECUTION_EVENTS_EVENT_TYPE_COLUMN); + expect(statusBadge.text()).to.contain(status); + }); + }); +}; + +export const assertAllEventsHaveTimestamp = (timestamp: string) => { + getExecutionEventsTableRows().should(($rows) => { + expect($rows.length).to.be.at.least(1); + $rows.each((_, row) => { + const timestampBadge = Cypress.$(row).find(EXECUTION_EVENTS_TIMESTAMP_COLUMN); + expect(timestampBadge.text()).to.contain(timestamp); + }); + }); +}; + +export const expandAllRows = () => { + getExecutionEventsTableRows().should('have.length.at.least', 1); + + getExecutionEventsTableRows().each(($row) => { + cy.wrap($row).find('[data-test-subj="executionEventsTable-expandButton"]').click(); + }); +}; + +export const collapseAllRows = () => { + getExecutionEventsTableRows().each(($row) => { + cy.wrap($row).find('[data-test-subj="executionEventsTable-collapseButton"]').click(); + }); +}; + +export const assertAllEventsHaveMessageContaining = (message: string) => { + getExecutionEventsTableRows().should('have.length.at.least', 1); + + getExecutionEventsTableRows().each(($row, index) => { + // Verify expanded content contains the message + cy.wrap($row) + .next('.euiTableRow-isExpandedRow') + .find('[data-test-subj="executionEventsTable-eventDetails"]') + .should('exist') + .find('.euiCodeBlock') + .first() + .should('contain.text', message); + }); +}; + +export const assertEventsAreSortedByTimestamp = (direction: 'asc' | 'desc') => { + getExecutionEventsTableRows().should(($rows) => { + expect($rows.length).to.be.at.least(2); + + // Take timestamp strings from every row, convert to numbers + const timestamps = $rows + .map((_, row) => + new Date(Cypress.$(row).find(EXECUTION_EVENTS_TIMESTAMP_COLUMN).text()).getTime() + ) + .get(); + + const sorted = [...timestamps].sort((a, b) => (direction === 'asc' ? a - b : b - a)); + + expect(timestamps).to.deep.equal(sorted); + }); +}; diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/urls/rule_details.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/urls/rule_details.ts index edaf3e77746d4..1dc497b646184 100644 --- a/x-pack/solutions/security/test/security_solution_cypress/cypress/urls/rule_details.ts +++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/urls/rule_details.ts @@ -9,7 +9,8 @@ export type RuleDetailsTabs = | 'alerts' | 'rule_exceptions' | 'endpoint_exceptions' - | 'execution_results'; + | 'execution_results' + | 'execution_events'; export function ruleDetailsUrl(ruleId: string, tab?: RuleDetailsTabs): string { return `/app/security/rules/id/${ruleId}${tab ? `/${tab}` : ''}`;