diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5c903966bd0cc..8794e81e4a441 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -61,6 +61,7 @@ export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000 as const; // ms export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100 as const; export const SECURITY_FEATURE_ID = 'Security' as const; export const SECURITY_TAG_NAME = 'Security Solution' as const; +export const SECURITY_TAG_DESCRIPTION = 'Security Solution auto-generated tag' as const; export const DEFAULT_SPACE_ID = 'default' as const; export const DEFAULT_RELATIVE_DATE_THRESHOLD = 24 as const; @@ -303,6 +304,10 @@ export const prebuiltSavedObjectsBulkCreateUrl = (templateName: string) => export const PREBUILT_SAVED_OBJECTS_BULK_DELETE = `${INTERNAL_RISK_SCORE_URL}/prebuilt_content/saved_objects/_bulk_delete/{template_name}`; export const prebuiltSavedObjectsBulkDeleteUrl = (templateName: string) => `${INTERNAL_RISK_SCORE_URL}/prebuilt_content/saved_objects/_bulk_delete/${templateName}` as const; + +export const INTERNAL_DASHBOARDS_URL = `/internal/dashboards` as const; +export const INTERNAL_TAGS_URL = `/internal/tags`; + export const RISK_SCORE_CREATE_INDEX = `${INTERNAL_RISK_SCORE_URL}/indices/create`; export const RISK_SCORE_DELETE_INDICES = `${INTERNAL_RISK_SCORE_URL}/indices/delete`; export const RISK_SCORE_CREATE_STORED_SCRIPT = `${INTERNAL_RISK_SCORE_URL}/stored_scripts/create`; diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/__mocks__/utils.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/__mocks__/utils.ts new file mode 100644 index 0000000000000..eb8263c9f9adc --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/__mocks__/utils.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MOCK_TAG_ID, DEFAULT_DASHBOARDS_RESPONSE } from '../api/__mocks__'; + +export const getSecurityTagIds = jest.fn().mockResolvedValue([MOCK_TAG_ID]); + +export const getSecurityDashboards = jest.fn().mockResolvedValue(DEFAULT_DASHBOARDS_RESPONSE); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/api/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/api/__mocks__/index.ts new file mode 100644 index 0000000000000..7dd6d2c9132ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/api/__mocks__/index.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SECURITY_TAG_NAME, SECURITY_TAG_DESCRIPTION } from '../../../../../../common/constants'; + +export const MOCK_TAG_ID = 'securityTagId'; + +export const DEFAULT_TAGS_RESPONSE = [ + { + id: MOCK_TAG_ID, + name: SECURITY_TAG_NAME, + description: SECURITY_TAG_DESCRIPTION, + color: '#2c7b82', + }, +]; + +export const DEFAULT_DASHBOARDS_RESPONSE = [ + { + type: 'dashboard', + id: 'c0ac2c00-c1c0-11e7-8995-936807a28b16-ecs', + namespaces: ['default'], + attributes: { + description: 'Summary of Linux kernel audit events.', + title: '[Auditbeat Auditd] Overview ECS', + version: 1, + }, + references: [ + { + name: 'tag-ref-ba964280-d211-11ed-890b-153ddf1a08e9', + id: 'ba964280-d211-11ed-890b-153ddf1a08e9', + type: 'tag', + }, + ], + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.7.0', + updated_at: '2023-04-03T11:38:00.902Z', + created_at: '2023-04-03T11:20:50.603Z', + version: 'WzE4NzQsMV0=', + score: 0, + }, +]; + +export const getSecuritySolutionTags = jest + .fn() + .mockImplementation(() => Promise.resolve(DEFAULT_TAGS_RESPONSE)); + +export const getSecuritySolutionDashboards = jest + .fn() + .mockImplementation(() => Promise.resolve(DEFAULT_DASHBOARDS_RESPONSE)); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/api/index.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/api/index.ts new file mode 100644 index 0000000000000..841f348bd24ab --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/api/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import type { Tag } from '@kbn/saved-objects-tagging-plugin/public'; +import { INTERNAL_TAGS_URL, INTERNAL_DASHBOARDS_URL } from '../../../../../common/constants'; +import type { DashboardTableItem } from '../types'; + +export const getSecuritySolutionTags = ({ http }: { http: HttpSetup }): Promise => + http.get(INTERNAL_TAGS_URL); + +export const getSecuritySolutionDashboards = ({ + http, +}: { + http: HttpSetup; +}): Promise => http.get(INTERNAL_DASHBOARDS_URL); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/types.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/types.ts new file mode 100644 index 0000000000000..e82c383192218 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface DashboardTableItem { + id: string; + type: string; + attributes: { + title: string; + description: string; + }; + references: Array<{ name: string; type: string; id: string }>; +} diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.test.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.test.ts index b92b303a7b77f..a41640d678262 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.test.ts @@ -9,26 +9,15 @@ import { renderHook, act } from '@testing-library/react-hooks'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock/test_providers'; -import type { Tag } from '@kbn/saved-objects-tagging-plugin/common'; import { useCreateSecurityDashboardLink } from './use_create_security_dashboard_link'; +import { MOCK_TAG_ID } from './api/__mocks__'; +import { getSecurityTagIds as mockGetSecurityTagIds } from './utils'; jest.mock('../../lib/kibana'); -const TAG_ID = 'securityTagId'; -const CREATED_TAG: Tag = { - id: TAG_ID, - name: 'tag title', - description: 'tag description', - color: '#999999', -}; const URL = '/path'; -const mockGetSecurityTagId = jest.fn(async (): Promise => null); -const mockCreateSecurityTag = jest.fn(async () => CREATED_TAG); -jest.mock('./utils', () => ({ - getSecurityTagId: () => mockGetSecurityTagId(), - createSecurityTag: () => mockCreateSecurityTag(), -})); +jest.mock('./utils'); const renderUseCreateSecurityDashboardLink = () => renderHook(() => useCreateSecurityDashboardLink(), { @@ -57,8 +46,7 @@ describe('useCreateSecurityDashboardLink', () => { it('should request when renders', async () => { await asyncRenderUseCreateSecurityDashboard(); - expect(mockGetSecurityTagId).toHaveBeenCalledTimes(1); - expect(mockCreateSecurityTag).toHaveBeenCalledTimes(1); + expect(mockGetSecurityTagIds).toHaveBeenCalledTimes(1); }); it('should return a memoized value when rerendered', async () => { @@ -71,28 +59,18 @@ describe('useCreateSecurityDashboardLink', () => { expect(result1).toBe(result2); }); - it('should not request create tag if already exists', async () => { - mockGetSecurityTagId.mockResolvedValueOnce(TAG_ID); - await asyncRenderUseCreateSecurityDashboard(); - - expect(mockGetSecurityTagId).toHaveBeenCalledTimes(1); - expect(mockCreateSecurityTag).not.toHaveBeenCalled(); - }); - it('should generate create url with tag', async () => { await asyncRenderUseCreateSecurityDashboard(); - expect(mockGetRedirectUrl).toHaveBeenCalledWith({ tags: [TAG_ID] }); + expect(mockGetRedirectUrl).toHaveBeenCalledWith({ tags: [MOCK_TAG_ID] }); }); it('should not re-request tag id when re-rendered', async () => { const { rerender } = await asyncRenderUseCreateSecurityDashboard(); - expect(mockGetSecurityTagId).toHaveBeenCalledTimes(1); - expect(mockCreateSecurityTag).toHaveBeenCalledTimes(1); + expect(mockGetSecurityTagIds).toHaveBeenCalledTimes(1); act(() => rerender()); - expect(mockGetSecurityTagId).toHaveBeenCalledTimes(1); - expect(mockCreateSecurityTag).toHaveBeenCalledTimes(1); + expect(mockGetSecurityTagIds).toHaveBeenCalledTimes(1); }); it('should return isLoading while requesting', async () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.ts index 2c8c5d7315de8..7b5a46d9b78e6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.ts +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.ts @@ -7,30 +7,24 @@ import { useEffect, useMemo, useState } from 'react'; import { useKibana } from '../../lib/kibana'; -import { getSecurityTagId, createSecurityTag } from './utils'; +import { getSecurityTagIds } from './utils'; type UseCreateDashboard = () => { isLoading: boolean; url: string }; export const useCreateSecurityDashboardLink: UseCreateDashboard = () => { - const { - dashboard: { locator } = {}, - savedObjects: { client: savedObjectsClient }, - savedObjectsTagging, - } = useKibana().services; + const { dashboard: { locator } = {}, savedObjectsTagging, http } = useKibana().services; - const [securityTagId, setSecurityTagId] = useState(null); + const [securityTagId, setSecurityTagId] = useState(null); useEffect(() => { let ignore = false; const getOrCreateSecurityTag = async () => { - if (savedObjectsClient && savedObjectsTagging) { - let tagId = await getSecurityTagId(savedObjectsClient); - if (!tagId) { - const newTag = await createSecurityTag(savedObjectsTagging.client); - tagId = newTag.id; - } - if (!ignore) { - setSecurityTagId(tagId); + if (http && savedObjectsTagging) { + // getSecurityTagIds creates a tag if it coundn't find one + const tagIds = await getSecurityTagIds(http); + + if (!ignore && tagIds) { + setSecurityTagId(tagIds); } } }; @@ -40,12 +34,12 @@ export const useCreateSecurityDashboardLink: UseCreateDashboard = () => { return () => { ignore = true; }; - }, [savedObjectsClient, savedObjectsTagging]); + }, [http, savedObjectsTagging]); const result = useMemo( () => ({ isLoading: securityTagId == null, - url: securityTagId ? locator?.getRedirectUrl({ tags: [securityTagId] }) ?? '' : '', + url: securityTagId ? locator?.getRedirectUrl({ tags: [securityTagId[0]] }) ?? '' : '', }), [securityTagId, locator] ); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx index 3a6a12be604e9..143a1335d4de8 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx @@ -12,12 +12,12 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import { EuiBasicTable } from '@elastic/eui'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock/test_providers'; -import type { DashboardTableItem } from './use_security_dashboards_table'; import { useSecurityDashboardsTableColumns, useSecurityDashboardsTableItems, } from './use_security_dashboards_table'; import * as telemetry from '../../lib/telemetry'; +import type { DashboardTableItem } from './types'; jest.mock('../../lib/kibana'); const spyTrack = jest.spyOn(telemetry, 'track'); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx index 771e68fde0b78..9380ce31ea49c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx @@ -8,26 +8,18 @@ import React, { useEffect, useMemo, useCallback } from 'react'; import type { MouseEventHandler } from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; -import type { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { SavedObject } from '@kbn/core/public'; import { getSecurityDashboards } from './utils'; import { LinkAnchor } from '../../components/links'; import { useKibana, useNavigateTo } from '../../lib/kibana'; import * as i18n from './translations'; import { useFetch, REQUEST_NAMES } from '../../hooks/use_fetch'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry'; - -export interface DashboardTableItem extends SavedObject { - title?: string; - description?: string; -} +import type { DashboardTableItem } from './types'; const EMPTY_DESCRIPTION = '-' as const; export const useSecurityDashboardsTableItems = () => { - const { - savedObjects: { client: savedObjectsClient }, - } = useKibana().services; + const { http } = useKibana().services; const { fetch, data, isLoading, error } = useFetch( REQUEST_NAMES.SECURITY_DASHBOARDS, @@ -35,10 +27,10 @@ export const useSecurityDashboardsTableItems = () => { ); useEffect(() => { - if (savedObjectsClient) { - fetch(savedObjectsClient); + if (http) { + fetch(http); } - }, [fetch, savedObjectsClient]); + }, [fetch, http]); const items = useMemo(() => { if (!data) { diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.test.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.test.ts index 24911a82a2e82..72c1057fb5e45 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.test.ts @@ -5,75 +5,82 @@ * 2.0. */ -import type { SavedObjectsClientContract } from '@kbn/core/public'; -import { SECURITY_TAG_NAME } from '../../../../common/constants'; -import { getSecurityTagId } from './utils'; +import type { HttpSetup } from '@kbn/core/public'; +import { + getSecuritySolutionTags as mockGetSecuritySolutionTags, + getSecuritySolutionDashboards as mockGetSecuritySolutionDashboards, +} from './api'; +import { getSecurityDashboards, getSecurityTagIds } from './utils'; -const TAG_ID = 'securityTagId'; -const DEFAULT_TAGS_RESPONSE = [ - { - id: TAG_ID, - attributes: { name: SECURITY_TAG_NAME }, - }, - { - id: `${TAG_ID}_2`, - attributes: { name: `${SECURITY_TAG_NAME}_2` }, - }, -]; - -const mockSavedObjectsFind = jest.fn(async () => ({ savedObjects: DEFAULT_TAGS_RESPONSE })); -const savedObjectsClient = { - find: mockSavedObjectsFind, -} as unknown as SavedObjectsClientContract; +jest.mock('./api'); +const mockHttp = {} as unknown as HttpSetup; describe('dashboards utils', () => { afterEach(() => { jest.clearAllMocks(); }); - describe('getSecurityTagId', () => { - it('should call saved objects find with security tag name', async () => { - await getSecurityTagId(savedObjectsClient); + describe('getSecurityTagIds', () => { + it('should call getSecuritySolutionTags with http', async () => { + await getSecurityTagIds(mockHttp); - expect(mockSavedObjectsFind).toHaveBeenCalledWith( - expect.objectContaining({ type: 'tag', search: SECURITY_TAG_NAME, searchFields: ['name'] }) + expect(mockGetSecuritySolutionTags).toHaveBeenCalledWith( + expect.objectContaining({ http: mockHttp }) ); }); - it('should find saved object with security tag name', async () => { - const result = await getSecurityTagId(savedObjectsClient); - - expect(result).toEqual(TAG_ID); - }); - - it('should not find saved object with wrong security tag name', async () => { - mockSavedObjectsFind.mockResolvedValueOnce({ savedObjects: [DEFAULT_TAGS_RESPONSE[1]] }); - const result = await getSecurityTagId(savedObjectsClient); + it('should find saved objects Ids with security tags', async () => { + const result = await getSecurityTagIds(mockHttp); - expect(result).toBeUndefined(); + expect(result).toMatchInlineSnapshot(` + Array [ + "securityTagId", + ] + `); }); }); - describe('createSecurityTag', () => { - it('should call saved objects find with security tag name', async () => { - await getSecurityTagId(savedObjectsClient); + describe('getSecurityDashboards', () => { + it('should call getSecuritySolutionDashboards with http', async () => { + await getSecurityDashboards(mockHttp); - expect(mockSavedObjectsFind).toHaveBeenCalledWith( - expect.objectContaining({ type: 'tag', search: SECURITY_TAG_NAME, searchFields: ['name'] }) + expect(mockGetSecuritySolutionDashboards).toHaveBeenCalledWith( + expect.objectContaining({ http: mockHttp }) ); }); - it('should find saved object with security tag name', async () => { - const result = await getSecurityTagId(savedObjectsClient); - - expect(result).toEqual(TAG_ID); - }); - - it('should not find saved object with wrong security tag name', async () => { - mockSavedObjectsFind.mockResolvedValueOnce({ savedObjects: [DEFAULT_TAGS_RESPONSE[1]] }); - const result = await getSecurityTagId(savedObjectsClient); - - expect(result).toBeUndefined(); + it('should find saved objects with security tags', async () => { + const result = await getSecurityDashboards(mockHttp); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object { + "description": "Summary of Linux kernel audit events.", + "title": "[Auditbeat Auditd] Overview ECS", + "version": 1, + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-04-03T11:20:50.603Z", + "id": "c0ac2c00-c1c0-11e7-8995-936807a28b16-ecs", + "namespaces": Array [ + "default", + ], + "references": Array [ + Object { + "id": "ba964280-d211-11ed-890b-153ddf1a08e9", + "name": "tag-ref-ba964280-d211-11ed-890b-153ddf1a08e9", + "type": "tag", + }, + ], + "score": 0, + "type": "dashboard", + "typeMigrationVersion": "8.7.0", + "updated_at": "2023-04-03T11:38:00.902Z", + "version": "WzE4NzQsMV0=", + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.ts index f5f4e56424513..97a40a7196b8a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.ts +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.ts @@ -5,65 +5,26 @@ * 2.0. */ -import type { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { SavedObjectsClientContract, SavedObject } from '@kbn/core/public'; -import type { Tag, TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; -import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; -import { SECURITY_TAG_NAME } from '../../../../common/constants'; - -export const SECURITY_TAG_DESCRIPTION = 'Security Solution auto-generated tag' as const; - -/** - * Returns the hex representation of a random color (e.g `#F1B7E2`) - */ -const getRandomColor = (): string => { - return `#${String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0')}`; -}; +import type { HttpSetup } from '@kbn/core/public'; +import { getSecuritySolutionDashboards, getSecuritySolutionTags } from './api'; +import type { DashboardTableItem } from './types'; /** - * Request the security tag saved object and returns the id if exists + * Request the security tag saved object and returns the id if exists. + * It creates one if the tag doesn't exist. */ -export const getSecurityTagId = async ( - savedObjectsClient: SavedObjectsClientContract -): Promise => { - const tagResponse = await savedObjectsClient.find({ - type: 'tag', - searchFields: ['name'], - search: SECURITY_TAG_NAME, - }); - // The search query returns partial matches, we need to find the exact tag name - return tagResponse.savedObjects.find(({ attributes }) => attributes.name === SECURITY_TAG_NAME) - ?.id; -}; - -/** - * Creates the security tag saved object and returns its id - */ -export const createSecurityTag = async ( - tagsClient: SavedObjectsTaggingApi['client'] -): Promise => { - // We need to use the TaggingApi client to make sure the Dashboards app tags cache is refreshed - const tagResponse = await tagsClient.create({ - name: SECURITY_TAG_NAME, - description: SECURITY_TAG_DESCRIPTION, - color: getRandomColor(), - }); - return tagResponse; +export const getSecurityTagIds = async (http: HttpSetup): Promise => { + const tagResponse = await getSecuritySolutionTags({ http }); + return tagResponse?.map(({ id }: { id: string }) => id); }; /** * Requests the saved objects of the security tagged dashboards */ export const getSecurityDashboards = async ( - savedObjectsClient: SavedObjectsClientContract -): Promise>> => { - const tagId = await getSecurityTagId(savedObjectsClient); - if (!tagId) { - return []; - } - const dashboardsResponse = await savedObjectsClient.find({ - type: 'dashboard', - hasReference: { id: tagId, type: 'tag' }, - }); - return dashboardsResponse.savedObjects; + http: HttpSetup +): Promise => { + const dashboardsResponse = await getSecuritySolutionDashboards({ http }); + + return dashboardsResponse; }; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/dashboards/__mocks__/index.ts new file mode 100644 index 0000000000000..85960cd4629fa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/__mocks__/index.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mockGetDashboardsResult = [ + { + type: 'dashboard', + id: 'd698d5f0-cd58-11ed-affc-fb75e701db4b', + namespaces: ['default'], + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, + description: '', + timeRestore: false, + optionsJSON: + '{"useMargins":true,"syncColors":false,"syncCursor":true,"syncTooltips":false,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"8.8.0","type":"lens","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"46c2105e-0edd-460c-8ecf-aaf3777b0c9b"},"panelIndex":"46c2105e-0edd-460c-8ecf-aaf3777b0c9b","embeddableConfig":{"attributes":{"title":"my alerts chart","description":"","visualizationType":"lnsXY","type":"lens","references":[{"type":"index-pattern","id":"security-solution-default","name":"indexpattern-datasource-layer-eafb5cfd-bd7e-4c1f-a675-ef11a17c616d"}],"state":{"visualization":{"title":"Empty XY chart","legend":{"isVisible":true,"position":"left","legendSize":"xlarge"},"valueLabels":"hide","preferredSeriesType":"bar_stacked","layers":[{"layerId":"eafb5cfd-bd7e-4c1f-a675-ef11a17c616d","accessors":["e09e0380-0740-4105-becc-0a4ca12e3944"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"layerType":"data","xAccessor":"aac9d7d0-13a3-480a-892b-08207a787926","splitAccessor":"34919782-4546-43a5-b668-06ac934d3acd"}],"yRightExtent":{"mode":"full"},"yLeftExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"valuesInLegend":true},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"negate":true,"disabled":false,"type":"exists","key":"kibana.alert.building_block_type"},"query":{"exists":{"field":"kibana.alert.building_block_type"}},"$state":{"store":"appState"}},{"meta":{"type":"phrases","key":"_index","params":[".alerts-security.alerts-default"],"alias":null,"negate":false,"disabled":false},"query":{"bool":{"should":[{"match_phrase":{"_index":".alerts-security.alerts-default"}}],"minimum_should_match":1}},"$state":{"store":"appState"}}],"datasourceStates":{"formBased":{"layers":{"eafb5cfd-bd7e-4c1f-a675-ef11a17c616d":{"columns":{"aac9d7d0-13a3-480a-892b-08207a787926":{"label":"@timestamp","dataType":"date","operationType":"date_histogram","sourceField":"@timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"e09e0380-0740-4105-becc-0a4ca12e3944":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"___records___"},"34919782-4546-43a5-b668-06ac934d3acd":{"label":"Top values of kibana.alert.rule.name","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"kibana.alert.rule.name","isBucketed":true,"params":{"size":1000,"orderBy":{"type":"column","columnId":"e09e0380-0740-4105-becc-0a4ca12e3944"},"orderDirection":"desc","otherBucket":true,"missingBucket":false,"parentFormat":{"id":"terms"},"secondaryFields":[]}}},"columnOrder":["34919782-4546-43a5-b668-06ac934d3acd","aac9d7d0-13a3-480a-892b-08207a787926","e09e0380-0740-4105-becc-0a4ca12e3944"],"incompleteColumns":{}}}}},"internalReferences":[],"adHocDataViews":{}}},"enhancements":{}}},{"version":"8.8.0","type":"lens","gridData":{"x":24,"y":0,"w":24,"h":15,"i":"8bcec072-4cf2-417d-a740-2b7f34c69473"},"panelIndex":"8bcec072-4cf2-417d-a740-2b7f34c69473","embeddableConfig":{"attributes":{"title":"events","description":"","visualizationType":"lnsXY","type":"lens","references":[{"type":"index-pattern","id":"security-solution-default","name":"indexpattern-datasource-layer-63ba54b1-ca7d-4fa3-b43b-d748723abad4"}],"state":{"visualization":{"title":"Empty XY chart","legend":{"isVisible":true,"position":"left","legendSize":"xlarge"},"valueLabels":"hide","preferredSeriesType":"bar_stacked","layers":[{"layerId":"63ba54b1-ca7d-4fa3-b43b-d748723abad4","accessors":["e09e0380-0740-4105-becc-0a4ca12e3944"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"layerType":"data","xAccessor":"aac9d7d0-13a3-480a-892b-08207a787926","splitAccessor":"34919782-4546-43a5-b668-06ac934d3acd"}],"yRightExtent":{"mode":"full"},"yLeftExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true}},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"disabled":false,"key":"query","negate":false,"type":"custom"},"query":{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}},"$state":{"store":"appState"}},{"meta":{"type":"phrases","key":"_index","params":["auditbeat-*","packetbeat-*"],"alias":null,"negate":false,"disabled":false},"query":{"bool":{"should":[{"match_phrase":{"_index":"auditbeat-*"}},{"match_phrase":{"_index":"packetbeat-*"}}],"minimum_should_match":1}},"$state":{"store":"appState"}}],"datasourceStates":{"formBased":{"layers":{"63ba54b1-ca7d-4fa3-b43b-d748723abad4":{"columns":{"aac9d7d0-13a3-480a-892b-08207a787926":{"label":"@timestamp","dataType":"date","operationType":"date_histogram","sourceField":"@timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"e09e0380-0740-4105-becc-0a4ca12e3944":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"___records___"},"34919782-4546-43a5-b668-06ac934d3acd":{"label":"Top values of event.action","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"event.action","isBucketed":true,"params":{"size":10,"orderBy":{"type":"column","columnId":"e09e0380-0740-4105-becc-0a4ca12e3944"},"orderDirection":"desc","otherBucket":true,"missingBucket":false,"parentFormat":{"id":"terms"}}}},"columnOrder":["34919782-4546-43a5-b668-06ac934d3acd","aac9d7d0-13a3-480a-892b-08207a787926","e09e0380-0740-4105-becc-0a4ca12e3944"],"incompleteColumns":{}}}}},"internalReferences":[],"adHocDataViews":{}}},"enhancements":{}}}]', + title: 'my alerts dashboard', + version: 1, + }, + references: [ + { + type: 'index-pattern', + id: 'security-solution-default', + name: '46c2105e-0edd-460c-8ecf-aaf3777b0c9b:indexpattern-datasource-layer-eafb5cfd-bd7e-4c1f-a675-ef11a17c616d', + }, + { + type: 'index-pattern', + id: 'security-solution-default', + name: '8bcec072-4cf2-417d-a740-2b7f34c69473:indexpattern-datasource-layer-63ba54b1-ca7d-4fa3-b43b-d748723abad4', + }, + { + type: 'tag', + id: 'de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + name: 'tag-ref-de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + }, + ], + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.7.0', + updated_at: '2023-03-28T11:27:28.365Z', + created_at: '2023-03-28T11:27:28.365Z', + version: 'WzE3NTIwLDFd', + score: 0, + }, + { + type: 'dashboard', + id: 'eee18bf0-cfc1-11ed-8380-f532c904188c', + namespaces: ['default'], + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"query":"","language":"kuery"},"filter":[{"meta":{"type":"phrase","key":"event.action","params":{"query":"process_stopped"},"disabled":false,"negate":false,"alias":null,"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"},"query":{"match_phrase":{"event.action":"process_stopped"}},"$state":{"store":"appState"}}]}', + }, + description: '', + timeRestore: false, + optionsJSON: + '{"useMargins":true,"syncColors":false,"syncCursor":true,"syncTooltips":false,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"8.8.0","type":"lens","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"46c2105e-0edd-460c-8ecf-aaf3777b0c9b"},"panelIndex":"46c2105e-0edd-460c-8ecf-aaf3777b0c9b","embeddableConfig":{"attributes":{"title":"my alerts chart","description":"","visualizationType":"lnsXY","type":"lens","references":[{"type":"index-pattern","id":"security-solution-default","name":"indexpattern-datasource-layer-eafb5cfd-bd7e-4c1f-a675-ef11a17c616d"}],"state":{"visualization":{"title":"Empty XY chart","legend":{"isVisible":true,"position":"left","legendSize":"xlarge"},"valueLabels":"hide","preferredSeriesType":"bar_stacked","layers":[{"layerId":"eafb5cfd-bd7e-4c1f-a675-ef11a17c616d","accessors":["e09e0380-0740-4105-becc-0a4ca12e3944"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"layerType":"data","xAccessor":"aac9d7d0-13a3-480a-892b-08207a787926","splitAccessor":"34919782-4546-43a5-b668-06ac934d3acd"}],"yRightExtent":{"mode":"full"},"yLeftExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"valuesInLegend":true},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"negate":true,"disabled":false,"type":"exists","key":"kibana.alert.building_block_type"},"query":{"exists":{"field":"kibana.alert.building_block_type"}},"$state":{"store":"appState"}},{"meta":{"type":"phrases","key":"_index","params":[".alerts-security.alerts-default"],"alias":null,"negate":false,"disabled":false},"query":{"bool":{"should":[{"match_phrase":{"_index":".alerts-security.alerts-default"}}],"minimum_should_match":1}},"$state":{"store":"appState"}}],"datasourceStates":{"formBased":{"layers":{"eafb5cfd-bd7e-4c1f-a675-ef11a17c616d":{"columns":{"aac9d7d0-13a3-480a-892b-08207a787926":{"label":"@timestamp","dataType":"date","operationType":"date_histogram","sourceField":"@timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"e09e0380-0740-4105-becc-0a4ca12e3944":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"___records___"},"34919782-4546-43a5-b668-06ac934d3acd":{"label":"Top values of kibana.alert.rule.name","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"kibana.alert.rule.name","isBucketed":true,"params":{"size":1000,"orderBy":{"type":"column","columnId":"e09e0380-0740-4105-becc-0a4ca12e3944"},"orderDirection":"desc","otherBucket":true,"missingBucket":false,"parentFormat":{"id":"terms"},"secondaryFields":[]}}},"columnOrder":["34919782-4546-43a5-b668-06ac934d3acd","aac9d7d0-13a3-480a-892b-08207a787926","e09e0380-0740-4105-becc-0a4ca12e3944"],"incompleteColumns":{}}}}},"internalReferences":[],"adHocDataViews":{}}},"enhancements":{}}},{"version":"8.8.0","type":"lens","gridData":{"x":24,"y":0,"w":24,"h":15,"i":"8bcec072-4cf2-417d-a740-2b7f34c69473"},"panelIndex":"8bcec072-4cf2-417d-a740-2b7f34c69473","embeddableConfig":{"attributes":{"title":"events","description":"","visualizationType":"lnsXY","type":"lens","references":[{"type":"index-pattern","id":"security-solution-default","name":"indexpattern-datasource-layer-63ba54b1-ca7d-4fa3-b43b-d748723abad4"}],"state":{"visualization":{"title":"Empty XY chart","legend":{"isVisible":true,"position":"left","legendSize":"xlarge"},"valueLabels":"hide","preferredSeriesType":"bar_stacked","layers":[{"layerId":"63ba54b1-ca7d-4fa3-b43b-d748723abad4","accessors":["e09e0380-0740-4105-becc-0a4ca12e3944"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"layerType":"data","xAccessor":"aac9d7d0-13a3-480a-892b-08207a787926","splitAccessor":"34919782-4546-43a5-b668-06ac934d3acd"}],"yRightExtent":{"mode":"full"},"yLeftExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true}},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"disabled":false,"key":"query","negate":false,"type":"custom"},"query":{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}},"$state":{"store":"appState"}},{"meta":{"type":"phrases","key":"_index","params":["auditbeat-*","packetbeat-*"],"alias":null,"negate":false,"disabled":false},"query":{"bool":{"should":[{"match_phrase":{"_index":"auditbeat-*"}},{"match_phrase":{"_index":"packetbeat-*"}}],"minimum_should_match":1}},"$state":{"store":"appState"}}],"datasourceStates":{"formBased":{"layers":{"63ba54b1-ca7d-4fa3-b43b-d748723abad4":{"columns":{"aac9d7d0-13a3-480a-892b-08207a787926":{"label":"@timestamp","dataType":"date","operationType":"date_histogram","sourceField":"@timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"e09e0380-0740-4105-becc-0a4ca12e3944":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"___records___"},"34919782-4546-43a5-b668-06ac934d3acd":{"label":"Top values of event.action","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"event.action","isBucketed":true,"params":{"size":10,"orderBy":{"type":"column","columnId":"e09e0380-0740-4105-becc-0a4ca12e3944"},"orderDirection":"desc","otherBucket":true,"missingBucket":false,"parentFormat":{"id":"terms"}}}},"columnOrder":["34919782-4546-43a5-b668-06ac934d3acd","aac9d7d0-13a3-480a-892b-08207a787926","e09e0380-0740-4105-becc-0a4ca12e3944"],"incompleteColumns":{}}}}},"internalReferences":[],"adHocDataViews":{}}},"enhancements":{}}}]', + title: 'my alerts dashboard - 2', + version: 1, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + id: 'security-solution-default', + }, + { + type: 'index-pattern', + id: 'security-solution-default', + name: '46c2105e-0edd-460c-8ecf-aaf3777b0c9b:indexpattern-datasource-layer-eafb5cfd-bd7e-4c1f-a675-ef11a17c616d', + }, + { + type: 'index-pattern', + id: 'security-solution-default', + name: '8bcec072-4cf2-417d-a740-2b7f34c69473:indexpattern-datasource-layer-63ba54b1-ca7d-4fa3-b43b-d748723abad4', + }, + { + type: 'tag', + id: 'de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + name: 'tag-ref-de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + }, + { + type: 'tag', + id: 'edb233b0-cfc1-11ed-8380-f532c904188c', + name: 'tag-ref-edb233b0-cfc1-11ed-8380-f532c904188c', + }, + ], + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.7.0', + updated_at: '2023-03-31T12:45:36.175Z', + created_at: '2023-03-31T12:45:36.175Z', + version: 'WzE5MTQyLDFd', + score: 0, + }, +]; + +export const mockGetTagsResult = [ + { + type: 'tag', + id: 'de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + namespaces: ['default'], + attributes: { + name: 'Security Solution', + description: 'Security Solution auto-generated tag', + color: '#4bc922', + }, + references: [], + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.0.0', + updated_at: '2023-03-27T17:57:41.647Z', + created_at: '2023-03-27T17:57:41.647Z', + version: 'WzE2Njc1LDFd', + score: null, + sort: [1679939861647], + }, +]; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/helpers.ts b/x-pack/plugins/security_solution/server/lib/dashboards/helpers.ts new file mode 100644 index 0000000000000..58bedbc4411d2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/helpers.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Logger } from '@kbn/core/server'; +import type { + SavedObject, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '@kbn/core-saved-objects-api-server'; +import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; +import type { DashboardAttributes } from '@kbn/dashboard-plugin/common'; +import type { OutputError } from '@kbn/securitysolution-es-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { SECURITY_TAG_NAME, SECURITY_TAG_DESCRIPTION } from '../../../common/constants'; +import { createTag, findTagsByName } from './saved_objects/tags'; + +/** + * Returns the hex representation of a random color (e.g `#F1B7E2`) + */ +const getRandomColor = (): string => { + return `#${String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0')}`; +}; + +export const getOrCreateSecurityTag = async ({ + logger, + savedObjectsClient, +}: { + logger: Logger; + savedObjectsClient: SavedObjectsClientContract; +}): Promise<{ + response: Array> | null; + error?: OutputError; +}> => { + const { response: existingTags } = await findTagsByName({ + savedObjectsClient, + search: SECURITY_TAG_NAME, + }); + + if (existingTags && existingTags.length > 0) { + return { response: existingTags }; + } else { + const { error, response: createdTag } = await createTag({ + savedObjectsClient, + tagName: SECURITY_TAG_NAME, + description: SECURITY_TAG_DESCRIPTION, + color: getRandomColor(), + }); + + if (createdTag && !error) { + return { response: [createdTag] }; + } else { + logger.error(`Failed to create ${SECURITY_TAG_NAME} tag - ${JSON.stringify(error?.message)}`); + return { + response: null, + error: error ?? transformError(new Error(`Failed to create ${SECURITY_TAG_NAME} tag`)), + }; + } + } +}; + +export const getSecuritySolutionDashboards = async ({ + logger, + savedObjectsClient, +}: { + logger: Logger; + savedObjectsClient: SavedObjectsClientContract; +}): Promise<{ + response: Array> | null; + error?: OutputError; +}> => { + const { response: foundTags } = await findTagsByName({ + savedObjectsClient, + search: SECURITY_TAG_NAME, + }); + + if (!foundTags || foundTags?.length === 0) { + return { response: [] }; + } + + try { + const dashboardsResponse = await savedObjectsClient.find({ + type: 'dashboard', + hasReference: foundTags.map(({ id: tagId }) => ({ id: tagId, type: 'tag' })), + }); + return { response: dashboardsResponse.saved_objects }; + } catch (e) { + logger.error(`Failed to get SecuritySolution Dashboards - ${JSON.stringify(e?.message)}`); + + return { response: null, error: transformError(e) }; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.test.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.test.ts new file mode 100644 index 0000000000000..5bca5246e2592 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Logger } from '@kbn/core/server'; +import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import { INTERNAL_DASHBOARDS_URL } from '../../../../common/constants'; +import { + serverMock, + requestContextMock, + mockGetCurrentUser, + requestMock, +} from '../../detection_engine/routes/__mocks__'; +import { getSecuritySolutionDashboards } from '../helpers'; +import { mockGetDashboardsResult } from '../__mocks__'; +import { getSecuritySolutionDashboardsRoute } from './get_security_solution_dashboards'; +jest.mock('../helpers', () => ({ getSecuritySolutionDashboards: jest.fn() })); + +describe('getSecuritySolutionDashboardsRoute', () => { + let server: ReturnType; + let securitySetup: SecurityPluginSetup; + const { context } = requestContextMock.createTools(); + const logger = { error: jest.fn() } as unknown as Logger; + const mockRequest = requestMock.create({ + method: 'get', + path: INTERNAL_DASHBOARDS_URL, + }); + beforeEach(() => { + jest.clearAllMocks(); + server = serverMock.create(); + + securitySetup = { + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown as SecurityPluginSetup; + + getSecuritySolutionDashboardsRoute(server.router, logger, securitySetup); + }); + + it('should return dashboards with Security Solution tags', async () => { + (getSecuritySolutionDashboards as jest.Mock).mockResolvedValue({ + response: mockGetDashboardsResult, + }); + + const response = await server.inject(mockRequest, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(mockGetDashboardsResult); + }); + + it('should return error', async () => { + const error = { + statusCode: 500, + message: 'Internal Server Error', + }; + (getSecuritySolutionDashboards as jest.Mock).mockResolvedValue({ + response: null, + error, + }); + + const response = await server.inject(mockRequest, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(error.statusCode); + expect(response.body.message).toEqual(`Failed to get dashboards - ${error.message}`); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.ts new file mode 100644 index 0000000000000..828a890f963c3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Logger } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; + +import { INTERNAL_DASHBOARDS_URL } from '../../../../common/constants'; +import type { SetupPlugins } from '../../../plugin'; +import type { SecuritySolutionPluginRouter } from '../../../types'; +import { buildSiemResponse } from '../../detection_engine/routes/utils'; +import { buildFrameworkRequest } from '../../timeline/utils/common'; +import { getSecuritySolutionDashboards } from '../helpers'; + +export const getSecuritySolutionDashboardsRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger, + security: SetupPlugins['security'] +) => { + router.get( + { + path: INTERNAL_DASHBOARDS_URL, + validate: false, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; + + const { response: dashboards, error } = await getSecuritySolutionDashboards({ + logger, + savedObjectsClient, + }); + if (!error && dashboards != null) { + return response.ok({ body: dashboards }); + } else { + return siemResponse.error({ + statusCode: error?.statusCode ?? 500, + body: i18n.translate( + 'xpack.securitySolution.dashboards.getSecuritySolutionDashboardsErrorTitle', + { + values: { message: error?.message }, + defaultMessage: `Failed to get dashboards - {message}`, + } + ), + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.test.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.test.ts new file mode 100644 index 0000000000000..8f59087ff9ee6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Logger } from '@kbn/core/server'; +import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import { INTERNAL_TAGS_URL, SECURITY_TAG_NAME } from '../../../../common/constants'; +import { + serverMock, + requestContextMock, + mockGetCurrentUser, + requestMock, +} from '../../detection_engine/routes/__mocks__'; +import { getOrCreateSecurityTag } from '../helpers'; +import { mockGetTagsResult } from '../__mocks__'; +import { getSecuritySolutionTagsRoute } from './get_security_solution_tags'; +jest.mock('../helpers', () => ({ getOrCreateSecurityTag: jest.fn() })); + +describe('getSecuritySolutionTagsRoute', () => { + let server: ReturnType; + let securitySetup: SecurityPluginSetup; + const { context } = requestContextMock.createTools(); + const logger = { error: jest.fn() } as unknown as Logger; + const mockRequest = requestMock.create({ + method: 'get', + path: INTERNAL_TAGS_URL, + }); + beforeEach(() => { + jest.clearAllMocks(); + server = serverMock.create(); + + securitySetup = { + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown as SecurityPluginSetup; + + getSecuritySolutionTagsRoute(server.router, logger, securitySetup); + }); + + it('should return tags with Security Solution tags', async () => { + (getOrCreateSecurityTag as jest.Mock).mockResolvedValue({ + response: mockGetTagsResult, + }); + + const response = await server.inject(mockRequest, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toMatchInlineSnapshot(` + Array [ + Object { + "color": "#4bc922", + "description": "Security Solution auto-generated tag", + "id": "de7ad1f0-ccc8-11ed-9175-1b0d4269ff48", + "name": "Security Solution", + }, + ] + `); + }); + + it('should return error', async () => { + const error = { + statusCode: 500, + message: 'Internal Server Error', + }; + (getOrCreateSecurityTag as jest.Mock).mockResolvedValue({ + response: null, + error, + }); + + const response = await server.inject(mockRequest, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(error.statusCode); + expect(response.body.message).toEqual( + `Failed to create ${SECURITY_TAG_NAME} tag - ${error.message}` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.ts new file mode 100644 index 0000000000000..8c70a114ba647 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Logger } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; + +import { INTERNAL_TAGS_URL, SECURITY_TAG_NAME } from '../../../../common/constants'; +import type { SetupPlugins } from '../../../plugin'; +import type { SecuritySolutionPluginRouter } from '../../../types'; +import { buildSiemResponse } from '../../detection_engine/routes/utils'; +import { buildFrameworkRequest } from '../../timeline/utils/common'; +import { getOrCreateSecurityTag } from '../helpers'; + +export const getSecuritySolutionTagsRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger, + security: SetupPlugins['security'] +) => { + router.get( + { + path: INTERNAL_TAGS_URL, + validate: false, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; + + const { response: tags, error } = await getOrCreateSecurityTag({ + logger, + savedObjectsClient, + }); + + if (tags && !error) { + return response.ok({ + body: tags.map(({ id, attributes: { name, description, color } }) => ({ + id, + name, + description, + color, + })), + }); + } else { + return siemResponse.error({ + statusCode: error?.statusCode ?? 500, + body: i18n.translate( + 'xpack.securitySolution.dashboards.getSecuritySolutionTagsErrorTitle', + { + values: { tagName: SECURITY_TAG_NAME, message: error?.message }, + defaultMessage: `Failed to create {tagName} tag - {message}`, + } + ), + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts new file mode 100644 index 0000000000000..be21be7f3e24a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { getSecuritySolutionDashboardsRoute } from './get_security_solution_dashboards'; +export { getSecuritySolutionTagsRoute } from './get_security_solution_tags'; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/create_tag.ts b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/create_tag.ts new file mode 100644 index 0000000000000..27646630b4f01 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/create_tag.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SavedObject, + SavedObjectReference, + SavedObjectsClientContract, +} from '@kbn/core/server'; +import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; +import type { OutputError } from '@kbn/securitysolution-es-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +interface CreateTagParams { + savedObjectsClient: SavedObjectsClientContract; + tagName: string; + description: string; + color: string; + references?: SavedObjectReference[]; +} + +interface CreateTagResponse { + error?: OutputError; + response: SavedObject | null; +} + +export const createTag = async ({ + savedObjectsClient, + tagName, + description, + color, + references, +}: CreateTagParams): Promise => { + const TYPE = 'tag'; + try { + const createdTag = await savedObjectsClient.create( + TYPE, + { + name: tagName, + description, + color, + }, + { references } + ); + + return { + response: createdTag, + }; + } catch (e) { + return { + error: transformError(e), + response: null, + }; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/find_tags_by_name.ts b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/find_tags_by_name.ts new file mode 100644 index 0000000000000..76cc55a950e0e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/find_tags_by_name.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract, SavedObjectsFindResult } from '@kbn/core/server'; +import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; +import type { OutputError } from '@kbn/securitysolution-es-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +export const findTagsByName = async ({ + savedObjectsClient, + search, +}: { + savedObjectsClient: SavedObjectsClientContract; + search: string; +}): Promise<{ + response: Array> | null; + error?: OutputError; +}> => { + try { + const tagResponse = await savedObjectsClient.find({ + type: 'tag', + search, + searchFields: ['name'], + sortField: 'updated_at', + sortOrder: 'desc', + }); + return { + response: tagResponse.saved_objects.filter(({ attributes: { name } }) => name === search), + }; + } catch (e) { + return { + error: transformError(e), + response: null, + }; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/index.ts b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/index.ts new file mode 100644 index 0000000000000..e9af2f18e5931 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { createTag } from './create_tag'; +export { findTagsByName } from './find_tags_by_name'; diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/bulk_create_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/bulk_create_saved_objects.ts index 7cd493cff2b74..96feae5a7c7b4 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/bulk_create_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/bulk_create_saved_objects.ts @@ -38,9 +38,10 @@ export const bulkCreateSavedObjects = async ({ spaceId, }); - const tagResult = tagResponse?.hostRiskScoreDashboards ?? tagResponse?.userRiskScoreDashboards; + const riskScoreTagResult = + tagResponse?.hostRiskScoreDashboards ?? tagResponse?.userRiskScoreDashboards; - if (!tagResult?.success) { + if (!riskScoreTagResult?.success) { return tagResponse; } @@ -79,7 +80,11 @@ export const bulkCreateSavedObjects = async ({ id: idReplaceMappings[so.id] ?? so.id, references: [ ...references, - { id: tagResult?.body?.id, name: tagResult?.body?.name, type: tagResult?.body?.type }, + { + id: riskScoreTagResult?.body?.id, + name: riskScoreTagResult?.body?.name, + type: riskScoreTagResult?.body?.type, + }, ], }; }); diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/find_or_create_tag.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/find_or_create_tag.ts index 9bb15266f2f71..fddb9dd0e47d0 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/find_or_create_tag.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/find_or_create_tag.ts @@ -9,9 +9,9 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-ser import { transformError } from '@kbn/securitysolution-es-utils'; import { i18n } from '@kbn/i18n'; import type { RiskScoreEntity } from '../../../../../common/search_strategy'; -import type { Tag } from './utils'; import { RISK_SCORE_TAG_DESCRIPTION, getRiskScoreTagName } from './utils'; import type { BulkCreateSavedObjectsResult } from '../types'; +import { createTag, findTagsByName } from '../../../dashboards/saved_objects/tags'; export const findRiskScoreTag = async ({ savedObjectsClient, @@ -20,17 +20,9 @@ export const findRiskScoreTag = async ({ savedObjectsClient: SavedObjectsClientContract; search: string; }) => { - const tagResponse = await savedObjectsClient.find({ - type: 'tag', - search, - searchFields: ['name'], - sortField: 'updated_at', - sortOrder: 'desc', - }); + const { response: tagResponse } = await findTagsByName({ savedObjectsClient, search }); - const existingRiskScoreTag = tagResponse.saved_objects.find( - ({ attributes }) => attributes.name === search - ); + const existingRiskScoreTag = tagResponse?.find(({ attributes }) => attributes.name === search); return existingRiskScoreTag ? { @@ -85,23 +77,26 @@ export const findOrCreateRiskScoreTag = async ({ }, }; } else { - try { - const { id: tagId } = await savedObjectsClient.create('tag', { - name: tagName, - description: RISK_SCORE_TAG_DESCRIPTION, - color: '#6edb7f', - }); + const { error, response: createTagResponse } = await createTag({ + savedObjectsClient, + tagName, + description: RISK_SCORE_TAG_DESCRIPTION, + color: '#6edb7f', + }); + if (!error && createTagResponse?.id) { return { [savedObjectTemplate]: { success: true, error: null, - body: { ...tag, id: tagId }, + body: { ...tag, id: createTagResponse?.id }, }, }; - } catch (e) { + } else { logger.error( - `${savedObjectTemplate} cannot be installed as failed to create the tag: ${tagName}` + `${savedObjectTemplate} cannot be installed as failed to create the tag: ${tagName} - ${JSON.stringify( + error?.message + )}` ); return { [savedObjectTemplate]: { diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/utils.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/utils.ts index 74a84a49eb92d..9392b157e6d56 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/utils.ts @@ -8,12 +8,6 @@ import { RiskScoreEntity } from '../../../../../common/search_strategy'; import type { SavedObjectTemplate } from '../types'; -export interface Tag { - id: string; - name: string; - description: string; -} - export const HOST_RISK_SCORE = 'Host Risk Score'; export const USER_RISK_SCORE = 'User Risk Score'; diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index af26823416a2b..2c88194291af9 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -71,6 +71,10 @@ import { readPrebuiltDevToolContentRoute, } from '../lib/risk_score/routes'; import { registerManageExceptionsRoutes } from '../lib/exceptions/api/register_routes'; +import { + getSecuritySolutionDashboardsRoute, + getSecuritySolutionTagsRoute, +} from '../lib/dashboards/routes'; export const initRoutes = ( router: SecuritySolutionPluginRouter, @@ -157,6 +161,10 @@ export const initRoutes = ( deletePrebuiltSavedObjectsRoute(router, security); getRiskScoreIndexStatusRoute(router); installRiskScoresRoute(router, logger, security); + + // Dashboards + getSecuritySolutionDashboardsRoute(router, logger, security); + getSecuritySolutionTagsRoute(router, logger, security); const { previewTelemetryUrlEnabled } = config.experimentalFeatures; if (previewTelemetryUrlEnabled) { // telemetry preview endpoint for e2e integration tests only at the moment.