Skip to content

Commit

Permalink
[SecuritySolution] Migrate away from browser-side SO client (#154174)
Browse files Browse the repository at this point in the history
## Summary

Issue: #154040


Apis added: 

1. Get all the tags with name `Security Solution` or Create a Security
Solution tag if no results found
#### GET /internal/tags
```
[
    {
        "id": "ba964280-d211-11ed-890b-153ddf1a08e9",
        "name": "Security Solution",
        "description": "Security Solution auto-generated tag",
        "color": "#2c7b82"
    }
]
```
2. Get dashboards with Security Solution tags
#### GET /internal/dashboards
```
[
    {
        "type": "dashboard",
        "id": "7de391b0-c1ca-11e7-8995-936807a28b16-ecs",
        "namespaces": [
            "default"
        ],
        "attributes": {
            "description": "Overview of kernel executions",
            "hits": 0,
            "kibanaSavedObjectMeta": {
                "searchSourceJSON": "{\"filter\":[],\"highlightAll\":true,\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}"
            },
            "optionsJSON": "{\"darkTheme\":false,\"useMargins\":false}",
            "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":12,\"i\":\"1\",\"w\":16,\"x\":16,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":12,\"i\":\"3\",\"w\":16,\"x\":32,\"y\":0},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":12,\"i\":\"5\",\"w\":16,\"x\":0,\"y\":0},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":20,\"i\":\"6\",\"w\":48,\"x\":0,\"y\":12},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]",
            "timeRestore": false,
            "title": "[Auditbeat Auditd] Executions ECS",
            "version": 1
        },
        "references": [
            {
                "name": "1:panel_1",
                "id": "20a8e8d0-c1c8-11e7-8995-936807a28b16-ecs",
                "type": "visualization"
            },
            {
                "name": "3:panel_3",
                "id": "f81a6de0-c1c1-11e7-8995-936807a28b16-ecs",
                "type": "visualization"
            },
            {
                "name": "5:panel_5",
                "id": "2efac370-c1ca-11e7-8995-936807a28b16-ecs",
                "type": "visualization"
            },
            {
                "name": "6:panel_6",
                "id": "d382f5b0-c1c6-11e7-8995-936807a28b16-ecs",
                "type": "search"
            },
            {
                "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:46.473Z",
        "version": "WzE4NzMsMV0=",
        "score": 0
    }
]
```

<img width="2557" alt="Screenshot 2023-03-31 at 16 10 49"
src="https://user-images.githubusercontent.com/6295984/229166859-6d765332-aa04-4da2-acde-456b04682914.png">


<img width="2547" alt="Screenshot 2023-03-31 at 16 09 28"
src="https://user-images.githubusercontent.com/6295984/229166834-0b61c1cd-53a8-4c5c-892e-94f19deb95f2.png">


### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
angorayc authored Apr 11, 2023
1 parent 5636060 commit b064003
Show file tree
Hide file tree
Showing 25 changed files with 834 additions and 191 deletions.
5 changes: 5 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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`;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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));
Original file line number Diff line number Diff line change
@@ -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<Tag[] | null> =>
http.get(INTERNAL_TAGS_URL);

export const getSecuritySolutionDashboards = ({
http,
}: {
http: HttpSetup;
}): Promise<DashboardTableItem[] | null> => http.get(INTERNAL_DASHBOARDS_URL);
Original file line number Diff line number Diff line change
@@ -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 }>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null> => null);
const mockCreateSecurityTag = jest.fn(async () => CREATED_TAG);
jest.mock('./utils', () => ({
getSecurityTagId: () => mockGetSecurityTagId(),
createSecurityTag: () => mockCreateSecurityTag(),
}));
jest.mock('./utils');

const renderUseCreateSecurityDashboardLink = () =>
renderHook(() => useCreateSecurityDashboardLink(), {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const [securityTagId, setSecurityTagId] = useState<string[] | null>(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);
}
}
};
Expand All @@ -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]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,29 @@
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<SavedObjectAttributes> {
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,
getSecurityDashboards
);

useEffect(() => {
if (savedObjectsClient) {
fetch(savedObjectsClient);
if (http) {
fetch(http);
}
}, [fetch, savedObjectsClient]);
}, [fetch, http]);

const items = useMemo(() => {
if (!data) {
Expand Down
Loading

0 comments on commit b064003

Please sign in to comment.