= ({ createAnomalyDetectionJobDisabled }
@@ -69,7 +52,6 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }
/>
),
- createJob: getCreateJobLink(createAnomalyDetectionJobDisabled),
transforms: (
{
+const mockReq = (
+ searchResult = {},
+ securityEnabled = true,
+ userHasPermissions = true,
+ securityErrorMessage = null
+) => {
return {
server: {
newPlatform: {
@@ -37,12 +42,14 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions =
},
},
plugins: {
- xpack_main: {
+ monitoring: {
info: {
- isAvailable: () => true,
- feature: () => ({
- isEnabled: () => securityEnabled,
- }),
+ getSecurityFeature: () => {
+ return {
+ isAvailable: securityEnabled,
+ isEnabled: securityEnabled,
+ };
+ },
},
},
elasticsearch: {
@@ -61,6 +68,11 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions =
params &&
params.path === '/_security/user/_has_privileges'
) {
+ if (securityErrorMessage !== null) {
+ return Promise.reject({
+ message: securityErrorMessage,
+ });
+ }
return Promise.resolve({ has_all_requested: userHasPermissions });
}
if (type === 'transport.request' && params && params.path === '/_nodes') {
@@ -245,6 +257,34 @@ describe('getCollectionStatus', () => {
expect(result.kibana.detected.doesExist).to.be(true);
});
+ it('should work properly with an unknown security message', async () => {
+ const req = mockReq({ hits: { total: { value: 1 } } }, true, true, 'foobar');
+ const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ expect(result._meta.hasPermissions).to.be(false);
+ });
+
+ it('should work properly with a known security message', async () => {
+ const req = mockReq(
+ { hits: { total: { value: 1 } } },
+ true,
+ true,
+ 'no handler found for uri [/_security/user/_has_privileges] and method [POST]'
+ );
+ const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ expect(result.kibana.detected.doesExist).to.be(true);
+ });
+
+ it('should work properly with another known security message', async () => {
+ const req = mockReq(
+ { hits: { total: { value: 1 } } },
+ true,
+ true,
+ 'Invalid index name [_security]'
+ );
+ const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ expect(result.kibana.detected.doesExist).to.be(true);
+ });
+
it('should not work if the user does not have the necessary permissions', async () => {
const req = mockReq({ hits: { total: { value: 1 } } }, true, false);
const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js
index 607503673276b..81cdfd6ecd172 100644
--- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js
+++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js
@@ -233,6 +233,10 @@ function isBeatFromAPM(bucket) {
}
async function hasNecessaryPermissions(req) {
+ const securityFeature = req.server.plugins.monitoring.info.getSecurityFeature();
+ if (!securityFeature.isAvailable || !securityFeature.isEnabled) {
+ return true;
+ }
try {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data');
const response = await callWithRequest(req, 'transport.request', {
@@ -250,6 +254,9 @@ async function hasNecessaryPermissions(req) {
) {
return true;
}
+ if (err.message.includes('Invalid index name [_security]')) {
+ return true;
+ }
return false;
}
}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts
index 49da7dbf6d514..9b0cec99b1b38 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts
@@ -17,7 +17,7 @@ export const PRE_BUILT_MSG = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage',
{
defaultMessage:
- 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.',
+ 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules except the Elastic Endpoint Security rule are disabled. You can select additional rules you want to activate.',
}
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx
index 35816e82540d1..0f16cb99862a5 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx
@@ -70,7 +70,7 @@ export const RiskScoreField = ({
{
field: newField?.name ?? '',
operator: 'equals',
- value: undefined,
+ value: '',
riskScore: undefined,
},
],
diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts
index 3636358ebe842..eeb1533f57a67 100644
--- a/x-pack/plugins/security_solution/public/management/common/routing.ts
+++ b/x-pack/plugins/security_solution/public/management/common/routing.ts
@@ -54,7 +54,8 @@ export const getHostListPath = (
};
export const getHostDetailsPath = (
- props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostDetailsUrlProps,
+ props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostIndexUIQueryParams &
+ HostDetailsUrlProps,
search?: string
) => {
const { name, ...queryParams } = props;
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index 58442ab417b60..f91bba3e3125a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -263,6 +263,7 @@ export const HostList = () => {
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
const toRoutePath = getHostDetailsPath({
name: 'hostPolicyResponse',
+ ...queryParams,
selected_host: item.metadata.host.id,
});
const toRouteUrl = formatUrl(toRoutePath);
diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
index 7a61427c56a3b..2a2354921a3d4 100644
--- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
@@ -251,6 +251,20 @@ export class Simulator {
return this.findInDOM('[data-test-subj="resolver:graph"]');
}
+ /**
+ * The outer panel container.
+ */
+ public panelElement(): ReactWrapper {
+ return this.findInDOM('[data-test-subj="resolver:panel"]');
+ }
+
+ /**
+ * The panel content element (which may include tables, lists, other data depending on the view).
+ */
+ public panelContentElement(): ReactWrapper {
+ return this.findInDOM('[data-test-subj^="resolver:panel:"]');
+ }
+
/**
* Like `this.wrapper.find` but only returns DOM nodes.
*/
diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
index 9cb900736677e..f339d128944cc 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
@@ -63,6 +63,16 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', (
expect(simulator.processNodeElements().length).toBe(3);
});
+ it(`should have the default "process list" panel present`, async () => {
+ expect(simulator.panelElement().length).toBe(1);
+ expect(simulator.panelContentElement().length).toBe(1);
+ const testSubjectName = simulator
+ .panelContentElement()
+ .getDOMNode()
+ .getAttribute('data-test-subj');
+ expect(testSubjectName).toMatch(/process-list/g);
+ });
+
describe("when the second child node's first button has been clicked", () => {
beforeEach(() => {
// Click the first button under the second child element.
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
index 83d3930065da6..f378ab36bac94 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
@@ -220,7 +220,7 @@ PanelContent.displayName = 'PanelContent';
export const Panel = memo(function Event({ className }: { className?: string }) {
return (
-
+
);
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx
index efb96cde431e5..8ca002ace26fe 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx
@@ -187,7 +187,12 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
{showWarning && }
- items={processTableView} columns={columns} sorting />
+
+ data-test-subj="resolver:panel:process-list"
+ items={processTableView}
+ columns={columns}
+ sorting
+ />
>
);
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx
index 6c1c88f511edb..75b6413bf08f9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx
@@ -17,11 +17,25 @@ import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page';
import { NotePreviews } from './note_previews';
import { OPEN_TIMELINE_CLASS_NAME } from './helpers';
-import { TimelineTabsStyle } from './types';
import { StatefulOpenTimeline } from '.';
+
import { useGetAllTimeline, getAllTimeline } from '../../containers/all';
+
+import { useParams } from 'react-router-dom';
+import { TimelineType } from '../../../../common/types/timeline';
+
jest.mock('../../../common/lib/kibana');
+jest.mock('../../../common/components/link_to');
+
+jest.mock('./helpers', () => {
+ const originalModule = jest.requireActual('./helpers');
+ return {
+ ...originalModule,
+ queryTimelineById: jest.fn(),
+ };
+});
+
jest.mock('../../containers/all', () => {
const originalModule = jest.requireActual('../../containers/all');
return {
@@ -30,19 +44,21 @@ jest.mock('../../containers/all', () => {
getAllTimeline: originalModule.getAllTimeline,
};
});
-jest.mock('./use_timeline_types', () => {
+
+jest.mock('react-router-dom', () => {
+ const originalModule = jest.requireActual('react-router-dom');
+
return {
- useTimelineTypes: jest.fn().mockReturnValue({
- timelineType: 'default',
- timelineTabs: ,
- timelineFilters: ,
- }),
+ ...originalModule,
+ useParams: jest.fn(),
+ useHistory: jest.fn().mockReturnValue([]),
};
});
describe('StatefulOpenTimeline', () => {
const title = 'All Timelines / Open Timelines';
beforeEach(() => {
+ (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default });
((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({
fetchAllTimeline: jest.fn(),
timelines: getAllTimeline(
@@ -433,10 +449,7 @@ describe('StatefulOpenTimeline', () => {
});
});
- /**
- * enable this test when createtTemplateTimeline is ready
- */
- test.skip('it renders the tabs', async () => {
+ test('it has the expected initial state for openTimeline - templateTimelineFilter', () => {
const wrapper = mount(
@@ -451,11 +464,27 @@ describe('StatefulOpenTimeline', () => {
);
- await waitFor(() => {
- expect(
- wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists()
- ).toEqual(true);
- });
+ expect(wrapper.find('[data-test-subj="open-timeline-subtabs"]').exists()).toEqual(true);
+ });
+
+ test('it has the expected initial state for openTimelineModalBody - templateTimelineFilter', () => {
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="open-timeline-modal-body-filters"]').exists()).toEqual(
+ true
+ );
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
index 7d54bb2209850..55afe845cdfb3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
@@ -7,26 +7,31 @@ import React, { useState, useCallback, useMemo } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui';
+import { noop } from 'lodash/fp';
import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline';
import { SecurityPageName } from '../../../app/types';
import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/link_to';
import * as i18n from './translations';
import { TimelineTabsStyle, TimelineTab } from './types';
-export const useTimelineTypes = ({
- defaultTimelineCount,
- templateTimelineCount,
-}: {
+export interface UseTimelineTypesArgs {
defaultTimelineCount?: number | null;
templateTimelineCount?: number | null;
-}): {
+}
+
+export interface UseTimelineTypesResult {
timelineType: TimelineTypeLiteralWithNull;
timelineTabs: JSX.Element;
timelineFilters: JSX.Element[];
-} => {
+}
+
+export const useTimelineTypes = ({
+ defaultTimelineCount,
+ templateTimelineCount,
+}: UseTimelineTypesArgs): UseTimelineTypesResult => {
const history = useHistory();
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines);
- const { tabName } = useParams<{ pageName: string; tabName: string }>();
+ const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>();
const [timelineType, setTimelineTypes] = useState(
tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null
);
@@ -61,7 +66,7 @@ export const useTimelineTypes = ({
timelineTabsStyle === TimelineTabsStyle.filter
? defaultTimelineCount ?? undefined
: undefined,
- onClick: goToTimeline,
+ onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop,
},
{
id: TimelineType.template,
@@ -76,7 +81,7 @@ export const useTimelineTypes = ({
timelineTabsStyle === TimelineTabsStyle.filter
? templateTimelineCount ?? undefined
: undefined,
- onClick: goToTemplateTimeline,
+ onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop,
},
],
[
@@ -106,7 +111,7 @@ export const useTimelineTypes = ({
const timelineTabs = useMemo(() => {
return (
<>
-
+
{getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => (
{
return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => (
{
expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy();
});
});
+
+ describe('getPinOnClick', () => {
+ const eventId = 'abcd';
+
+ test('it invokes `onPinEvent` with the expected eventId when the event is NOT pinned, and allowUnpinning is true', () => {
+ const isEventPinned = false; // the event is NOT pinned
+ const allowUnpinning = true;
+ const onPinEvent = jest.fn();
+
+ getPinOnClick({
+ allowUnpinning,
+ eventId,
+ onPinEvent,
+ onUnPinEvent: jest.fn(),
+ isEventPinned,
+ });
+
+ expect(onPinEvent).toBeCalledWith(eventId);
+ });
+
+ test('it does NOT invoke `onPinEvent` when the event is NOT pinned, and allowUnpinning is false', () => {
+ const isEventPinned = false; // the event is NOT pinned
+ const allowUnpinning = false;
+ const onPinEvent = jest.fn();
+
+ getPinOnClick({
+ allowUnpinning,
+ eventId,
+ onPinEvent,
+ onUnPinEvent: jest.fn(),
+ isEventPinned,
+ });
+
+ expect(onPinEvent).not.toBeCalled();
+ });
+
+ test('it invokes `onUnPinEvent` with the expected eventId when the event is pinned, and allowUnpinning is true', () => {
+ const isEventPinned = true; // the event is pinned
+ const allowUnpinning = true;
+ const onUnPinEvent = jest.fn();
+
+ getPinOnClick({
+ allowUnpinning,
+ eventId,
+ onPinEvent: jest.fn(),
+ onUnPinEvent,
+ isEventPinned,
+ });
+
+ expect(onUnPinEvent).toBeCalledWith(eventId);
+ });
+
+ test('it does NOT invoke `onUnPinEvent` when the event is pinned, and allowUnpinning is false', () => {
+ const isEventPinned = true; // the event is pinned
+ const allowUnpinning = false;
+ const onUnPinEvent = jest.fn();
+
+ getPinOnClick({
+ allowUnpinning,
+ eventId,
+ onPinEvent: jest.fn(),
+ onUnPinEvent,
+ isEventPinned,
+ });
+
+ expect(onUnPinEvent).not.toBeCalled();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
index 6a5e25632c29b..73b5a58ef7b65 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
@@ -3,7 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { get, isEmpty, noop } from 'lodash/fp';
+
+import { get, isEmpty } from 'lodash/fp';
import { Dispatch } from 'redux';
import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types';
@@ -65,11 +66,16 @@ export const getPinOnClick = ({
onPinEvent,
onUnPinEvent,
isEventPinned,
-}: GetPinOnClickParams): (() => void) => {
+}: GetPinOnClickParams) => {
if (!allowUnpinning) {
- return noop;
+ return;
+ }
+
+ if (isEventPinned) {
+ onUnPinEvent(eventId);
+ } else {
+ onPinEvent(eventId);
}
- return isEventPinned ? () => onUnPinEvent(eventId) : () => onPinEvent(eventId);
};
/**
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx
index 657976e2f4787..2ca27ded86c9d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx
@@ -4,7 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getPinIcon } from './';
+import { mount } from 'enzyme';
+import React from 'react';
+
+import { TimelineType } from '../../../../../common/types/timeline';
+
+import { getPinIcon, Pin } from './';
+
+interface ButtonIcon {
+ isDisabled: boolean;
+}
describe('pin', () => {
describe('getPinRotation', () => {
@@ -16,4 +25,62 @@ describe('pin', () => {
expect(getPinIcon(false)).toEqual('pin');
});
});
+
+ describe('disabled button behavior', () => {
+ test('the button is enabled when allowUnpinning is true, and timelineType is NOT `template` (the default)', () => {
+ const allowUnpinning = true;
+ const wrapper = mount(
+
+ );
+
+ expect(
+ (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled
+ ).toBe(false);
+ });
+
+ test('the button is disabled when allowUnpinning is false, and timelineType is NOT `template` (the default)', () => {
+ const allowUnpinning = false;
+ const wrapper = mount(
+
+ );
+
+ expect(
+ (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled
+ ).toBe(true);
+ });
+
+ test('the button is disabled when allowUnpinning is true, and timelineType is `template`', () => {
+ const allowUnpinning = true;
+ const timelineType = TimelineType.template;
+ const wrapper = mount(
+
+ );
+
+ expect(
+ (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled
+ ).toBe(true);
+ });
+
+ test('the button is disabled when allowUnpinning is false, and timelineType is `template`', () => {
+ const allowUnpinning = false;
+ const timelineType = TimelineType.template;
+ const wrapper = mount(
+
+ );
+
+ expect(
+ (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled
+ ).toBe(true);
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx
index 30fe8ae0ca1f6..27780c7754d00 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx
@@ -34,7 +34,7 @@ export const Pin = React.memo(
iconSize={iconSize}
iconType={getPinIcon(pinned)}
onClick={onClick}
- isDisabled={isTemplate}
+ isDisabled={isTemplate || !allowUnpinning}
/>
);
}
diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
index b59f9e90f8e74..c22acf6ba7cc1 100644
--- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
@@ -33,7 +33,7 @@ const TimelinesContainer = styled.div`
export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
export const TimelinesPageComponent: React.FC = () => {
- const { tabName } = useParams();
+ const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>();
const [importDataModalToggle, setImportDataModalToggle] = useState(false);
const onImportTimelineBtnClick = useCallback(() => {
setImportDataModalToggle(true);
diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts
new file mode 100644
index 0000000000000..1735c6473bb3a
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { filterIndexes } from './resolvers';
+
+describe('resolvers', () => {
+ test('it should filter single index that has an empty string', () => {
+ const emptyArray = filterIndexes(['']);
+ expect(emptyArray).toEqual([]);
+ });
+
+ test('it should filter single index that has blanks within it', () => {
+ const emptyArray = filterIndexes([' ']);
+ expect(emptyArray).toEqual([]);
+ });
+
+ test('it should filter indexes that has an empty string and a valid index', () => {
+ const emptyArray = filterIndexes(['', 'valid-index']);
+ expect(emptyArray).toEqual(['valid-index']);
+ });
+
+ test('it should filter indexes that have blanks within them and a valid index', () => {
+ const emptyArray = filterIndexes([' ', 'valid-index']);
+ expect(emptyArray).toEqual(['valid-index']);
+ });
+
+ test('it should filter single index that has _all within it', () => {
+ const emptyArray = filterIndexes(['_all']);
+ expect(emptyArray).toEqual([]);
+ });
+
+ test('it should filter single index that has _all within it surrounded by spaces', () => {
+ const emptyArray = filterIndexes([' _all ']);
+ expect(emptyArray).toEqual([]);
+ });
+
+ test('it should filter indexes that _all within them and a valid index', () => {
+ const emptyArray = filterIndexes(['_all', 'valid-index']);
+ expect(emptyArray).toEqual(['valid-index']);
+ });
+
+ test('it should filter indexes that _all surrounded with spaces within them and a valid index', () => {
+ const emptyArray = filterIndexes([' _all ', 'valid-index']);
+ expect(emptyArray).toEqual(['valid-index']);
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts
index 8d55e645d6791..84320b1699531 100644
--- a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts
+++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts
@@ -32,27 +32,34 @@ export const createSourceStatusResolvers = (libs: {
};
} => ({
SourceStatus: {
- async indicesExist(source, args, { req }) {
- if (
- args.defaultIndex.length === 1 &&
- (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all')
- ) {
+ async indicesExist(_, args, { req }) {
+ const indexes = filterIndexes(args.defaultIndex);
+ if (indexes.length !== 0) {
+ return libs.sourceStatus.hasIndices(req, indexes);
+ } else {
return false;
}
- return libs.sourceStatus.hasIndices(req, args.defaultIndex);
},
- async indexFields(source, args, { req }) {
- if (
- args.defaultIndex.length === 1 &&
- (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all')
- ) {
+ async indexFields(_, args, { req }) {
+ const indexes = filterIndexes(args.defaultIndex);
+ if (indexes.length !== 0) {
+ return libs.fields.getFields(req, indexes);
+ } else {
return [];
}
- return libs.fields.getFields(req, args.defaultIndex);
},
},
});
+/**
+ * Given a set of indexes this will remove anything that is:
+ * - blank or empty strings are removed as not valid indexes
+ * - _all is removed as that is not a valid index
+ * @param indexes Indexes with invalid values removed
+ */
+export const filterIndexes = (indexes: string[]): string[] =>
+ indexes.filter((index) => index.trim() !== '' && index.trim() !== '_all');
+
export const toIFieldSubTypeNonNullableScalar = new GraphQLScalarType({
name: 'IFieldSubType',
description: 'Represents value in index pattern field item',
diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts
index 944fc588afc8a..bb0a4b9e2ba9b 100644
--- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts
+++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts
@@ -17,26 +17,21 @@ import {
import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { FieldsAdapter, IndexFieldDescriptor } from './types';
-type IndexesAliasIndices = Record;
-
export class ElasticsearchIndexFieldAdapter implements FieldsAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise {
const indexPatternsService = this.framework.getIndexPatternsService(request);
- const indexesAliasIndices: IndexesAliasIndices = indices.reduce(
- (accumulator: IndexesAliasIndices, indice: string) => {
- const key = getIndexAlias(indices, indice);
+ const indexesAliasIndices = indices.reduce>((accumulator, indice) => {
+ const key = getIndexAlias(indices, indice);
- if (get(key, accumulator)) {
- accumulator[key] = [...accumulator[key], indice];
- } else {
- accumulator[key] = [indice];
- }
- return accumulator;
- },
- {} as IndexesAliasIndices
- );
+ if (get(key, accumulator)) {
+ accumulator[key] = [...accumulator[key], indice];
+ } else {
+ accumulator[key] = [indice];
+ }
+ return accumulator;
+ }, {});
const responsesIndexFields: IndexFieldDescriptor[][] = await Promise.all(
Object.values(indexesAliasIndices).map((indicesByGroup) =>
indexPatternsService.getFieldsForWildcard({
diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts
index 56ceca2b70e9c..5f002aa7fad7b 100644
--- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts
+++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts
@@ -401,10 +401,17 @@ describe('Schema Beat', () => {
const result = getIndexAlias([leadingWildcardIndex], leadingWildcardIndex);
expect(result).toBe(leadingWildcardIndex);
});
+
test('getIndexAlias no match returns "unknown" string', () => {
const index = 'auditbeat-*';
const result = getIndexAlias([index], 'hello');
expect(result).toBe('unknown');
});
+
+ test('empty index should not cause an error to return although it will cause an invalid regular expression to occur', () => {
+ const index = '';
+ const result = getIndexAlias([index], 'hello');
+ expect(result).toBe('unknown');
+ });
});
});
diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts
index ff7331cf39bc7..6ec15d328714d 100644
--- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts
+++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts
@@ -77,10 +77,16 @@ const convertFieldsToAssociativeArray = (
: {};
export const getIndexAlias = (defaultIndex: string[], indexName: string): string => {
- const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null);
- if (found != null) {
- return found;
- } else {
+ try {
+ const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null);
+ if (found != null) {
+ return found;
+ } else {
+ return 'unknown';
+ }
+ } catch (error) {
+ // if we encounter an error because the index contains invalid regular expressions then we should return an unknown
+ // rather than blow up with a toaster error upstream
return 'unknown';
}
};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index c81aade2b063e..e2f59f3fa910a 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -11084,7 +11084,6 @@
"xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "編集",
"xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "Kibanaインデックスパターンの作成中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。",
- "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "JSONエディターからこのフォームには戻れません。",
"xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "データフレーム分析ジョブの作成中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "既存のデータフレーム分析ジョブIDの取得中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。",
@@ -12362,9 +12361,7 @@
"xpack.ml.overview.feedbackSectionLink": "オンラインでのフィードバック",
"xpack.ml.overview.feedbackSectionText": "ご利用に際し、ご意見やご提案がありましたら、{feedbackLink}までお送りください。",
"xpack.ml.overview.feedbackSectionTitle": "フィードバック",
- "xpack.ml.overview.gettingStartedSectionCreateJob": "新規ジョブを作成中",
"xpack.ml.overview.gettingStartedSectionDocs": "ドキュメンテーション",
- "xpack.ml.overview.gettingStartedSectionText": "機械学習へようこそ。はじめに{docs}や{createJob}をご参照ください。{transforms}を使用して、分析ジョブの機能インデックスを作成することをお勧めします。",
"xpack.ml.overview.gettingStartedSectionTitle": "はじめて使う",
"xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearchの変換",
"xpack.ml.overview.overviewLabel": "概要",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index aba5adf72c2f8..316d3247d19d5 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -11086,7 +11086,6 @@
"xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "编辑",
"xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误:",
"xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。",
- "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "您不能从 json 编辑器切回到此表单。",
"xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "创建数据帧分析作业时发生错误:",
"xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "获取现有数据帧分析作业 ID 时发生错误:",
"xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:",
@@ -12365,9 +12364,7 @@
"xpack.ml.overview.feedbackSectionLink": "在线反馈",
"xpack.ml.overview.feedbackSectionText": "如果您在体验方面有任何意见或建议,请提交{feedbackLink}。",
"xpack.ml.overview.feedbackSectionTitle": "反馈",
- "xpack.ml.overview.gettingStartedSectionCreateJob": "创建新作业",
"xpack.ml.overview.gettingStartedSectionDocs": "文档",
- "xpack.ml.overview.gettingStartedSectionText": "欢迎使用 Machine Learning。首先阅读我们的{docs}或{createJob}。建议使用 {transforms} 为分析作业创建功能索引。",
"xpack.ml.overview.gettingStartedSectionTitle": "入门",
"xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearch 的转换",
"xpack.ml.overview.overviewLabel": "概览",
diff --git a/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts
new file mode 100644
index 0000000000000..873cdc5d71baa
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations';
+import { Annotation } from '../../../../../plugins/ml/common/types/annotations';
+
+export const commonJobConfig = {
+ description: 'test_job_annotation',
+ groups: ['farequote', 'automated', 'single-metric'],
+ analysis_config: {
+ bucket_span: '15m',
+ influencers: [],
+ detectors: [
+ {
+ function: 'mean',
+ field_name: 'responsetime',
+ },
+ {
+ function: 'min',
+ field_name: 'responsetime',
+ },
+ ],
+ },
+ data_description: { time_field: '@timestamp' },
+ analysis_limits: { model_memory_limit: '10mb' },
+};
+
+export const createJobConfig = (jobId: string) => {
+ return { ...commonJobConfig, job_id: jobId };
+};
+
+export const testSetupJobConfigs = [1, 2, 3, 4].map((num) => ({
+ ...commonJobConfig,
+ job_id: `job_annotation_${num}_${Date.now()}`,
+ description: `Test annotation ${num}`,
+}));
+export const jobIds = testSetupJobConfigs.map((j) => j.job_id);
+
+export const createAnnotationRequestBody = (jobId: string): Partial => {
+ return {
+ timestamp: Date.now(),
+ end_timestamp: Date.now(),
+ annotation: 'Test annotation',
+ job_id: jobId,
+ type: ANNOTATION_TYPE.ANNOTATION,
+ event: 'user',
+ detector_index: 1,
+ partition_field_name: 'airline',
+ partition_field_value: 'AAL',
+ };
+};
+
+export const testSetupAnnotations = testSetupJobConfigs.map((job) =>
+ createAnnotationRequestBody(job.job_id)
+);
diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts
new file mode 100644
index 0000000000000..14ecf1bfe524e
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common';
+import { USER } from '../../../../functional/services/ml/security_common';
+import { Annotation } from '../../../../../plugins/ml/common/types/annotations';
+import { createJobConfig, createAnnotationRequestBody } from './common_jobs';
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ const jobId = `job_annotation_${Date.now()}`;
+ const testJobConfig = createJobConfig(jobId);
+ const annotationRequestBody = createAnnotationRequestBody(jobId);
+
+ describe('create_annotations', function () {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/farequote');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ await ml.api.createAnomalyDetectionJob(testJobConfig);
+ });
+
+ after(async () => {
+ await ml.api.cleanMlIndices();
+ });
+
+ it('should successfully create annotations for anomaly job', async () => {
+ const { body } = await supertest
+ .put('/api/ml/annotations/index')
+ .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(annotationRequestBody)
+ .expect(200);
+ const annotationId = body._id;
+
+ const fetchedAnnotation = await ml.api.getAnnotationById(annotationId);
+
+ expect(fetchedAnnotation).to.not.be(undefined);
+
+ if (fetchedAnnotation) {
+ Object.keys(annotationRequestBody).forEach((key) => {
+ const field = key as keyof Annotation;
+ expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]);
+ });
+ }
+ expect(fetchedAnnotation?.create_username).to.eql(USER.ML_POWERUSER);
+ });
+
+ it('should successfully create annotation for user with ML read permissions', async () => {
+ const { body } = await supertest
+ .put('/api/ml/annotations/index')
+ .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(annotationRequestBody)
+ .expect(200);
+
+ const annotationId = body._id;
+ const fetchedAnnotation = await ml.api.getAnnotationById(annotationId);
+ expect(fetchedAnnotation).to.not.be(undefined);
+ if (fetchedAnnotation) {
+ Object.keys(annotationRequestBody).forEach((key) => {
+ const field = key as keyof Annotation;
+ expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]);
+ });
+ }
+ expect(fetchedAnnotation?.create_username).to.eql(USER.ML_VIEWER);
+ });
+
+ it('should not allow to create annotation for unauthorized user', async () => {
+ const { body } = await supertest
+ .put('/api/ml/annotations/index')
+ .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(annotationRequestBody)
+ .expect(404);
+
+ expect(body.error).to.eql('Not Found');
+ expect(body.message).to.eql('Not Found');
+ });
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts
new file mode 100644
index 0000000000000..4fbb26e9b5a3e
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common';
+import { USER } from '../../../../functional/services/ml/security_common';
+import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ describe('delete_annotations', function () {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/farequote');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+
+ // generate one annotation for each job
+ for (let i = 0; i < testSetupJobConfigs.length; i++) {
+ const job = testSetupJobConfigs[i];
+ const annotationToIndex = testSetupAnnotations[i];
+ await ml.api.createAnomalyDetectionJob(job);
+ await ml.api.indexAnnotation(annotationToIndex);
+ }
+ });
+
+ after(async () => {
+ await ml.api.cleanMlIndices();
+ });
+
+ it('should delete annotation by id', async () => {
+ const annotationsForJob = await ml.api.getAnnotations(jobIds[0]);
+ expect(annotationsForJob).to.have.length(1);
+
+ const annotationIdToDelete = annotationsForJob[0]._id;
+
+ const { body } = await supertest
+ .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`)
+ .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
+ .set(COMMON_REQUEST_HEADERS)
+ .expect(200);
+
+ expect(body._id).to.eql(annotationIdToDelete);
+ expect(body.result).to.eql('deleted');
+
+ await ml.api.waitForAnnotationNotToExist(annotationIdToDelete);
+ });
+
+ it('should delete annotation by id for user with viewer permission', async () => {
+ const annotationsForJob = await ml.api.getAnnotations(jobIds[1]);
+ expect(annotationsForJob).to.have.length(1);
+
+ const annotationIdToDelete = annotationsForJob[0]._id;
+
+ const { body } = await supertest
+ .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`)
+ .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER))
+ .set(COMMON_REQUEST_HEADERS)
+ .expect(200);
+
+ expect(body._id).to.eql(annotationIdToDelete);
+ expect(body.result).to.eql('deleted');
+
+ await ml.api.waitForAnnotationNotToExist(annotationIdToDelete);
+ });
+
+ it('should not delete annotation for unauthorized user', async () => {
+ const annotationsForJob = await ml.api.getAnnotations(jobIds[2]);
+ expect(annotationsForJob).to.have.length(1);
+
+ const annotationIdToDelete = annotationsForJob[0]._id;
+
+ const { body } = await supertest
+ .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`)
+ .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED))
+ .set(COMMON_REQUEST_HEADERS)
+ .expect(404);
+
+ expect(body.error).to.eql('Not Found');
+ expect(body.message).to.eql('Not Found');
+
+ await ml.api.waitForAnnotationToExist(annotationIdToDelete);
+ });
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts
new file mode 100644
index 0000000000000..710473eed6901
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts
@@ -0,0 +1,130 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+import { omit } from 'lodash';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common';
+import { USER } from '../../../../functional/services/ml/security_common';
+import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ describe('get_annotations', function () {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/farequote');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+
+ // generate one annotation for each job
+ for (let i = 0; i < testSetupJobConfigs.length; i++) {
+ const job = testSetupJobConfigs[i];
+ const annotationToIndex = testSetupAnnotations[i];
+ await ml.api.createAnomalyDetectionJob(job);
+ await ml.api.indexAnnotation(annotationToIndex);
+ }
+ });
+
+ after(async () => {
+ await ml.api.cleanMlIndices();
+ });
+
+ it('should fetch all annotations for jobId', async () => {
+ const requestBody = {
+ jobIds: [jobIds[0]],
+ earliestMs: 1454804100000,
+ latestMs: Date.now(),
+ maxAnnotations: 500,
+ };
+ const { body } = await supertest
+ .post('/api/ml/annotations')
+ .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(requestBody)
+ .expect(200);
+
+ expect(body.success).to.eql(true);
+ expect(body.annotations).not.to.be(undefined);
+ [jobIds[0]].forEach((jobId, idx) => {
+ expect(body.annotations).to.have.property(jobId);
+ expect(body.annotations[jobId]).to.have.length(1);
+
+ const indexedAnnotation = omit(body.annotations[jobId][0], '_id');
+ expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]);
+ });
+ });
+
+ it('should fetch all annotations for multiple jobs', async () => {
+ const requestBody = {
+ jobIds,
+ earliestMs: 1454804100000,
+ latestMs: Date.now(),
+ maxAnnotations: 500,
+ };
+ const { body } = await supertest
+ .post('/api/ml/annotations')
+ .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(requestBody)
+ .expect(200);
+
+ expect(body.success).to.eql(true);
+ expect(body.annotations).not.to.be(undefined);
+ jobIds.forEach((jobId, idx) => {
+ expect(body.annotations).to.have.property(jobId);
+ expect(body.annotations[jobId]).to.have.length(1);
+
+ const indexedAnnotation = omit(body.annotations[jobId][0], '_id');
+ expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]);
+ });
+ });
+
+ it('should fetch all annotations for user with ML read permissions', async () => {
+ const requestBody = {
+ jobIds: testSetupJobConfigs.map((j) => j.job_id),
+ earliestMs: 1454804100000,
+ latestMs: Date.now(),
+ maxAnnotations: 500,
+ };
+ const { body } = await supertest
+ .post('/api/ml/annotations')
+ .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(requestBody)
+ .expect(200);
+ expect(body.success).to.eql(true);
+ expect(body.annotations).not.to.be(undefined);
+ jobIds.forEach((jobId, idx) => {
+ expect(body.annotations).to.have.property(jobId);
+ expect(body.annotations[jobId]).to.have.length(1);
+
+ const indexedAnnotation = omit(body.annotations[jobId][0], '_id');
+ expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]);
+ });
+ });
+
+ it('should not allow to fetch annotation for unauthorized user', async () => {
+ const requestBody = {
+ jobIds: testSetupJobConfigs.map((j) => j.job_id),
+ earliestMs: 1454804100000,
+ latestMs: Date.now(),
+ maxAnnotations: 500,
+ };
+ const { body } = await supertest
+ .post('/api/ml/annotations')
+ .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(requestBody)
+ .expect(404);
+
+ expect(body.error).to.eql('Not Found');
+ expect(body.message).to.eql('Not Found');
+ });
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/annotations/index.ts b/x-pack/test/api_integration/apis/ml/annotations/index.ts
new file mode 100644
index 0000000000000..7d73ee43d4d99
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/annotations/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+ describe('annotations', function () {
+ loadTestFile(require.resolve('./create_annotations'));
+ loadTestFile(require.resolve('./get_annotations'));
+ loadTestFile(require.resolve('./delete_annotations'));
+ loadTestFile(require.resolve('./update_annotations'));
+ });
+}
diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts
new file mode 100644
index 0000000000000..ba73617151120
--- /dev/null
+++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts
@@ -0,0 +1,175 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common';
+import { USER } from '../../../../functional/services/ml/security_common';
+import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations';
+import { Annotation } from '../../../../../plugins/ml/common/types/annotations';
+import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const supertest = getService('supertestWithoutAuth');
+ const ml = getService('ml');
+
+ const commonAnnotationUpdateRequestBody: Partial = {
+ timestamp: Date.now(),
+ end_timestamp: Date.now(),
+ annotation: 'Updated annotation',
+ type: ANNOTATION_TYPE.ANNOTATION,
+ event: 'model_change',
+ detector_index: 2,
+ partition_field_name: 'airline',
+ partition_field_value: 'ANA',
+ };
+
+ describe('update_annotations', function () {
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/farequote');
+ await ml.testResources.setKibanaTimeZoneToUTC();
+
+ // generate one annotation for each job
+ for (let i = 0; i < testSetupJobConfigs.length; i++) {
+ const job = testSetupJobConfigs[i];
+ const annotationToIndex = testSetupAnnotations[i];
+ await ml.api.createAnomalyDetectionJob(job);
+ await ml.api.indexAnnotation(annotationToIndex);
+ }
+ });
+
+ after(async () => {
+ await ml.api.cleanMlIndices();
+ });
+
+ it('should correctly update annotation by id', async () => {
+ const annotationsForJob = await ml.api.getAnnotations(jobIds[0]);
+ expect(annotationsForJob).to.have.length(1);
+
+ const originalAnnotation = annotationsForJob[0];
+ const annotationUpdateRequestBody = {
+ ...commonAnnotationUpdateRequestBody,
+ job_id: originalAnnotation._source.job_id,
+ _id: originalAnnotation._id,
+ };
+
+ const { body } = await supertest
+ .put('/api/ml/annotations/index')
+ .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(annotationUpdateRequestBody)
+ .expect(200);
+
+ expect(body._id).to.eql(originalAnnotation._id);
+ expect(body.result).to.eql('updated');
+
+ const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id);
+
+ if (updatedAnnotation) {
+ Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => {
+ const field = key as keyof Annotation;
+ expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]);
+ });
+ }
+ });
+
+ it('should correctly update annotation for user with viewer permission', async () => {
+ const annotationsForJob = await ml.api.getAnnotations(jobIds[1]);
+ expect(annotationsForJob).to.have.length(1);
+
+ const originalAnnotation = annotationsForJob[0];
+ const annotationUpdateRequestBody = {
+ ...commonAnnotationUpdateRequestBody,
+ job_id: originalAnnotation._source.job_id,
+ _id: originalAnnotation._id,
+ };
+
+ const { body } = await supertest
+ .put('/api/ml/annotations/index')
+ .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(annotationUpdateRequestBody)
+ .expect(200);
+
+ expect(body._id).to.eql(originalAnnotation._id);
+ expect(body.result).to.eql('updated');
+
+ const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id);
+ if (updatedAnnotation) {
+ Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => {
+ const field = key as keyof Annotation;
+ expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]);
+ });
+ }
+ });
+
+ it('should not update annotation for unauthorized user', async () => {
+ const annotationsForJob = await ml.api.getAnnotations(jobIds[2]);
+ expect(annotationsForJob).to.have.length(1);
+
+ const originalAnnotation = annotationsForJob[0];
+
+ const annotationUpdateRequestBody = {
+ ...commonAnnotationUpdateRequestBody,
+ job_id: originalAnnotation._source.job_id,
+ _id: originalAnnotation._id,
+ };
+
+ const { body } = await supertest
+ .put('/api/ml/annotations/index')
+ .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(annotationUpdateRequestBody)
+ .expect(404);
+
+ expect(body.error).to.eql('Not Found');
+ expect(body.message).to.eql('Not Found');
+
+ const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id);
+ expect(updatedAnnotation).to.eql(originalAnnotation._source);
+ });
+
+ it('should override fields correctly', async () => {
+ const annotationsForJob = await ml.api.getAnnotations(jobIds[3]);
+ expect(annotationsForJob).to.have.length(1);
+
+ const originalAnnotation = annotationsForJob[0];
+ const annotationUpdateRequestBodyWithMissingFields: Partial = {
+ timestamp: Date.now(),
+ end_timestamp: Date.now(),
+ annotation: 'Updated annotation',
+ job_id: originalAnnotation._source.job_id,
+ type: ANNOTATION_TYPE.ANNOTATION,
+ event: 'model_change',
+ detector_index: 2,
+ _id: originalAnnotation._id,
+ };
+ await supertest
+ .put('/api/ml/annotations/index')
+ .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
+ .set(COMMON_REQUEST_HEADERS)
+ .send(annotationUpdateRequestBodyWithMissingFields)
+ .expect(200);
+
+ const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id);
+ if (updatedAnnotation) {
+ Object.keys(annotationUpdateRequestBodyWithMissingFields).forEach((key) => {
+ if (key !== '_id') {
+ const field = key as keyof Annotation;
+ expect(updatedAnnotation[field]).to.eql(
+ annotationUpdateRequestBodyWithMissingFields[field]
+ );
+ }
+ });
+ }
+ // validate missing fields in the annotationUpdateRequestBody
+ expect(updatedAnnotation?.partition_field_name).to.be(undefined);
+ expect(updatedAnnotation?.partition_field_value).to.be(undefined);
+ });
+ });
+};
diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts
index b29bc47b50394..969f291b0d8b3 100644
--- a/x-pack/test/api_integration/apis/ml/index.ts
+++ b/x-pack/test/api_integration/apis/ml/index.ts
@@ -60,5 +60,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./data_frame_analytics'));
loadTestFile(require.resolve('./filters'));
loadTestFile(require.resolve('./calendars'));
+ loadTestFile(require.resolve('./annotations'));
});
}
diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts
index a9bbf09a9e6f9..f99dd4c65fc83 100644
--- a/x-pack/test/api_integration/apis/security_solution/sources.ts
+++ b/x-pack/test/api_integration/apis/security_solution/sources.ts
@@ -18,22 +18,97 @@ export default function ({ getService }: FtrProviderContext) {
before(() => esArchiver.load('auditbeat/default'));
after(() => esArchiver.unload('auditbeat/default'));
- it('Make sure that we get source information when auditbeat indices is there', () => {
- return client
- .query({
- query: sourceQuery,
- variables: {
- sourceId: 'default',
- defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
- docValueFields: [],
- },
- })
- .then((resp) => {
- const sourceStatus = resp.data.source.status;
- // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz
- expect(sourceStatus.indexFields.length).to.be(397);
- expect(sourceStatus.indicesExist).to.be(true);
- });
+ it('Make sure that we get source information when auditbeat indices is there', async () => {
+ const resp = await client.query({
+ query: sourceQuery,
+ variables: {
+ sourceId: 'default',
+ defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
+ docValueFields: [],
+ },
+ });
+ const sourceStatus = resp.data.source.status;
+ // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz
+ expect(sourceStatus.indexFields.length).to.be(397);
+ expect(sourceStatus.indicesExist).to.be(true);
+ });
+
+ it('should find indexes as being available when they exist', async () => {
+ const resp = await client.query({
+ query: sourceQuery,
+ variables: {
+ sourceId: 'default',
+ defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
+ docValueFields: [],
+ },
+ });
+ const sourceStatus = resp.data.source.status;
+ expect(sourceStatus.indicesExist).to.be(true);
+ });
+
+ it('should not find indexes as existing when there is an empty array of them', async () => {
+ const resp = await client.query({
+ query: sourceQuery,
+ variables: {
+ sourceId: 'default',
+ defaultIndex: [],
+ docValueFields: [],
+ },
+ });
+ const sourceStatus = resp.data.source.status;
+ expect(sourceStatus.indicesExist).to.be(false);
+ });
+
+ it('should not find indexes as existing when there is a _all within it', async () => {
+ const resp = await client.query({
+ query: sourceQuery,
+ variables: {
+ sourceId: 'default',
+ defaultIndex: ['_all'],
+ docValueFields: [],
+ },
+ });
+ const sourceStatus = resp.data.source.status;
+ expect(sourceStatus.indicesExist).to.be(false);
+ });
+
+ it('should not find indexes as existing when there are empty strings within it', async () => {
+ const resp = await client.query({
+ query: sourceQuery,
+ variables: {
+ sourceId: 'default',
+ defaultIndex: [''],
+ docValueFields: [],
+ },
+ });
+ const sourceStatus = resp.data.source.status;
+ expect(sourceStatus.indicesExist).to.be(false);
+ });
+
+ it('should not find indexes as existing when there are blank spaces within it', async () => {
+ const resp = await client.query({
+ query: sourceQuery,
+ variables: {
+ sourceId: 'default',
+ defaultIndex: [' '],
+ docValueFields: [],
+ },
+ });
+ const sourceStatus = resp.data.source.status;
+ expect(sourceStatus.indicesExist).to.be(false);
+ });
+
+ it('should find indexes when one is an empty index but the others are valid', async () => {
+ const resp = await client.query({
+ query: sourceQuery,
+ variables: {
+ sourceId: 'default',
+ defaultIndex: ['', 'auditbeat-*'],
+ docValueFields: [],
+ },
+ });
+ const sourceStatus = resp.data.source.status;
+ expect(sourceStatus.indicesExist).to.be(true);
});
});
}
diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts
index c2500dca78444..68e5045c1f36c 100644
--- a/x-pack/test/functional/apps/graph/graph.ts
+++ b/x-pack/test/functional/apps/graph/graph.ts
@@ -129,17 +129,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should show venn when clicking a line', async function () {
await buildGraph();
- const { edges } = await PageObjects.graph.getGraphObjects();
await PageObjects.graph.isolateEdge('test', '/test/wp-admin/');
await PageObjects.graph.stopLayout();
await PageObjects.common.sleep(1000);
- const testTestWpAdminBlogEdge = edges.find(
- ({ sourceNode, targetNode }) =>
- targetNode.label === '/test/wp-admin/' && sourceNode.label === 'test'
- )!;
- await testTestWpAdminBlogEdge.element.click();
+ await browser.execute(() => {
+ const event = document.createEvent('SVGEvents');
+ event.initEvent('click', true, true);
+ return document.getElementsByClassName('gphEdge')[0].dispatchEvent(event);
+ });
await PageObjects.common.sleep(1000);
await PageObjects.graph.startLayout();
diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts
index ccf35a1e63e37..7e9a2cd85935e 100644
--- a/x-pack/test/functional/apps/uptime/certificates.ts
+++ b/x-pack/test/functional/apps/uptime/certificates.ts
@@ -14,8 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const es = getService('es');
- // Failing: See https://github.com/elastic/kibana/issues/70493
- describe.skip('certificates', function () {
+ describe('certificates', function () {
before(async () => {
await makeCheck({ es, tls: true });
await uptime.goToRoot(true);
@@ -58,6 +57,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const certId = getSha256();
const { monitorId } = await makeCheck({
es,
+ monitorId: 'cert-test-check-id',
+ fields: {
+ monitor: {
+ name: 'Cert Test Check',
+ },
+ url: {
+ full: 'https://site-to-check.com/',
+ },
+ },
tls: {
sha256: certId,
},
diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts
index 028ab3ff8803a..6b2b61cba2b64 100644
--- a/x-pack/test/functional/apps/uptime/index.ts
+++ b/x-pack/test/functional/apps/uptime/index.ts
@@ -55,6 +55,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
loadTestFile(require.resolve('./settings'));
loadTestFile(require.resolve('./certificates'));
});
+
describe('with real-world data', () => {
before(async () => {
await esArchiver.unload(ARCHIVE);
diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts
index 9dfec3a17dec0..401a96c5c11bd 100644
--- a/x-pack/test/functional/services/ml/api.ts
+++ b/x-pack/test/functional/services/ml/api.ts
@@ -5,13 +5,29 @@
*/
import expect from '@kbn/expect';
import { ProvidedType } from '@kbn/test/types/ftr';
+import { IndexDocumentParams } from 'elasticsearch';
import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index';
+import { Annotation } from '../../../../plugins/ml/common/types/annotations';
import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common';
import { FtrProviderContext } from '../../ftr_provider_context';
import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states';
import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs';
export type MlApi = ProvidedType;
+import {
+ ML_ANNOTATIONS_INDEX_ALIAS_READ,
+ ML_ANNOTATIONS_INDEX_ALIAS_WRITE,
+} from '../../../../plugins/ml/common/constants/index_patterns';
+
+interface EsIndexResult {
+ _index: string;
+ _id: string;
+ _version: number;
+ result: string;
+ _shards: any;
+ _seq_no: number;
+ _primary_term: number;
+}
export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
const es = getService('legacyEs');
@@ -634,5 +650,77 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
}
});
},
+
+ async getAnnotations(jobId: string) {
+ log.debug(`Fetching annotations for job '${jobId}'...`);
+
+ const results = await es.search({
+ index: ML_ANNOTATIONS_INDEX_ALIAS_READ,
+ body: {
+ query: {
+ match: {
+ job_id: jobId,
+ },
+ },
+ },
+ });
+ expect(results).to.not.be(undefined);
+ expect(results).to.have.property('hits');
+ return results.hits.hits;
+ },
+
+ async getAnnotationById(annotationId: string): Promise {
+ log.debug(`Fetching annotation '${annotationId}'...`);
+
+ const result = await es.search({
+ index: ML_ANNOTATIONS_INDEX_ALIAS_READ,
+ body: {
+ size: 1,
+ query: {
+ match: {
+ _id: annotationId,
+ },
+ },
+ },
+ });
+ // @ts-ignore due to outdated type for hits.total
+ if (result.hits.total.value === 1) {
+ return result?.hits?.hits[0]?._source as Annotation;
+ }
+ return undefined;
+ },
+
+ async indexAnnotation(annotationRequestBody: Partial) {
+ log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`);
+ // @ts-ignore due to outdated type for IndexDocumentParams.type
+ const params: IndexDocumentParams> = {
+ index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE,
+ body: annotationRequestBody,
+ refresh: 'wait_for',
+ };
+ const results: EsIndexResult = await es.index(params);
+ await this.waitForAnnotationToExist(results._id);
+ return results;
+ },
+
+ async waitForAnnotationToExist(annotationId: string, errorMsg?: string) {
+ await retry.tryForTime(30 * 1000, async () => {
+ if ((await this.getAnnotationById(annotationId)) !== undefined) {
+ return true;
+ } else {
+ throw new Error(errorMsg ?? `annotation '${annotationId}' should exist`);
+ }
+ });
+ },
+
+ async waitForAnnotationNotToExist(annotationId: string, errorMsg?: string) {
+ await retry.tryForTime(30 * 1000, async () => {
+ if ((await this.getAnnotationById(annotationId)) === undefined) {
+ return true;
+ } else {
+ throw new Error(errorMsg ?? `annotation '${annotationId}' should not exist`);
+ }
+ });
+ },
};
}
diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts
index 2ceab1ca89e54..06de9be5af7e9 100644
--- a/x-pack/test/functional/services/uptime/certificates.ts
+++ b/x-pack/test/functional/services/uptime/certificates.ts
@@ -7,10 +7,12 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
-export function UptimeCertProvider({ getService }: FtrProviderContext) {
+export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
+ const PageObjects = getPageObjects(['common', 'timePicker', 'header']);
+
const changeSearchField = async (text: string) => {
const input = await testSubjects.find('uptimeCertSearch');
await input.clearValueWithKeyboard();
@@ -61,6 +63,7 @@ export function UptimeCertProvider({ getService }: FtrProviderContext) {
const self = this;
return retry.tryForTime(60 * 1000, async () => {
await changeSearchField(monId);
+ await PageObjects.header.waitUntilLoadingHasFinished();
await self.hasCertificates(1);
});
},
diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts
index ab511abf130a5..710923c886cbe 100644
--- a/x-pack/test/functional/services/uptime/navigation.ts
+++ b/x-pack/test/functional/services/uptime/navigation.ts
@@ -17,7 +17,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv
if (await testSubjects.exists('uptimeSettingsToOverviewLink', { timeout: 0 })) {
await testSubjects.click('uptimeSettingsToOverviewLink');
await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 });
- } else if (!(await testSubjects.exists('uptimeOverviewPage', { timeout: 0 }))) {
+ } else {
await PageObjects.common.navigateToApp('uptime');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 });
diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
index 6cb74aff95be2..a6de87d6f7b1a 100644
--- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
@@ -8,8 +8,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
- // FLAKY: https://github.com/elastic/kibana/issues/65948
- describe.skip('uptime alerts', () => {
+ describe('uptime alerts', () => {
const pageObjects = getPageObjects(['common', 'uptime']);
const supertest = getService('supertest');
const retry = getService('retry');
@@ -105,7 +104,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
alertTypeId,
consumer,
id,
- params: { numTimes, timerange, locations, filters },
+ params: { numTimes, timerangeUnit, timerangeCount, filters },
schedule: { interval },
tags,
} = alert;
@@ -119,14 +118,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(interval).to.eql('11m');
expect(tags).to.eql(['uptime', 'another']);
expect(numTimes).to.be(3);
- expect(timerange.from).to.be('now-1h');
- expect(timerange.to).to.be('now');
- expect(locations).to.eql(['mpls']);
- expect(filters).to.eql(
- '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' +
- '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' +
- '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' +
- '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}'
+ expect(timerangeUnit).to.be('h');
+ expect(timerangeCount).to.be(1);
+ expect(JSON.stringify(filters)).to.eql(
+ `{"url.port":["5678"],"observer.geo.name":["mpls"],"monitor.type":["http"],"tags":[]}`
);
} finally {
await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204);