From d0683653477b9e0cca5c6677d82ec70ab321953e Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Sat, 31 Aug 2019 08:51:31 -0600 Subject: [PATCH] [SIEM] Adds a configuraton option for the default SIEM date time range (#44540) (#44558) ## Summary Adds a configuration for the SIEM date time picker and interval within advanced settings: ``` ${root}/app/kibana#/management/kibana/settings ``` Screen Shot 2019-08-30 at 1 24 29 PM ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [x] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../legacy/plugins/siem/common/constants.ts | 9 + x-pack/legacy/plugins/siem/index.ts | 44 +- .../siem/public/components/charts/common.tsx | 5 +- .../components/open_timeline/helpers.ts | 8 +- .../components/super_date_picker/index.tsx | 4 +- .../plugins/siem/public/mock/global_state.ts | 14 +- .../plugins/siem/public/mock/ui_settings.ts | 33 +- .../siem/public/store/inputs/reducer.ts | 35 +- .../utils/default_date_settings.test.ts | 546 ++++++++++++++++++ .../public/utils/default_date_settings.ts | 164 ++++++ 10 files changed, 829 insertions(+), 33 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 7e8854515bd38..1641ee983bee9 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -11,8 +11,17 @@ export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; export const DEFAULT_DARK_MODE = 'theme:darkMode'; export const DEFAULT_INDEX_KEY = 'siem:defaultIndex'; +export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; +export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; +export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; +export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore'; export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; export const DEFAULT_KBN_VERSION = 'kbnVersion'; export const DEFAULT_TIMEZONE_BROWSER = 'timezoneBrowser'; +export const DEFAULT_FROM = 'now-24h'; +export const DEFAULT_TO = 'now'; +export const DEFAULT_INTERVAL_PAUSE = true; +export const DEFAULT_INTERVAL_TYPE = 'manual'; +export const DEFAULT_INTERVAL_VALUE = 300000; // ms diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 255b0cf864c04..78fc0a52dc3d6 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -11,7 +11,18 @@ import { Server } from 'hapi'; import { initServerWithKibana } from './server/kibana.index'; import { savedObjectMappings } from './server/saved_objects'; -import { APP_ID, APP_NAME, DEFAULT_INDEX_KEY, DEFAULT_ANOMALY_SCORE } from './common/constants'; +import { + APP_ID, + APP_NAME, + DEFAULT_INDEX_KEY, + DEFAULT_ANOMALY_SCORE, + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_FROM, + DEFAULT_TO, +} from './common/constants'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function siem(kibana: any) { @@ -45,6 +56,37 @@ export function siem(kibana: any) { }, ], uiSettingDefaults: { + [DEFAULT_SIEM_REFRESH_INTERVAL]: { + type: 'json', + name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', { + defaultMessage: 'Time picker refresh interval', + }), + value: `{ + "pause": ${DEFAULT_INTERVAL_PAUSE}, + "value": ${DEFAULT_INTERVAL_VALUE} +}`, + description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', { + defaultMessage: "The SIEM timefilter's default refresh interval", + }), + category: ['siem'], + requiresPageReload: true, + }, + [DEFAULT_SIEM_TIME_RANGE]: { + type: 'json', + name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', { + defaultMessage: 'Time picker defaults', + }), + value: `{ + "from": "${DEFAULT_FROM}", + "to": "${DEFAULT_TO}" +}`, + description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', { + defaultMessage: + 'The SIEM timefilter selection to use when Kibana is started without one', + }), + category: ['siem'], + requiresPageReload: true, + }, [DEFAULT_INDEX_KEY]: { name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', { defaultMessage: 'Default index', diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx index 00b0f75078bfd..36793d6a0f6ae 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx @@ -23,6 +23,7 @@ import { import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; import moment from 'moment-timezone'; +import { DEFAULT_DATE_FORMAT_TZ, DEFAULT_DARK_MODE } from '../../../common/constants'; const chartHeight = 74; const chartDefaultRotation: Rotation = 0; @@ -138,10 +139,10 @@ export const getTheme = () => { barsPadding: 0.5, }, }; - const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); + const isDarkMode: boolean = chrome.getUiSettingsClient().get(DEFAULT_DARK_MODE); const defaultTheme = isDarkMode ? DARK_THEME : LIGHT_THEME; return mergeWithDefaultTheme(theme, defaultTheme); }; -const kibanaTimezone = chrome.getUiSettingsClient().get('dateFormat:tz'); +const kibanaTimezone: string = chrome.getUiSettingsClient().get(DEFAULT_DATE_FORMAT_TZ); export const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index c7817da04e38f..cc2197f040c26 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import dateMath from '@elastic/datemath'; import ApolloClient from 'apollo-client'; import { getOr, set } from 'lodash/fp'; import { ActionCreator } from 'typescript-fsa'; @@ -28,6 +27,7 @@ import { import { DEFAULT_DATE_COLUMN_MIN_WIDTH, DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types'; +import { getDefaultFromValue, getDefaultToValue } from '../../utils/default_date_settings'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -177,16 +177,14 @@ export const queryTimelineById = ({ ); const { timeline, notes } = formatTimelineResultToModel(timelineToOpen, duplicate); - - const momentDate = dateMath.parse('now-24h'); if (updateTimeline) { updateTimeline({ duplicate, - from: getOr(momentDate ? momentDate.valueOf() : 0, 'dateRange.start', timeline), + from: getOr(getDefaultFromValue(), 'dateRange.start', timeline), id: 'timeline-1', notes, timeline, - to: getOr(Date.now(), 'dateRange.end', timeline), + to: getOr(getDefaultToValue(), 'dateRange.end', timeline), })(); } }) diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx index 636b6acea54f4..d73b0268c4d8d 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx @@ -31,7 +31,7 @@ import { isLoadingSelector, refetchSelector, } from './selectors'; -import { InputsRange } from '../../store/inputs/model'; +import { InputsRange, Policy } from '../../store/inputs/model'; const MAX_RECENTLY_USED_RANGES = 9; @@ -53,7 +53,7 @@ const MyEuiSuperDatePicker: React.SFC = EuiSuperDateP interface SuperDatePickerStateRedux { duration: number; - policy: string; + policy: Policy['kind']; kind: string; fromStr: string; toStr: string; diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index f8bc5919bad4d..7a19c65ec9f34 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -19,6 +19,12 @@ import { import { State } from '../store'; import { defaultHeaders } from './header'; +import { + DEFAULT_FROM, + DEFAULT_TO, + DEFAULT_INTERVAL_TYPE, + DEFAULT_INTERVAL_VALUE, +} from '../../common/constants'; export const mockGlobalState: State = { app: { @@ -111,16 +117,16 @@ export const mockGlobalState: State = { }, inputs: { global: { - timerange: { kind: 'relative', fromStr: 'now-24h', toStr: 'now', from: 0, to: 1 }, + timerange: { kind: 'relative', fromStr: DEFAULT_FROM, toStr: DEFAULT_TO, from: 0, to: 1 }, linkTo: ['timeline'], query: [], - policy: { kind: 'manual', duration: 300000 }, + policy: { kind: DEFAULT_INTERVAL_TYPE, duration: DEFAULT_INTERVAL_VALUE }, }, timeline: { - timerange: { kind: 'relative', fromStr: 'now-24h', toStr: 'now', from: 0, to: 1 }, + timerange: { kind: 'relative', fromStr: DEFAULT_FROM, toStr: DEFAULT_TO, from: 0, to: 1 }, linkTo: ['global'], query: [], - policy: { kind: 'manual', duration: 300000 }, + policy: { kind: DEFAULT_INTERVAL_TYPE, duration: DEFAULT_INTERVAL_VALUE }, }, }, dragAndDrop: { dataProviders: {} }, diff --git a/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts b/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts index a0b42b5b161e7..4ee41f33fa463 100644 --- a/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts +++ b/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts @@ -5,18 +5,41 @@ */ import chrome from 'ui/chrome'; +import { + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INDEX_KEY, + DEFAULT_DATE_FORMAT_TZ, + DEFAULT_DARK_MODE, + DEFAULT_TIME_RANGE, + DEFAULT_REFRESH_RATE_INTERVAL, + DEFAULT_FROM, + DEFAULT_TO, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, +} from '../../common/constants'; chrome.getUiSettingsClient().get.mockImplementation((key: string) => { switch (key) { - case 'timepicker:timeDefaults': + case DEFAULT_TIME_RANGE: return { from: 'now-15m', to: 'now', mode: 'quick' }; - case 'timepicker:refreshIntervalDefaults': + case DEFAULT_REFRESH_RATE_INTERVAL: return { pause: false, value: 0 }; - case 'siem:defaultIndex': + case DEFAULT_SIEM_TIME_RANGE: + return { + from: DEFAULT_FROM, + to: DEFAULT_TO, + }; + case DEFAULT_SIEM_REFRESH_INTERVAL: + return { + pause: DEFAULT_INTERVAL_PAUSE, + value: DEFAULT_INTERVAL_VALUE, + }; + case DEFAULT_INDEX_KEY: return ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; - case 'dateFormat:tz': + case DEFAULT_DATE_FORMAT_TZ: return 'Asia/Taipei'; - case 'theme:darkMode': + case DEFAULT_DARK_MODE: return false; default: throw new Error(`Unexpected config key: ${key}`); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/reducer.ts b/x-pack/legacy/plugins/siem/public/store/inputs/reducer.ts index 7839f6794c079..32d869244f953 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/reducer.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import dateMath from '@elastic/datemath'; import { get } from 'lodash/fp'; import { reducerWithInitialState } from 'typescript-fsa-reducers'; @@ -35,37 +34,45 @@ import { addTimelineLink, } from './helpers'; import { InputsModel, TimeRange } from './model'; +import { + getDefaultFromValue, + getDefaultToValue, + getDefaultFromString, + getDefaultToString, + getDefaultIntervalKind, + getDefaultIntervalDuration, +} from '../../utils/default_date_settings'; export type InputsState = InputsModel; -const momentDate = dateMath.parse('now-24h'); + export const initialInputsState: InputsState = { global: { timerange: { kind: 'relative', - fromStr: 'now-24h', - toStr: 'now', - from: momentDate ? momentDate.valueOf() : 0, - to: Date.now(), + fromStr: getDefaultFromString(), + toStr: getDefaultToString(), + from: getDefaultFromValue(), + to: getDefaultToValue(), }, query: [], policy: { - kind: 'manual', - duration: 300000, + kind: getDefaultIntervalKind(), + duration: getDefaultIntervalDuration(), }, linkTo: ['timeline'], }, timeline: { timerange: { kind: 'relative', - fromStr: 'now-24h', - toStr: 'now', - from: momentDate ? momentDate.valueOf() : 0, - to: Date.now(), + fromStr: getDefaultFromString(), + toStr: getDefaultToString(), + from: getDefaultFromValue(), + to: getDefaultToValue(), }, query: [], policy: { - kind: 'manual', - duration: 300000, + kind: getDefaultIntervalKind(), + duration: getDefaultIntervalDuration(), }, linkTo: ['global'], }, diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts new file mode 100644 index 0000000000000..ce84da6ddf129 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts @@ -0,0 +1,546 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import chrome from 'ui/chrome'; +import { + getDefaultFromString, + DefaultTimeRangeSetting, + getDefaultToString, + getDefaultFromValue, + getDefaultToValue, + getDefaultFromMoment, + getDefaultToMoment, + DefaultIntervalSetting, + getDefaultIntervalKind, + getDefaultIntervalDuration, + parseDateString, + parseDateWithDefault, +} from './default_date_settings'; +import { + DEFAULT_FROM, + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_TO, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_INTERVAL_TYPE, +} from '../../common/constants'; +import { Policy } from '../store/inputs/model'; +import moment from 'moment'; + +// Change the constants to be static values so we can test against those instead of +// relative sliding date times. Jest cannot access these outer scoped variables so +// we have to repeat ourselves once +const DEFAULT_FROM_DATE = '1983-05-31T13:03:54.234Z'; +const DEFAULT_TO_DATE = '1990-05-31T13:03:54.234Z'; +jest.mock('../../common/constants', () => ({ + DEFAULT_FROM: '1983-05-31T13:03:54.234Z', + DEFAULT_TO: '1990-05-31T13:03:54.234Z', + DEFAULT_INTERVAL_PAUSE: true, + DEFAULT_INTERVAL_TYPE: 'manual', + DEFAULT_INTERVAL_VALUE: 300000, + DEFAULT_SIEM_REFRESH_INTERVAL: 'siem:refreshIntervalDefaults', + DEFAULT_SIEM_TIME_RANGE: 'siem:timeDefaults', +})); + +/** + * We utilize the internal chrome mocking that is built in to be able to mock different time range + * scenarios here or the absence of a time range setting. + * @param timeRange timeRange to use as a mock, including malformed data + * @param interval interval to use as a mock, including malformed data + */ +const mockTimeRange = ( + timeRange: DefaultTimeRangeSetting = { from: DEFAULT_FROM, to: DEFAULT_TO }, + interval: DefaultIntervalSetting = { + pause: DEFAULT_INTERVAL_PAUSE, + value: DEFAULT_INTERVAL_VALUE, + } +) => { + chrome.getUiSettingsClient().get.mockImplementation((key: string) => { + switch (key) { + case DEFAULT_SIEM_TIME_RANGE: + return timeRange; + case DEFAULT_SIEM_REFRESH_INTERVAL: + return interval; + default: + throw new Error(`Unexpected config key: ${key}`); + } + }); +}; + +/** + * Return that this unknown is only an object but we recognize in Typescript that we are ok + * with the object being malformed. + * @param timeRange Malformed object + */ +const isMalformedTimeRange = (timeRange: unknown): timeRange is DefaultTimeRangeSetting => + typeof timeRange === 'object'; + +/** + * Return that this unknown is only an object but we recognize in Typescript that we are ok + * with the object being malformed. + * @param interval Malformed object + */ +const isMalformedInterval = (interval: unknown): interval is DefaultIntervalSetting => + typeof interval === 'object'; + +describe('default_date_settings', () => { + beforeEach(() => { + chrome.getUiSettingsClient().get.mockClear(); + }); + + describe('#getDefaultFromString', () => { + test('should return the DEFAULT_FROM constant by default', () => { + mockTimeRange(); + const stringDefault = getDefaultFromString(); + expect(stringDefault).toBe(DEFAULT_FROM); + }); + + test('should return a custom from range', () => { + mockTimeRange({ from: 'now-15m' }); + const stringDefault = getDefaultFromString(); + expect(stringDefault).toBe('now-15m'); + }); + + test('should return the DEFAULT_FROM when the whole object is null', () => { + mockTimeRange(null); + const stringDefault = getDefaultFromString(); + expect(stringDefault).toBe(DEFAULT_FROM); + }); + + test('should return the DEFAULT_FROM when the whole object is undefined', () => { + mockTimeRange(null); + const stringDefault = getDefaultFromString(); + expect(stringDefault).toBe(DEFAULT_FROM); + }); + + test('should return the DEFAULT_FROM when the from value is null', () => { + mockTimeRange({ from: null }); + const stringDefault = getDefaultFromString(); + expect(stringDefault).toBe(DEFAULT_FROM); + }); + + test('should return the DEFAULT_FROM when the from value is undefined', () => { + mockTimeRange({ from: undefined }); + const stringDefault = getDefaultFromString(); + expect(stringDefault).toBe(DEFAULT_FROM); + }); + + test('should return the DEFAULT_FROM when the from value is malformed', () => { + const malformedTimeRange = { from: true }; + if (isMalformedTimeRange(malformedTimeRange)) { + mockTimeRange(malformedTimeRange); + const stringDefault = getDefaultFromString(); + expect(stringDefault).toBe(DEFAULT_FROM); + } else { + throw Error('Was expecting an object to be used for the malformed time range'); + } + }); + }); + + describe('#getDefaultToString', () => { + test('should return the DEFAULT_TO constant by default', () => { + mockTimeRange(); + const stringDefault = getDefaultToString(); + expect(stringDefault).toBe(DEFAULT_TO); + }); + + test('should return a custom from range', () => { + mockTimeRange({ to: 'now-15m' }); + const stringDefault = getDefaultToString(); + expect(stringDefault).toBe('now-15m'); + }); + + test('should return the DEFAULT_TO when the whole object is null', () => { + mockTimeRange(null); + const stringDefault = getDefaultToString(); + expect(stringDefault).toBe(DEFAULT_TO); + }); + + test('should return the DEFAULT_TO when the whole object is undefined', () => { + mockTimeRange(null); + const stringDefault = getDefaultToString(); + expect(stringDefault).toBe(DEFAULT_TO); + }); + + test('should return the DEFAULT_TO when the to value is null', () => { + mockTimeRange({ from: null }); + const stringDefault = getDefaultToString(); + expect(stringDefault).toBe(DEFAULT_TO); + }); + + test('should return the DEFAULT_TO when the from value is undefined', () => { + mockTimeRange({ to: undefined }); + const stringDefault = getDefaultToString(); + expect(stringDefault).toBe(DEFAULT_TO); + }); + + test('should return the DEFAULT_TO when the to value is malformed', () => { + const malformedTimeRange = { to: true }; + if (isMalformedTimeRange(malformedTimeRange)) { + mockTimeRange(malformedTimeRange); + const stringDefault = getDefaultToString(); + expect(stringDefault).toBe(DEFAULT_TO); + } else { + throw Error('Was expecting an object to be used for the malformed time range'); + } + }); + }); + + describe('#getDefaultFromValue', () => { + test('should return DEFAULT_FROM', () => { + mockTimeRange(); + const value = getDefaultFromValue(); + expect(value).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return a custom from range', () => { + const from = '2019-08-30T17:49:18.396Z'; + mockTimeRange({ from }); + const value = getDefaultFromValue(); + expect(value).toBe(new Date(from).valueOf()); + }); + + test('should return the DEFAULT_FROM when the whole object is null', () => { + mockTimeRange(null); + const stringDefault = getDefaultFromValue(); + expect(stringDefault).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return the DEFAULT_FROM when the whole object is undefined', () => { + mockTimeRange(null); + const stringDefault = getDefaultFromValue(); + expect(stringDefault).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return the DEFAULT_FROM when the from value is null', () => { + mockTimeRange({ from: null }); + const stringDefault = getDefaultFromValue(); + expect(stringDefault).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return the DEFAULT_FROM when the from value is undefined', () => { + mockTimeRange({ from: undefined }); + const stringDefault = getDefaultFromValue(); + expect(stringDefault).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return the DEFAULT_FROM when the from value is malformed', () => { + const malformedTimeRange = { from: true }; + if (isMalformedTimeRange(malformedTimeRange)) { + mockTimeRange(malformedTimeRange); + const stringDefault = getDefaultFromValue(); + expect(stringDefault).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + } else { + throw Error('Was expecting an object to be used for the malformed time range'); + } + }); + }); + + describe('#getDefaultToValue', () => { + test('should return DEFAULT_TO', () => { + mockTimeRange(); + const value = getDefaultToValue(); + expect(value).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + }); + + test('should return a custom from range', () => { + const to = '2000-08-30T17:49:18.396Z'; + mockTimeRange({ to }); + const value = getDefaultToValue(); + expect(value).toBe(new Date(to).valueOf()); + }); + + test('should return the DEFAULT_TO_DATE when the whole object is null', () => { + mockTimeRange(null); + const stringDefault = getDefaultToValue(); + expect(stringDefault).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + }); + + test('should return the DEFAULT_TO_DATE when the whole object is undefined', () => { + mockTimeRange(null); + const stringDefault = getDefaultToValue(); + expect(stringDefault).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + }); + + test('should return the DEFAULT_TO_DATE when the from value is null', () => { + mockTimeRange({ from: null }); + const stringDefault = getDefaultToValue(); + expect(stringDefault).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + }); + + test('should return the DEFAULT_TO_DATE when the from value is undefined', () => { + mockTimeRange({ from: undefined }); + const stringDefault = getDefaultToValue(); + expect(stringDefault).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + }); + + test('should return the DEFAULT_TO_DATE when the from value is malformed', () => { + const malformedTimeRange = { from: true }; + if (isMalformedTimeRange(malformedTimeRange)) { + mockTimeRange(malformedTimeRange); + const stringDefault = getDefaultToValue(); + expect(stringDefault).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + } else { + throw Error('Was expecting an object to be used for the malformed time range'); + } + }); + }); + + describe('#getDefaultFromMoment', () => { + test('should return DEFAULT_FROM', () => { + mockTimeRange(); + const value = getDefaultFromMoment(); + expect(value.valueOf()).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return a custom from range', () => { + const from = '2019-08-30T17:49:18.396Z'; + mockTimeRange({ from }); + const value = getDefaultFromMoment(); + expect(value.valueOf()).toBe(new Date(from).valueOf()); + }); + + test('should return the DEFAULT_FROM when the whole object is null', () => { + mockTimeRange(null); + const defaultMoment = getDefaultFromMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return the DEFAULT_FROM when the whole object is undefined', () => { + mockTimeRange(null); + const defaultMoment = getDefaultFromMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return the DEFAULT_FROM when the from value is null', () => { + mockTimeRange({ from: null }); + const defaultMoment = getDefaultFromMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return the DEFAULT_FROM when the from value is undefined', () => { + mockTimeRange({ from: undefined }); + const defaultMoment = getDefaultFromMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + }); + + test('should return the DEFAULT_FROM when the from value is malformed', () => { + const malformedTimeRange = { from: true }; + if (isMalformedTimeRange(malformedTimeRange)) { + mockTimeRange(malformedTimeRange); + const defaultMoment = getDefaultFromMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + } else { + throw Error('Was expecting an object to be used for the malformed time range'); + } + }); + }); + + describe('#getDefaultToMoment', () => { + test('should return DEFAULT_TO', () => { + mockTimeRange(); + const value = getDefaultToMoment(); + expect(value.valueOf()).toBe(new Date(DEFAULT_TO).valueOf()); + }); + + test('should return a custom range', () => { + const to = '2019-08-30T17:49:18.396Z'; + mockTimeRange({ to }); + const value = getDefaultToMoment(); + expect(value.valueOf()).toBe(new Date(to).valueOf()); + }); + + test('should return the DEFAULT_TO when the whole object is null', () => { + mockTimeRange(null); + const defaultMoment = getDefaultToMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_TO).valueOf()); + }); + + test('should return the DEFAULT_TO when the whole object is undefined', () => { + mockTimeRange(null); + const defaultMoment = getDefaultToMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_TO).valueOf()); + }); + + test('should return the DEFAULT_TO when the from value is null', () => { + mockTimeRange({ from: null }); + const defaultMoment = getDefaultToMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_TO).valueOf()); + }); + + test('should return the DEFAULT_TO when the from value is undefined', () => { + mockTimeRange({ from: undefined }); + const defaultMoment = getDefaultToMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_TO).valueOf()); + }); + + test('should return the DEFAULT_TO when the from value is malformed', () => { + const malformedTimeRange = { from: true }; + if (isMalformedTimeRange(malformedTimeRange)) { + mockTimeRange(malformedTimeRange); + const defaultMoment = getDefaultToMoment(); + expect(defaultMoment.valueOf()).toBe(new Date(DEFAULT_TO).valueOf()); + } else { + throw Error('Was expecting an object to be used for the malformed time range'); + } + }); + }); + + describe('#getDefaultIntervalKind', () => { + test('should return default', () => { + mockTimeRange(); + const value = getDefaultIntervalKind(); + expect(value).toBe(DEFAULT_INTERVAL_TYPE); + }); + + test('should return an interval when given non paused value', () => { + const interval: DefaultIntervalSetting = { pause: false }; + mockTimeRange(undefined, interval); + const value = getDefaultIntervalKind(); + const expected: Policy['kind'] = 'interval'; + expect(value).toBe(expected); + }); + + test('should return a manual when given a paused value', () => { + const interval: DefaultIntervalSetting = { pause: true }; + mockTimeRange(undefined, interval); + const value = getDefaultIntervalKind(); + const expected: Policy['kind'] = 'manual'; + expect(value).toBe(expected); + }); + + test('should return the default when the whole object is null', () => { + mockTimeRange(undefined, null); + const value = getDefaultIntervalKind(); + expect(value).toBe(DEFAULT_INTERVAL_TYPE); + }); + + test('should return the default when the whole object is undefined', () => { + mockTimeRange(undefined, undefined); + const value = getDefaultIntervalKind(); + expect(value).toBe(DEFAULT_INTERVAL_TYPE); + }); + + test('should return the default when the value is null', () => { + mockTimeRange(undefined, { pause: null }); + const value = getDefaultIntervalKind(); + expect(value).toBe(DEFAULT_INTERVAL_TYPE); + }); + + test('should return the default when the value is undefined', () => { + mockTimeRange(undefined, { pause: undefined }); + const value = getDefaultIntervalKind(); + expect(value).toBe(DEFAULT_INTERVAL_TYPE); + }); + + test('should return the default when the from value is malformed', () => { + const malformedInterval = { pause: 'whoops a string' }; + if (isMalformedInterval(malformedInterval)) { + mockTimeRange(undefined, malformedInterval); + const value = getDefaultIntervalKind(); + expect(value).toBe(DEFAULT_INTERVAL_TYPE); + } else { + throw Error('Was expecting an object to be used for the malformed interval'); + } + }); + }); + + describe('#getDefaultIntervalDuration', () => { + test('should return default', () => { + mockTimeRange(); + const value = getDefaultIntervalDuration(); + expect(value).toBe(DEFAULT_INTERVAL_VALUE); + }); + + test('should return a value when given a paused value', () => { + const interval: DefaultIntervalSetting = { value: 5 }; + mockTimeRange(undefined, interval); + const value = getDefaultIntervalDuration(); + const expected: Policy['duration'] = 5; + expect(value).toBe(expected); + }); + + test('should return the default when the whole object is null', () => { + mockTimeRange(undefined, null); + const value = getDefaultIntervalDuration(); + expect(value).toBe(DEFAULT_INTERVAL_VALUE); + }); + + test('should return the default when the whole object is undefined', () => { + mockTimeRange(undefined, undefined); + const value = getDefaultIntervalDuration(); + expect(value).toBe(DEFAULT_INTERVAL_VALUE); + }); + + test('should return the default when the value is null', () => { + mockTimeRange(undefined, { value: null }); + const value = getDefaultIntervalDuration(); + expect(value).toBe(DEFAULT_INTERVAL_VALUE); + }); + + test('should return the default when the value is undefined', () => { + mockTimeRange(undefined, { value: undefined }); + const value = getDefaultIntervalDuration(); + expect(value).toBe(DEFAULT_INTERVAL_VALUE); + }); + + test('should return the default when the value is malformed', () => { + const malformedInterval = { value: 'whoops a string' }; + if (isMalformedInterval(malformedInterval)) { + mockTimeRange(undefined, malformedInterval); + const value = getDefaultIntervalDuration(); + expect(value).toBe(DEFAULT_INTERVAL_VALUE); + } else { + throw Error('Was expecting an object to be used for the malformed interval'); + } + }); + }); + + describe('#parseDateString', () => { + test('should return the first value as a moment if everything is ok', () => { + const value = parseDateString( + '1930-05-31T13:03:54.234Z', + '1940-05-31T13:03:54.234Z', + moment('1950-05-31T13:03:54.234Z') + ); + expect(value.valueOf()).toBe(new Date('1930-05-31T13:03:54.234Z').valueOf()); + }); + + test('should return the second value as a moment if the first is null', () => { + const value = parseDateString( + null, + '1940-05-31T13:03:54.234Z', + moment('1950-05-31T13:03:54.234Z') + ); + expect(value.valueOf()).toBe(new Date('1940-05-31T13:03:54.234Z').valueOf()); + }); + + test('should return the second value as a moment if the first is undefined', () => { + const value = parseDateString( + null, + '1940-05-31T13:03:54.234Z', + moment('1950-05-31T13:03:54.234Z') + ); + expect(value.valueOf()).toBe(new Date('1940-05-31T13:03:54.234Z').valueOf()); + }); + }); + + describe('#parseDateWithDefault', () => { + test('should return the first value if it is ok', () => { + const value = parseDateWithDefault( + '1930-05-31T13:03:54.234Z', + moment('1950-05-31T13:03:54.234Z') + ); + expect(value.valueOf()).toBe(new Date('1930-05-31T13:03:54.234Z').valueOf()); + }); + + test('should return the second value if the first is a bad string', () => { + const value = parseDateWithDefault('trashed string', moment('1950-05-31T13:03:54.234Z')); + expect(value.valueOf()).toBe(new Date('1950-05-31T13:03:54.234Z').valueOf()); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts new file mode 100644 index 0000000000000..273988c167255 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import chrome from 'ui/chrome'; +import dateMath from '@elastic/datemath'; +import moment from 'moment'; +import { isString, isBoolean, isNumber } from 'lodash/fp'; +import { + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_FROM, + DEFAULT_TO, + DEFAULT_INTERVAL_TYPE, + DEFAULT_INTERVAL_VALUE, +} from '../../common/constants'; +import { Policy } from '../store/inputs/model'; + +interface DefaultTimeRange { + from?: string | null; + to?: string | null; +} + +interface DefaultInterval { + pause?: boolean | null; + value?: number | null; +} + +export type DefaultTimeRangeSetting = DefaultTimeRange | null | undefined; + +export type DefaultIntervalSetting = DefaultInterval | null | undefined; + +// Defaults for if everything fails including dateMath.parse(DEFAULT_FROM) or dateMath.parse(DEFAULT_TO) +// These should not really be hit unless we are in an extreme buggy state. +const DEFAULT_FROM_MOMENT = moment().subtract(24, 'hours'); +const DEFAULT_TO_MOMENT = moment(); + +/** + * Returns the default SIEM time range "from" string. This should be used only in + * non-ReactJS code. For ReactJS code, use the settings context hook instead + */ +export const getDefaultFromString = (): string => { + const defaultTimeRange: DefaultTimeRangeSetting = chrome + .getUiSettingsClient() + .get(DEFAULT_SIEM_TIME_RANGE); + if (defaultTimeRange != null && isString(defaultTimeRange.from)) { + return defaultTimeRange.from; + } else { + return DEFAULT_FROM; + } +}; + +/** + * Returns the default SIEM time range "to" string. This should be used only in + * non-ReactJS code. For ReactJS code, use the settings context hook instead + */ +export const getDefaultToString = (): string => { + const defaultTimeRange: DefaultTimeRangeSetting = chrome + .getUiSettingsClient() + .get(DEFAULT_SIEM_TIME_RANGE); + if (defaultTimeRange != null && isString(defaultTimeRange.to)) { + return defaultTimeRange.to; + } else { + return DEFAULT_TO; + } +}; + +/** + * Returns the default SIEM time range "from" Epoch. This should be used only in + * non-ReactJS code. For ReactJS code, use the settings context hook instead + */ +export const getDefaultFromValue = (): number => getDefaultFromMoment().valueOf(); + +/** + * Returns the default SIEM time range "from" moment. This should be used only in + * non-ReactJS code. For ReactJS code, use the settings context hook instead + */ +export const getDefaultFromMoment = (): moment.Moment => { + const defaultTimeRange: DefaultTimeRangeSetting = chrome + .getUiSettingsClient() + .get(DEFAULT_SIEM_TIME_RANGE); + if (defaultTimeRange != null && isString(defaultTimeRange.from)) { + return parseDateString(defaultTimeRange.from, DEFAULT_FROM, DEFAULT_FROM_MOMENT); + } else { + return parseDateWithDefault(DEFAULT_FROM, DEFAULT_FROM_MOMENT); + } +}; + +/** + * Returns the default SIEM time range "to" Epoch. This should be used only in + * non-ReactJS code. For ReactJS code, use the settings context hook instead + */ +export const getDefaultToValue = (): number => getDefaultToMoment().valueOf(); + +/** + * Returns the default SIEM time range "to" moment. This should be used only in + * non-ReactJS code. For ReactJS code, use the settings context hook instead + */ +export const getDefaultToMoment = (): moment.Moment => { + const defaultTimeRange: DefaultTimeRangeSetting = chrome + .getUiSettingsClient() + .get(DEFAULT_SIEM_TIME_RANGE); + if (defaultTimeRange != null && isString(defaultTimeRange.to)) { + return parseDateString(defaultTimeRange.to, DEFAULT_TO, DEFAULT_TO_MOMENT); + } else { + return parseDateWithDefault(DEFAULT_TO, DEFAULT_TO_MOMENT); + } +}; + +/** + * Returns the default SIEM interval "kind". This should be used only in + * non-ReactJS code. For ReactJS code, use the settings context hook instead + */ +export const getDefaultIntervalKind = (): Policy['kind'] => { + const defaultInterval: DefaultIntervalSetting = chrome + .getUiSettingsClient() + .get(DEFAULT_SIEM_REFRESH_INTERVAL); + if (defaultInterval != null && isBoolean(defaultInterval.pause)) { + return defaultInterval.pause ? 'manual' : 'interval'; + } else { + return DEFAULT_INTERVAL_TYPE; + } +}; + +/** + * Returns the default SIEM interval "duration". This should be used only in + * non-ReactJS code. For ReactJS code, use the settings context hook instead + */ +export const getDefaultIntervalDuration = (): Policy['duration'] => { + const defaultInterval: DefaultIntervalSetting = chrome + .getUiSettingsClient() + .get(DEFAULT_SIEM_REFRESH_INTERVAL); + if (defaultInterval != null && isNumber(defaultInterval.value)) { + return defaultInterval.value; + } else { + return DEFAULT_INTERVAL_VALUE; + } +}; + +export const parseDateString = ( + dateString: string | null | undefined, + defaultDateString: string, + defaultDate: moment.Moment +): moment.Moment => { + if (dateString != null) { + return parseDateWithDefault(dateString, defaultDate); + } else { + return parseDateWithDefault(defaultDateString, defaultDate); + } +}; + +export const parseDateWithDefault = ( + dateString: string, + defaultDate: moment.Moment +): moment.Moment => { + const date = dateMath.parse(dateString); + if (date != null && date.isValid()) { + return date; + } else { + return defaultDate; + } +};