diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 14e12b2ea463..747b0bed3a04 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -15,6 +15,9 @@ import { getListArrayMock } from '../../../../common/detection_engine/schemas/ty import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; +// @ts-expect-error +moment.suppressDeprecationWarnings = true; + import { generateId, parseInterval, @@ -32,6 +35,7 @@ import { createSearchAfterReturnType, mergeReturns, createTotalHitsFromSearchResult, + lastValidDate, } from './utils'; import { BulkResponseErrorAggregation, SearchAfterAndBulkCreateReturnType } from './types'; import { @@ -45,6 +49,7 @@ import { sampleEmptyDocSearchResults, sampleDocSearchResultsNoSortIdNoHits, repeatedSearchResultsWithSortId, + sampleDocSearchResultsNoSortId, } from './__mocks__/es_results'; import { ShardError } from '../../types'; @@ -967,6 +972,57 @@ describe('utils', () => { const { success } = createSearchAfterReturnTypeFromResponse({ searchResult }); expect(success).toEqual(true); }); + + test('It will not set an invalid date time stamp from a non-existent @timestamp when the index is not 100% ECS compliant', () => { + const searchResult = sampleDocSearchResultsNoSortId(); + (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = undefined; + const { lastLookBackDate } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(lastLookBackDate).toEqual(null); + }); + + test('It will not set an invalid date time stamp from a null @timestamp when the index is not 100% ECS compliant', () => { + const searchResult = sampleDocSearchResultsNoSortId(); + (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = null; + const { lastLookBackDate } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(lastLookBackDate).toEqual(null); + }); + + test('It will not set an invalid date time stamp from an invalid @timestamp string', () => { + const searchResult = sampleDocSearchResultsNoSortId(); + (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = 'invalid'; + const { lastLookBackDate } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(lastLookBackDate).toEqual(null); + }); + }); + + describe('lastValidDate', () => { + test('It returns undefined if the search result contains a null timestamp', () => { + const searchResult = sampleDocSearchResultsNoSortId(); + (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = null; + const date = lastValidDate(searchResult); + expect(date).toEqual(undefined); + }); + + test('It returns undefined if the search result contains a undefined timestamp', () => { + const searchResult = sampleDocSearchResultsNoSortId(); + (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = undefined; + const date = lastValidDate(searchResult); + expect(date).toEqual(undefined); + }); + + test('It returns undefined if the search result contains an invalid string value', () => { + const searchResult = sampleDocSearchResultsNoSortId(); + (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = 'invalid value'; + const date = lastValidDate(searchResult); + expect(date).toEqual(undefined); + }); + + test('It returns correct date time stamp if the search result contains an invalid string value', () => { + const searchResult = sampleDocSearchResultsNoSortId(); + (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = 'invalid value'; + const date = lastValidDate(searchResult); + expect(date).toEqual(undefined); + }); }); describe('createSearchAfterReturnType', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 53089b7f1ca2..93e9622dcb6a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -514,6 +514,25 @@ export const createErrorsFromShard = ({ errors }: { errors: ShardError[] }): str }); }; +/** + * Given a SignalSearchResponse this will return a valid last date if it can find one, otherwise it + * will return undefined. + * @param result The result to try and parse out the timestamp. + */ +export const lastValidDate = (result: SignalSearchResponse): Date | undefined => { + if (result.hits.hits.length === 0) { + return undefined; + } else { + const lastTimestamp = result.hits.hits[result.hits.hits.length - 1]._source['@timestamp']; + const isValid = lastTimestamp != null && moment(lastTimestamp).isValid(); + if (!isValid) { + return undefined; + } else { + return new Date(lastTimestamp); + } + } +}; + export const createSearchAfterReturnTypeFromResponse = ({ searchResult, }: { @@ -521,10 +540,7 @@ export const createSearchAfterReturnTypeFromResponse = ({ }): SearchAfterAndBulkCreateReturnType => { return createSearchAfterReturnType({ success: searchResult._shards.failed === 0, - lastLookBackDate: - searchResult.hits.hits.length > 0 - ? new Date(searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp']) - : undefined, + lastLookBackDate: lastValidDate(searchResult), }); };