diff --git a/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.test.ts b/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.test.ts index 29511ab9b199e..3ff6aa895da6c 100644 --- a/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.test.ts +++ b/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.test.ts @@ -101,44 +101,142 @@ describe('Index Pattern Fetcher - server', () => { expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(0); }); - describe('getExistingIndices', () => { - test('getExistingIndices returns the valid matched indices', async () => { - indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); - indexPatterns.getFieldsForWildcard = jest - .fn() - .mockResolvedValueOnce({ indices: ['length'] }) - .mockResolvedValue({ indices: [] }); - const result = await indexPatterns.getIndexPatternsWithMatches([ - 'packetbeat-*', - 'filebeat-*', - ]); - expect(indexPatterns.getFieldsForWildcard).toBeCalledTimes(2); - expect(result.length).toBe(1); + describe('getIndexPatternMatches', () => { + describe('without negated index patterns', () => { + test('returns the valid matched index patterns', async () => { + indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); + indexPatterns.getFieldsForWildcard = jest + .fn() + .mockResolvedValueOnce({ indices: ['index1'] }) + .mockResolvedValue({ indices: [] }); + + const result = await indexPatterns.getIndexPatternMatches(['packetbeat-*', 'filebeat-*']); + + expect(result.matchedIndexPatterns).toEqual(['packetbeat-*']); + }); + + test('returns the valid matched indices', async () => { + indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); + indexPatterns.getFieldsForWildcard = jest + .fn() + .mockResolvedValueOnce({ indices: ['index1'] }) + .mockResolvedValue({ indices: [] }); + + const result = await indexPatterns.getIndexPatternMatches(['packetbeat-*', 'filebeat-*']); + + expect(result.matchedIndices).toEqual(['index1']); + }); + + test('returns the valid matched indices per index pattern', async () => { + indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); + indexPatterns.getFieldsForWildcard = jest + .fn() + .mockResolvedValueOnce({ indices: ['index1'] }) + .mockResolvedValue({ indices: ['index2'] }); + + const result = await indexPatterns.getIndexPatternMatches(['packetbeat-*', 'filebeat-*']); + + expect(result.matchesByIndexPattern).toEqual({ + 'packetbeat-*': ['index1'], + 'filebeat-*': ['index2'], + }); + }); }); - test('getExistingIndices checks the positive pattern if provided with a negative pattern', async () => { - indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); - const mockFn = jest.fn().mockResolvedValue({ indices: ['length'] }); - indexPatterns.getFieldsForWildcard = mockFn; - const result = await indexPatterns.getIndexPatternsWithMatches(['-filebeat-*', 'filebeat-*']); - expect(mockFn.mock.calls[0][0].pattern).toEqual('filebeat-*'); - expect(mockFn.mock.calls[1][0].pattern).toEqual('filebeat-*'); - expect(result).toEqual(['-filebeat-*', 'filebeat-*']); + describe('with negated index patterns', () => { + test('returns the valid matched index patterns', async () => { + indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); + const mockFn = jest.fn().mockResolvedValue({ indices: ['index1'] }); + indexPatterns.getFieldsForWildcard = mockFn; + + const result = await indexPatterns.getIndexPatternMatches([ + '-filebeat-*', + 'filebeat-*', + 'logs-*', + '-logs-excluded-*', + ]); + + expect(result.matchedIndexPatterns).toEqual(['filebeat-*', 'logs-*']); + }); + + test('returns the valid matched indices', async () => { + indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); + const mockFn = jest.fn().mockResolvedValue({ indices: ['index1'] }); + indexPatterns.getFieldsForWildcard = mockFn; + + const result = await indexPatterns.getIndexPatternMatches([ + '-filebeat-*', + 'filebeat-*', + 'logs-*', + '-logs-excluded-*', + ]); + + expect(result.matchedIndices).toEqual(['index1']); + }); + + test('returns the valid matched indices per index pattern', async () => { + indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); + const mockFn = jest + .fn() + .mockResolvedValueOnce({ indices: ['index1'] }) + .mockResolvedValue({ indices: ['index2'] }); + indexPatterns.getFieldsForWildcard = mockFn; + + const result = await indexPatterns.getIndexPatternMatches([ + '-filebeat-*', + 'filebeat-*', + 'logs-*', + '-logs-excluded-*', + ]); + + expect(result.matchesByIndexPattern).toEqual({ + 'filebeat-*': ['index1'], + 'logs-*': ['index2'], + }); + }); + + test('queries each positive pattern with all negated patterns for field caps', async () => { + indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); + const mockFn = jest.fn().mockResolvedValue({ indices: ['length'] }); + indexPatterns.getFieldsForWildcard = mockFn; + + await indexPatterns.getIndexPatternMatches([ + '-filebeat-*', + 'filebeat-*', + 'logs-*', + '-logs-excluded-*', + ]); + + expect(mockFn.mock.calls[0][0].pattern).toEqual([ + 'filebeat-*', + '-filebeat-*', + '-logs-excluded-*', + ]); + expect(mockFn.mock.calls[1][0].pattern).toEqual([ + 'logs-*', + '-filebeat-*', + '-logs-excluded-*', + ]); + }); }); - test('getExistingIndices handles an error', async () => { + test('handles an error', async () => { indexPatterns = new IndexPatternsFetcher(esClient, optionalParams); indexPatterns.getFieldsForWildcard = jest .fn() - .mockImplementationOnce(async () => { - throw new DataViewMissingIndices('Catch me if you can!'); - }) - .mockImplementation(() => Promise.resolve({ indices: ['length'] })); - const result = await indexPatterns.getIndexPatternsWithMatches([ - 'packetbeat-*', - 'filebeat-*', - ]); - expect(result).toEqual(['filebeat-*']); + .mockRejectedValueOnce(new DataViewMissingIndices('Catch me if you can!')) + .mockResolvedValue({ indices: ['index1'] }); + + const result = await indexPatterns.getIndexPatternMatches(['packetbeat-*', 'filebeat-*']); + + expect(result).toMatchObject({ + matchedIndexPatterns: ['filebeat-*'], + matchedIndices: ['index1'], + matchesByIndexPattern: { + 'packetbeat-*': [], + 'filebeat-*': ['index1'], + }, + }); }); }); }); diff --git a/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.ts b/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.ts index ff78341b26ed5..86ba62de9fd45 100644 --- a/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.ts +++ b/src/platform/plugins/shared/data_views/server/fetcher/index_patterns_fetcher.ts @@ -10,7 +10,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server'; import { keyBy } from 'lodash'; -import { defer, from } from 'rxjs'; +import { catchError, defer, from, map, of } from 'rxjs'; import { rateLimitingForkJoin } from '../../common/data_views/utils'; import type { QueryDslQueryContainer } from '../../common/types'; @@ -48,6 +48,12 @@ interface IndexPatternsFetcherOptionalParams { rollupsEnabled?: boolean; } +export interface GetIndexPatternMatchesResult { + matchedIndexPatterns: string[]; + matchedIndices?: string[]; + matchesByIndexPattern?: Record; +} + export class IndexPatternsFetcher { private readonly uiSettingsClient?: IUiSettingsClient; private readonly allowNoIndices: boolean; @@ -159,37 +165,84 @@ export class IndexPatternsFetcher { } /** - * Get existing index pattern list by providing string array index pattern list. - * @param indexPatterns - index pattern list - * @returns index pattern list of index patterns that match indices + * Checks whether the passed index pattern is an excluding one. + * The excluding index pattern starts with a dash, e.g. "-logs-excluded-*" + * meaning all indices matching "logs-excluded-*" will be excluded from search + * + * @param indexPattern - Index pattern to check + * @returns Whether the passed index pattern is a negated one */ - async getIndexPatternsWithMatches(indexPatterns: string[]): Promise { - const indexPatternsObs = indexPatterns.map((indexPattern) => { - // when checking a negative pattern, check if the positive pattern exists - const indexToQuery = indexPattern.trim().startsWith('-') - ? indexPattern.trim().substring(1) - : indexPattern.trim(); + isExcludingIndexPattern(indexPattern: string): boolean { + return indexPattern.trim().startsWith('-'); + } + + /** + * For each input pattern, checks whether it resolves to at least one backing index. + * + * Including index patterns (not starting with `-`) are checked with field caps using that pattern + * together with every excluding index pattern (starting with `-`) in the list, so resolution matches + * Elasticsearch multi-target syntax. + * + * @param indexPatterns - Index patterns to check (may include wildcards and excluded entries). + * @returns Resolves to {@link GetIndexPatternMatchesResult}: + * - `matchedIndexPatterns`: input patterns that matched at least one index. + * - `matchedIndices`: deduplicated concrete index names matching index patterns (omitted on failure). + * - `matchesByIndexPattern`: per-input-pattern matched indices (omitted on failure). + */ + async getIndexPatternMatches(indexPatterns: string[]): Promise { + const excludingIndexPatterns = indexPatterns.filter(this.isExcludingIndexPattern); + const indexPatternsToMatch = indexPatterns + .filter((indexPattern) => !this.isExcludingIndexPattern(indexPattern)) + .map((indexPattern) => [indexPattern, ...excludingIndexPatterns]); + + const matchIndexPatterns = indexPatternsToMatch.map((pattern) => { return defer(() => from( this.getFieldsForWildcard({ - // check one field to keep request fast/small fields: ['_id'], - pattern: indexToQuery, + pattern, }) + ).pipe( + // expecting pattern[0] to contain an including index pattern + // and pattern[1..end] to contain excluding index patterns + map((match) => ({ ...match, indexPattern: pattern[0] })), + catchError(() => of({ fields: [], indices: [], indexPattern: pattern[0] })) ) ); }); - return new Promise((resolve) => { - rateLimitingForkJoin(indexPatternsObs, 3, { fields: [], indices: [] }).subscribe((value) => { - resolve(value.map((v) => v.indices.length > 0)); + return new Promise((resolve) => { + rateLimitingForkJoin(matchIndexPatterns, 3, { + fields: [], + indices: [], + indexPattern: '', + }).subscribe((indexPatternMatches) => { + const matchedIndexPatterns: string[] = []; + const uniqueMatchedIndices = new Set(); + const matchesByIndexPattern: Record = {}; + + for (const indexPatternMatch of indexPatternMatches) { + const { indexPattern, indices } = indexPatternMatch; + + matchesByIndexPattern[indexPattern] = indices; + + if (indices.length === 0) { + continue; + } + + matchedIndexPatterns.push(indexPattern); + + for (const index of indices) { + uniqueMatchedIndices.add(index); + } + } + + resolve({ + matchedIndexPatterns, + matchedIndices: Array.from(uniqueMatchedIndices), + matchesByIndexPattern, + }); }); - }) - .then((allPatterns: boolean[]) => - indexPatterns.filter( - (indexPattern, i, self) => self.indexOf(indexPattern) === i && allPatterns[i] - ) - ) - .catch(() => indexPatterns); + }).catch(() => ({ matchedIndexPatterns: [] })); } } diff --git a/src/platform/plugins/shared/data_views/server/rest_api_routes/internal/existing_indices.ts b/src/platform/plugins/shared/data_views/server/rest_api_routes/internal/existing_indices.ts index 54aa7ba824e68..fe0118930900c 100644 --- a/src/platform/plugins/shared/data_views/server/rest_api_routes/internal/existing_indices.ts +++ b/src/platform/plugins/shared/data_views/server/rest_api_routes/internal/existing_indices.ts @@ -44,8 +44,8 @@ export const handler: RequestHandler<{}, { indices: string | string[] }, string[ const elasticsearchClient = core.elasticsearch.client.asCurrentUser; const indexPatterns = new IndexPatternsFetcher(elasticsearchClient); - const response: string[] = await indexPatterns.getIndexPatternsWithMatches(indexArray); - return res.ok({ body: response }); + const { matchedIndexPatterns } = await indexPatterns.getIndexPatternMatches(indexArray); + return res.ok({ body: matchedIndexPatterns }); } catch (error) { return res.badRequest(); } diff --git a/x-pack/platform/plugins/shared/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/platform/plugins/shared/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index ff8403a24ad85..5aab5e8d7cbc8 100644 --- a/x-pack/platform/plugins/shared/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/platform/plugins/shared/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -801,6 +801,7 @@ export function updateEvent(event: IEvent, opts: UpdateEventOpts) { if (consumerMetrics) { set(event, 'kibana.alert.rule.execution.metrics', { ...event.kibana?.alert?.rule?.execution?.metrics, + matched_indices_count: consumerMetrics.matched_indices_count, alerts_candidate_count: consumerMetrics.alerts_candidate_count, alerts_suppressed_count: consumerMetrics.alerts_suppressed_count, frozen_indices_queried_count: consumerMetrics.frozen_indices_queried_count, diff --git a/x-pack/platform/plugins/shared/alerting/server/task_runner/ad_hoc_task_runner.test.ts b/x-pack/platform/plugins/shared/alerting/server/task_runner/ad_hoc_task_runner.test.ts index 0d958321e0c32..d4c10475682d6 100644 --- a/x-pack/platform/plugins/shared/alerting/server/task_runner/ad_hoc_task_runner.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/task_runner/ad_hoc_task_runner.test.ts @@ -595,6 +595,7 @@ describe('Ad Hoc Task Runner', () => { test('passes consumer metrics to AlertingEventLogger', async () => { const consumerMetrics = { + matched_indices_count: 3, alerts_candidate_count: 100, total_enrichment_duration_ms: 50, }; diff --git a/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner.test.ts b/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner.test.ts index 51a2a4689c77e..65c886c6459ee 100644 --- a/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner.test.ts @@ -395,9 +395,10 @@ describe('Task Runner', () => { test('passes consumer metrics to AlertingEventLogger', async () => { const consumerMetrics = { + matched_indices_count: 3, + frozen_indices_queried_count: 3, alerts_candidate_count: 42, alerts_suppressed_count: 7, - frozen_indices_queried_count: 3, }; ruleType.executor.mockImplementation(async ({ services: { ruleMonitoringService } }) => { ruleMonitoringService?.setMetrics(consumerMetrics); diff --git a/x-pack/platform/plugins/shared/alerting/server/types.ts b/x-pack/platform/plugins/shared/alerting/server/types.ts index bfbe693928151..a2956725e6e86 100644 --- a/x-pack/platform/plugins/shared/alerting/server/types.ts +++ b/x-pack/platform/plugins/shared/alerting/server/types.ts @@ -457,6 +457,7 @@ export interface ConsumerExecutionMetrics { total_enrichment_duration_ms: number; gap_duration_s: number; gap_range: { lte: string; gte: string }; + matched_indices_count: number; alerts_candidate_count: number; alerts_suppressed_count: number; frozen_indices_queried_count: number; diff --git a/x-pack/platform/plugins/shared/event_log/generated/mappings.json b/x-pack/platform/plugins/shared/event_log/generated/mappings.json index bab32b1306272..33a8753ca3df8 100644 --- a/x-pack/platform/plugins/shared/event_log/generated/mappings.json +++ b/x-pack/platform/plugins/shared/event_log/generated/mappings.json @@ -477,6 +477,9 @@ } } }, + "matched_indices_count": { + "type": "long" + }, "frozen_indices_queried_count": { "type": "long" }, diff --git a/x-pack/platform/plugins/shared/event_log/generated/schemas.ts b/x-pack/platform/plugins/shared/event_log/generated/schemas.ts index ae7c76d82d515..97987c1ebac1a 100644 --- a/x-pack/platform/plugins/shared/event_log/generated/schemas.ts +++ b/x-pack/platform/plugins/shared/event_log/generated/schemas.ts @@ -209,6 +209,7 @@ export const EventSchema = schema.maybe( type: ecsString(), }) ), + matched_indices_count: ecsStringOrNumber(), frozen_indices_queried_count: ecsStringOrNumber(), rule_type_run_duration_ms: ecsStringOrNumber(), process_alerts_duration_ms: ecsStringOrNumber(), diff --git a/x-pack/platform/plugins/shared/event_log/scripts/mappings.js b/x-pack/platform/plugins/shared/event_log/scripts/mappings.js index 14360e7e23bb9..a967afe94f310 100644 --- a/x-pack/platform/plugins/shared/event_log/scripts/mappings.js +++ b/x-pack/platform/plugins/shared/event_log/scripts/mappings.js @@ -243,6 +243,9 @@ exports.EcsCustomPropertyMappings = { }, }, }, + matched_indices_count: { + type: 'long', + }, frozen_indices_queried_count: { type: 'long', }, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts index 864e2115cfaba..421e783ff8ffb 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts @@ -589,9 +589,9 @@ describe('AIAssistantKnowledgeBaseDataClient', () => { describe('getAssistantTools', () => { it('should return structured tools for relevant index entries', async () => { - IndexPatternsFetcher.prototype.getIndexPatternsWithMatches = jest + IndexPatternsFetcher.prototype.getIndexPatternMatches = jest .fn() - .mockResolvedValue(['test']); + .mockResolvedValue({ matchedIndexPatterns: ['test'] }); esClientMock.search.mockReturnValue( // @ts-expect-error not full response interface getKnowledgeBaseEntrySearchEsMock('index') diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts index fa60b81f7a368..4819979d05b17 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts @@ -920,13 +920,13 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { if (results) { const entries = transformESSearchToKnowledgeBaseEntry(results.data) as IndexEntry[]; const indexPatternFetcher = new IndexPatternsFetcher(esClient); - const existingIndices = await indexPatternFetcher.getIndexPatternsWithMatches( + const { matchedIndexPatterns } = await indexPatternFetcher.getIndexPatternMatches( map(entries, 'index') ); return ( entries // Filter out any IndexEntries that don't have an existing index - .filter((entry) => existingIndices.includes(entry.index)) + .filter((entry) => matchedIndexPatterns.includes(entry.index)) .map((indexEntry) => { return getStructuredToolForIndexEntry({ indexEntry, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index de72f1086d62f..f77d15752a5bc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -24,7 +24,7 @@ import { createMockEndpointAppContextService } from '../../../../endpoint/mocks' jest.mock('@kbn/data-views-plugin/server', () => ({ ...jest.requireActual('@kbn/data-views-plugin/server'), IndexPatternsFetcher: jest.fn().mockImplementation(() => ({ - getIndexPatternsWithMatches: jest.fn().mockResolvedValue(['some-index']), + getIndexPatternMatches: jest.fn().mockResolvedValue({ matchedIndexPatterns: ['some-index'] }), })), })); @@ -127,7 +127,7 @@ describe('Custom Query Alerts', () => { it('short-circuits and writes a warning if no indices are found', async () => { (IndexPatternsFetcher as jest.Mock).mockImplementationOnce(() => ({ - getIndexPatternsWithMatches: jest.fn().mockResolvedValue([]), + getIndexPatternMatches: jest.fn().mockResolvedValue({ matchedIndexPatterns: [] }), })); const queryAlertType = securityRuleTypeWrapper( createQueryAlertType({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/validation/run_execution_validation.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/validation/run_execution_validation.ts index 3bc0d37ec6aca..929ac9bec1706 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/validation/run_execution_validation.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/validation/run_execution_validation.ts @@ -67,9 +67,14 @@ export const runExecutionValidation = async ( const indexPatterns = new IndexPatternsFetcher(scopedClusterClient.asCurrentUser); try { - const indexPatternsWithMatches = await indexPatterns.getIndexPatternsWithMatches(inputIndex); + const { matchedIndexPatterns, matchedIndices } = await indexPatterns.getIndexPatternMatches( + inputIndex + ); + + // Collect rule execution metrics + ruleExecutionLogger.logMetric('matched_indices_count', matchedIndices?.length); - if (indexPatternsWithMatches.length === 0) { + if (matchedIndexPatterns.length === 0) { warnings.push( `Unable to find matching indices for rule ${ruleName}. This warning will persist until one of the following occurs: a matching index is created or the rule is disabled.` ); @@ -81,11 +86,10 @@ export const runExecutionValidation = async ( if (isThreatParams(params)) { try { - const threatIndexPatternsWithMatches = await indexPatterns.getIndexPatternsWithMatches( - params.threatIndex - ); + const { matchedIndexPatterns: matchedThreatIndexPatterns } = + await indexPatterns.getIndexPatternMatches(params.threatIndex); - if (threatIndexPatternsWithMatches.length === 0) { + if (matchedThreatIndexPatterns.length === 0) { warnings.push( `Unable to find matching threat indicator indices for rule ${ruleName}. This warning will persist until one of the following occurs: a matching threat index is created or the rule is disabled.` ); diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/eql/trial_license_complete_tier/eql_metrics.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/eql/trial_license_complete_tier/eql_metrics.ts index aa58f22ea93bc..cc2ff73949bfa 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/eql/trial_license_complete_tier/eql_metrics.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/eql/trial_license_complete_tier/eql_metrics.ts @@ -6,6 +6,7 @@ */ import expect from 'expect'; +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import { createRule, deleteAllRules, deleteAllAlerts } from '@kbn/detections-response-ftr-services'; import { getEqlRuleParams, @@ -21,7 +22,7 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const { indexListOfDocuments: indexListOfSourceDocuments } = dataGeneratorFactory({ es, - index: 'logs-1', + index: 'test-data-1', log, }); @@ -31,29 +32,133 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllRules(supertest, log); await es.indices.delete({ - index: 'logs-1', + index: 'test-data-1,test-data-2', ignore_unavailable: true, }); - await es.indices.create({ - index: 'logs-1', - mappings: { - properties: { - '@timestamp': { - type: 'date', - }, - host: { - properties: { - name: { - type: 'keyword', - }, + + const mappings: MappingTypeMapping = { + properties: { + '@timestamp': { + type: 'date', + }, + host: { + properties: { + name: { + type: 'keyword', }, }, }, }, + }; + await es.indices.create({ + index: 'test-data-1', + mappings, + }); + await es.indices.create({ + index: 'test-data-2', + mappings, }); }); describe('metrics collection', () => { + describe('matched_indices_count', () => { + it('records matched_indices_count for one matching index pattern', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { + name: 'test', + }, + }; + const rule = getEqlRuleParams({ + index: ['test-data-1'], + query: 'any where true', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenAlerts(supertest, log, es, createdRule); + + expect(alerts.hits.hits).toHaveLength(1); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(1); + }); + + it('records matched_indices_count for a single index pattern with wildcard', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { + name: 'test', + }, + }; + const rule = getEqlRuleParams({ + index: ['test-data-*'], + query: 'any where true', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenAlerts(supertest, log, es, createdRule); + + expect(alerts.hits.hits).toHaveLength(1); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + + it('records matched_indices_count for multiple matching index patterns', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { + name: 'test', + }, + }; + const rule = getEqlRuleParams({ + index: ['test-da*', 'test-data-1', 'test-data-2'], + query: 'any where true', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenAlerts(supertest, log, es, createdRule); + + expect(alerts.hits.hits).toHaveLength(1); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + }); + describe('alerts_candidate_count', () => { it('records alerts_candidate_count value', async () => { const timestamp = new Date().toISOString(); @@ -64,7 +169,7 @@ export default ({ getService }: FtrProviderContext) => { }, }; const rule = getEqlRuleParams({ - index: ['logs-1'], + index: ['test-data-1'], query: 'any where true', from: 'now-35m', interval: '30m', @@ -93,7 +198,7 @@ export default ({ getService }: FtrProviderContext) => { }, }; const rule = getEqlRuleParams({ - index: ['logs-1'], + index: ['test-data-1'], query: 'any where true', alert_suppression: { group_by: ['host.name'], diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/esql/trial_license_complete_tier/esql_metrics.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/esql/trial_license_complete_tier/esql_metrics.ts index bddd5e4b5ad16..4417c27830a00 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/esql/trial_license_complete_tier/esql_metrics.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/esql/trial_license_complete_tier/esql_metrics.ts @@ -6,6 +6,7 @@ */ import expect from 'expect'; +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import { createRule, deleteAllRules, deleteAllAlerts } from '@kbn/detections-response-ftr-services'; import { getEsqlRuleParams, @@ -21,7 +22,7 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const { indexListOfDocuments: indexListOfSourceDocuments } = dataGeneratorFactory({ es, - index: 'logs-1', + index: 'test-data-1', log, }); @@ -31,29 +32,115 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllRules(supertest, log); await es.indices.delete({ - index: 'logs-1', + index: 'test-data-1,test-data-2', ignore_unavailable: true, }); - await es.indices.create({ - index: 'logs-1', - mappings: { - properties: { - '@timestamp': { - type: 'date', - }, - host: { - properties: { - name: { - type: 'keyword', - }, + + const mappings: MappingTypeMapping = { + properties: { + '@timestamp': { + type: 'date', + }, + host: { + properties: { + name: { + type: 'keyword', }, }, }, }, + }; + await es.indices.create({ + index: 'test-data-1', + mappings, + }); + await es.indices.create({ + index: 'test-data-2', + mappings, }); }); describe('metrics collection', () => { + describe('matched_indices_count', () => { + it('records matched_indices_count for one source index in the ES|QL query', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getEsqlRuleParams({ + query: 'from test-data-1 metadata _id, _index, _version', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(1); + }); + + it('records matched_indices_count for a single index pattern with wildcard in the ES|QL query', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getEsqlRuleParams({ + query: 'from test-data-* metadata _id, _index, _version', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + + it('records matched_indices_count for multiple source indices in the ES|QL query', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getEsqlRuleParams({ + query: 'from test-da*, test-data-1, test-data-2 metadata _id, _index, _version', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + }); + describe('alerts_candidate_count', () => { it('records alerts_candidate_count value', async () => { const timestamp = new Date().toISOString(); @@ -62,7 +149,7 @@ export default ({ getService }: FtrProviderContext) => { host: { name: 'test-1' }, }; const rule = getEsqlRuleParams({ - query: 'from logs-1 metadata _id, _index, _version', + query: 'from test-data-1 metadata _id, _index, _version', from: 'now-35m', interval: '30m', enabled: true, @@ -85,7 +172,7 @@ export default ({ getService }: FtrProviderContext) => { host: { name: 'test-1' }, }; const rule = getEsqlRuleParams({ - query: 'from logs-1 metadata _id, _index, _version', + query: 'from test-data-1 metadata _id, _index, _version', alert_suppression: { group_by: ['host.name'], duration: { diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match_metrics.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match_metrics.ts index 065d006667c75..4f2f2f1762e33 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match_metrics.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match_metrics.ts @@ -6,7 +6,7 @@ */ import expect from 'expect'; -import { v4 as uuidv4 } from 'uuid'; +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import { deleteAllAlerts, deleteAllRules, createRule } from '@kbn/detections-response-ftr-services'; import { dataGeneratorFactory, @@ -22,12 +22,12 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const { indexListOfDocuments: indexThreatIndicatorDocuments } = dataGeneratorFactory({ es, - index: 'logs-ti_1', + index: 'ti_test_1', log, }); const { indexListOfDocuments: indexListOfSourceDocuments } = dataGeneratorFactory({ es, - index: 'logs-1', + index: 'test-data-1', log, }); @@ -35,27 +35,168 @@ export default ({ getService }: FtrProviderContext) => { beforeEach(async () => { await deleteAllAlerts(supertest, log, es); await deleteAllRules(supertest, log); + + await es.indices.delete({ + index: 'ti_test_1,ti_test_2,test-data-1,test-data-2', + ignore_unavailable: true, + }); + + const mappings: MappingTypeMapping = { + properties: { + '@timestamp': { + type: 'date', + }, + id: { + type: 'keyword', + }, + host: { + properties: { + name: { + type: 'keyword', + }, + }, + }, + }, + }; + await es.indices.create({ + index: 'ti_test_1', + mappings, + }); + await es.indices.create({ + index: 'ti_test_2', + mappings, + }); + await es.indices.create({ + index: 'test-data-1', + mappings, + }); + await es.indices.create({ + index: 'test-data-2', + mappings, + }); }); describe('metrics collection', () => { + describe('matched_indices_count', () => { + it('records matched_indices_count for one matching source index pattern', async () => { + const timestamp = new Date().toISOString(); + const threatIndicatorDocument = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getThreatMatchRuleParams({ + threat_query: '*:*', + threat_index: ['ti_test_1'], + query: '*:*', + index: ['test-data-1'], + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexThreatIndicatorDocuments([threatIndicatorDocument]); + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(1); + }); + + it('records matched_indices_count for a single index pattern with wildcard', async () => { + const timestamp = new Date().toISOString(); + const threatIndicatorDocument = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getThreatMatchRuleParams({ + threat_query: '*:*', + threat_index: ['ti_test_1'], + query: '*:*', + index: ['test-data-*'], + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexThreatIndicatorDocuments([threatIndicatorDocument]); + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + + it('records matched_indices_count for multiple matching source index patterns', async () => { + const timestamp = new Date().toISOString(); + const threatIndicatorDocument = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getThreatMatchRuleParams({ + threat_query: '*:*', + threat_index: ['ti_test_1'], + query: '*:*', + index: ['test-da*', 'test-data-1', 'test-data-2'], + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexThreatIndicatorDocuments([threatIndicatorDocument]); + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + }); + describe('alerts_candidate_count', () => { it('records alerts_candidate_count value', async () => { - const id = uuidv4(); const timestamp = new Date().toISOString(); const threatIndicatorDocument = { '@timestamp': timestamp, host: { name: 'test-1' }, }; const document = { - id, '@timestamp': timestamp, host: { name: 'test-1' }, }; const rule = getThreatMatchRuleParams({ threat_query: '*:*', - threat_index: ['logs-ti_1'], - query: `id : "${id}"`, - index: ['logs-1'], + threat_index: ['ti_test_1'], + query: '*:*', + index: ['test-data-1'], from: 'now-35m', interval: '30m', enabled: true, @@ -73,22 +214,20 @@ export default ({ getService }: FtrProviderContext) => { }); it('records alerts_candidate_count higher than the number of suppressed alerts', async () => { - const id = uuidv4(); const timestamp = new Date().toISOString(); const threatIndicatorDocument = { '@timestamp': timestamp, host: { name: 'test-1' }, }; const document = { - id, '@timestamp': timestamp, host: { name: 'test-1' }, }; const rule = getThreatMatchRuleParams({ threat_query: '*:*', - threat_index: ['logs-ti_1'], - query: `id : "${id}"`, - index: ['logs-1'], + threat_index: ['ti_test_1'], + query: '*:*', + index: ['test-data-1'], from: 'now-35m', interval: '30m', alert_suppression: { diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/machine_learning/trial_license_complete_tier/machine_learning_metrics.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/machine_learning/trial_license_complete_tier/machine_learning_metrics.ts index 9b037a999a7b2..fe5c6995a30c1 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/machine_learning/trial_license_complete_tier/machine_learning_metrics.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/machine_learning/trial_license_complete_tier/machine_learning_metrics.ts @@ -82,6 +82,30 @@ export default ({ getService }: FtrProviderContext) => { }); describe('metrics collection', () => { + describe('matched_indices_count', () => { + it('does not record matched_indices_count for machine learning rules', async () => { + const createdRule = await createRule( + supertest, + log, + getMLRuleParams({ + ...sharedMlRuleRewrites, + anomaly_threshold: 30, + from: '1900-01-01T00:00:00.000Z', + enabled: true, + }) + ); + await getOpenAlerts(supertest, log, es, createdRule); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBeUndefined(); + }); + }); + describe('alerts_candidate_count', () => { it('records alerts_candidate_count value', async () => { const createdRule = await createRule( diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/new_terms/trial_license_complete_tier/new_terms_metrics.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/new_terms/trial_license_complete_tier/new_terms_metrics.ts index 7ce693018331d..c819d6abc7f17 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/new_terms/trial_license_complete_tier/new_terms_metrics.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/new_terms/trial_license_complete_tier/new_terms_metrics.ts @@ -6,6 +6,7 @@ */ import expect from 'expect'; +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import { createRule, deleteAllRules, deleteAllAlerts } from '@kbn/detections-response-ftr-services'; import { dataGeneratorFactory, @@ -21,7 +22,7 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const { indexListOfDocuments } = dataGeneratorFactory({ es, - index: 'logs-1', + index: 'test-data-1', log, }); @@ -31,32 +32,151 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllRules(supertest, log); await es.indices.delete({ - index: 'logs-1', + index: 'test-data-1,test-data-2', ignore_unavailable: true, }); - await es.indices.create({ - index: 'logs-1', - mappings: { - properties: { - '@timestamp': { - type: 'date', - }, - host: { - properties: { - name: { - type: 'keyword', - }, - ip: { - type: 'ip', - }, + + const mappings: MappingTypeMapping = { + properties: { + '@timestamp': { + type: 'date', + }, + host: { + properties: { + name: { + type: 'keyword', }, }, }, }, + }; + await es.indices.create({ + index: 'test-data-1', + mappings, + }); + await es.indices.create({ + index: 'test-data-2', + mappings, }); }); describe('metrics collection', () => { + describe('matched_indices_count', () => { + it('records matched_indices_count for one matching index pattern', async () => { + const timestamp = new Date().toISOString(); + const documents = [ + { + '@timestamp': timestamp, + host: { name: 'host-0' }, + }, + { + '@timestamp': timestamp, + host: { name: 'host-0' }, + }, + ]; + + await indexListOfDocuments(documents); + + const createdRule = await createRule( + supertest, + log, + getNewTermsRuleParams({ + index: ['test-data-1'], + query: '*:*', + new_terms_fields: ['host.name'], + history_window_start: 'now-1h', + from: 'now-35m', + interval: '30m', + enabled: true, + }) + ); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(1); + }); + + it('records matched_indices_count for a single index pattern with wildcard', async () => { + const timestamp = new Date().toISOString(); + const documents = [ + { + '@timestamp': timestamp, + host: { name: 'host-0' }, + }, + { + '@timestamp': timestamp, + host: { name: 'host-0' }, + }, + ]; + + await indexListOfDocuments(documents); + + const createdRule = await createRule( + supertest, + log, + getNewTermsRuleParams({ + index: ['test-data-*'], + query: '*:*', + new_terms_fields: ['host.name'], + history_window_start: 'now-1h', + from: 'now-35m', + interval: '30m', + enabled: true, + }) + ); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + + it('records matched_indices_count for multiple matching index patterns', async () => { + const timestamp = new Date().toISOString(); + const documents = [ + { + '@timestamp': timestamp, + host: { name: 'host-0' }, + }, + { + '@timestamp': timestamp, + host: { name: 'host-0' }, + }, + ]; + + await indexListOfDocuments(documents); + + const createdRule = await createRule( + supertest, + log, + getNewTermsRuleParams({ + index: ['test-da*', 'test-data-1', 'test-data-2'], + query: '*:*', + new_terms_fields: ['host.name'], + history_window_start: 'now-1h', + from: 'now-35m', + interval: '30m', + enabled: true, + }) + ); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + }); + describe('alerts_candidate_count', () => { it('records alerts_candidate_count value', async () => { const timestamp = new Date().toISOString(); @@ -77,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => { supertest, log, getNewTermsRuleParams({ - index: ['logs-1'], + index: ['test-data-1'], query: '*:*', new_terms_fields: ['host.name'], history_window_start: 'now-1h', @@ -120,7 +240,7 @@ export default ({ getService }: FtrProviderContext) => { supertest, log, getNewTermsRuleParams({ - index: ['logs-1'], + index: ['test-data-1'], query: '*:*', new_terms_fields: ['host.name'], history_window_start: 'now-1h', diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/query/trial_license_complete_tier/custom_query_metrics.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/query/trial_license_complete_tier/custom_query_metrics.ts index 2359744b75ba1..2b8c65a6f7624 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/query/trial_license_complete_tier/custom_query_metrics.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/query/trial_license_complete_tier/custom_query_metrics.ts @@ -6,6 +6,7 @@ */ import expect from 'expect'; +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import { createRule, deleteAllRules, deleteAllAlerts } from '@kbn/detections-response-ftr-services'; import { getCustomQueryRuleParams, @@ -21,7 +22,7 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const { indexListOfDocuments: indexListOfSourceDocuments } = dataGeneratorFactory({ es, - index: 'logs-1', + index: 'test-data-1', log, }); @@ -31,29 +32,127 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllRules(supertest, log); await es.indices.delete({ - index: 'logs-1', + index: 'test-data-1,test-data-2', ignore_unavailable: true, }); - await es.indices.create({ - index: 'logs-1', - mappings: { - properties: { - '@timestamp': { - type: 'date', - }, - host: { - properties: { - name: { - type: 'keyword', - }, + + const mappings: MappingTypeMapping = { + properties: { + '@timestamp': { + type: 'date', + }, + host: { + properties: { + name: { + type: 'keyword', }, }, }, }, + }; + await es.indices.create({ + index: 'test-data-1', + mappings, + }); + await es.indices.create({ + index: 'test-data-2', + mappings, }); }); describe('metrics collection', () => { + describe('matched_indices_count', () => { + it('records matched_indices_count for one matching index pattern', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getCustomQueryRuleParams({ + index: ['test-data-1'], + query: '*:*', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenAlerts(supertest, log, es, createdRule); + + expect(alerts.hits.hits).toHaveLength(1); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(1); + }); + + it('records matched_indices_count for a single index pattern with wildcard', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getCustomQueryRuleParams({ + index: ['test-data-*'], + query: '*:*', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenAlerts(supertest, log, es, createdRule); + + expect(alerts.hits.hits).toHaveLength(1); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + + it('records matched_indices_count for multiple matching index patterns', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + const rule = getCustomQueryRuleParams({ + index: ['test-da*', 'test-data-1', 'test-data-2'], + query: '*:*', + from: 'now-35m', + interval: '30m', + enabled: true, + }); + + await indexListOfSourceDocuments([document]); + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenAlerts(supertest, log, es, createdRule); + + expect(alerts.hits.hits).toHaveLength(1); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + }); + describe('alerts_candidate_count', () => { it('records alerts_candidate_count value', async () => { const timestamp = new Date().toISOString(); @@ -62,7 +161,7 @@ export default ({ getService }: FtrProviderContext) => { host: { name: 'test-1' }, }; const rule = getCustomQueryRuleParams({ - index: ['logs-1'], + index: ['test-data-1'], query: '*:*', from: 'now-35m', interval: '30m', @@ -89,7 +188,7 @@ export default ({ getService }: FtrProviderContext) => { host: { name: 'test-1' }, }; const rule = getCustomQueryRuleParams({ - index: ['logs-1'], + index: ['test-data-1'], query: '*:*', alert_suppression: { group_by: ['host.name'], diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/threshold/trial_license_complete_tier/threshold_metrics.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/threshold/trial_license_complete_tier/threshold_metrics.ts index a774ec1d993d0..864ba56d9a640 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/threshold/trial_license_complete_tier/threshold_metrics.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/threshold/trial_license_complete_tier/threshold_metrics.ts @@ -6,6 +6,7 @@ */ import expect from 'expect'; +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import { createRule, deleteAllRules, @@ -27,7 +28,7 @@ export default ({ getService }: FtrProviderContext) => { const detectionsApi = getService('detectionsApi'); const { indexListOfDocuments: indexListOfSourceDocuments } = dataGeneratorFactory({ es, - index: 'logs-1', + index: 'test-data-1', log, }); @@ -37,29 +38,139 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllRules(supertest, log); await es.indices.delete({ - index: 'logs-1', + index: 'test-data-1,test-data-2', ignore_unavailable: true, }); - await es.indices.create({ - index: 'logs-1', - mappings: { - properties: { - '@timestamp': { - type: 'date', - }, - host: { - properties: { - name: { - type: 'keyword', - }, + + const mappings: MappingTypeMapping = { + properties: { + '@timestamp': { + type: 'date', + }, + host: { + properties: { + name: { + type: 'keyword', }, }, }, }, + }; + await es.indices.create({ + index: 'test-data-1', + mappings, + }); + await es.indices.create({ + index: 'test-data-2', + mappings, }); }); describe('metrics collection', () => { + describe('matched_indices_count', () => { + it('records matched_indices_count for one matching index pattern', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + + await indexListOfSourceDocuments([document, document]); + + const createdRule = await createRule( + supertest, + log, + getThresholdRuleParams({ + index: ['test-data-1'], + query: '*:*', + threshold: { + field: ['host.name'], + value: 2, + }, + from: 'now-35m', + interval: '30m', + enabled: true, + }) + ); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(1); + }); + + it('records matched_indices_count for a single index pattern with wildcard', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + + await indexListOfSourceDocuments([document, document]); + + const createdRule = await createRule( + supertest, + log, + getThresholdRuleParams({ + index: ['test-data-*'], + query: '*:*', + threshold: { + field: ['host.name'], + value: 2, + }, + from: 'now-35m', + interval: '30m', + enabled: true, + }) + ); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + + it('records matched_indices_count for multiple matching index patterns', async () => { + const timestamp = new Date().toISOString(); + const document = { + '@timestamp': timestamp, + host: { name: 'test-1' }, + }; + + await indexListOfSourceDocuments([document, document]); + + const createdRule = await createRule( + supertest, + log, + getThresholdRuleParams({ + index: ['test-da*', 'test-data-1', 'test-data-2'], + query: '*:*', + threshold: { + field: ['host.name'], + value: 2, + }, + from: 'now-35m', + interval: '30m', + enabled: true, + }) + ); + + const { matched_indices_count } = await getLatestSecurityRuleExecutionMetricsFromEventLog( + es, + log, + createdRule.id + ); + + expect(matched_indices_count).toBe(2); + }); + }); + describe('alerts_candidate_count', () => { it('records alerts_candidate_count value', async () => { const timestamp = new Date().toISOString(); @@ -74,7 +185,7 @@ export default ({ getService }: FtrProviderContext) => { supertest, log, getThresholdRuleParams({ - index: ['logs-1'], + index: ['test-data-1'], query: '*:*', threshold: { field: ['host.name'], @@ -106,7 +217,7 @@ export default ({ getService }: FtrProviderContext) => { supertest, log, getThresholdRuleParams({ - index: ['logs-1'], + index: ['test-data-1', 'test-data-2'], query: '*:*', threshold: { field: ['host.name'],