mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Host risk score module UI enhancement (#133708)
* test * open in dev tool * Adding comments and removing the space placeholder in some places * import dashboard * clean up * clean up buttons * isSignalIndexExists * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * getIndexExists * import dashboard * clean up * sync with the main branch * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * useDashboardButtonHref * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * tooltip * clean up * types * clean up * clean up * rename * fix unit and cypress tests * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fix type error * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fix type error * fixup * i18n * update script * unit tests * unit tests * UI review * fix up * unit tests * add trouble shooting hint and update tests * update cypress tests * Update x-pack/plugins/security_solution/server/lib/prebuilt_dev_tool_content/routes/read_prebuilt_dev_tool_content_route.ts Co-authored-by: Sergi Massaneda <sergi.massaneda@gmail.com> * review * UI review * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * update api path * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * update read_alerts_index_exists_route * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * update dev tool content * update api path * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * update snapshot Co-authored-by: Apoorva <appujo@gmail.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sergi Massaneda <sergi.massaneda@gmail.com>
This commit is contained in:
parent
141b765568
commit
5b465b9f99
48 changed files with 2648 additions and 92 deletions
|
@ -259,6 +259,7 @@ export const DETECTION_ENGINE_PREPACKAGED_URL =
|
|||
`${DETECTION_ENGINE_RULES_URL}/prepackaged` as const;
|
||||
export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges` as const;
|
||||
export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index` as const;
|
||||
|
||||
export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags` as const;
|
||||
export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL =
|
||||
`${DETECTION_ENGINE_RULES_URL}/prepackaged/_status` as const;
|
||||
|
@ -272,13 +273,23 @@ export const DETECTION_ENGINE_RULES_BULK_CREATE =
|
|||
export const DETECTION_ENGINE_RULES_BULK_UPDATE =
|
||||
`${DETECTION_ENGINE_RULES_URL}/_bulk_update` as const;
|
||||
|
||||
export const DEV_TOOL_PREBUILT_CONTENT =
|
||||
`/internal/prebuilt_content/dev_tool/{console_id}` as const;
|
||||
export const devToolPrebuiltContentUrl = (consoleId: string) =>
|
||||
`/internal/prebuilt_content/dev_tool/${consoleId}` as const;
|
||||
export const PREBUILT_SAVED_OBJECTS_BULK_CREATE =
|
||||
'/internal/prebuilt_content/saved_objects/_bulk_create/{template_name}';
|
||||
export const prebuiltSavedObjectsBulkCreateUrl = (templateName: string) =>
|
||||
`/internal/prebuilt_content/saved_objects/_bulk_create/${templateName}` as const;
|
||||
|
||||
/**
|
||||
* Internal detection engine routes
|
||||
*/
|
||||
export const INTERNAL_DETECTION_ENGINE_URL = '/internal/detection_engine' as const;
|
||||
export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL =
|
||||
`${INTERNAL_DETECTION_ENGINE_URL}/fleet/integrations/installed` as const;
|
||||
|
||||
export const DETECTION_ENGINE_ALERTS_INDEX_URL =
|
||||
`${INTERNAL_DETECTION_ENGINE_URL}/signal/index` as const;
|
||||
/**
|
||||
* Telemetry detection endpoint for any previews requested of what data we are
|
||||
* providing through UI/UX and for e2e tests.
|
||||
|
|
|
@ -11,7 +11,8 @@ import {
|
|||
OVERVIEW_RISKY_HOSTS_LINKS_ERROR_INNER_PANEL,
|
||||
OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL,
|
||||
OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT,
|
||||
OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON,
|
||||
OVERVIEW_RISKY_HOSTS_DOC_LINK,
|
||||
OVERVIEW_RISKY_HOSTS_IMPORT_DASHBOARD_BUTTON,
|
||||
} from '../../screens/overview';
|
||||
|
||||
import { login, visit } from '../../tasks/login';
|
||||
|
@ -34,10 +35,9 @@ describe('Risky Hosts Link Panel', () => {
|
|||
cy.get(`${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_ERROR_INNER_PANEL}`).should(
|
||||
'exist'
|
||||
);
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON}`).should('be.disabled');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 hosts');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON}`).should('exist');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON}`)
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_DOC_LINK}`)
|
||||
.should('have.attr', 'href')
|
||||
.and('match', /host-risk-score.md/);
|
||||
});
|
||||
|
@ -60,7 +60,6 @@ describe('Risky Hosts Link Panel', () => {
|
|||
cy.get(
|
||||
`${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL}`
|
||||
).should('exist');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON}`).should('be.disabled');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 hosts');
|
||||
});
|
||||
|
||||
|
@ -69,12 +68,11 @@ describe('Risky Hosts Link Panel', () => {
|
|||
cy.get(
|
||||
`${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL}`
|
||||
).should('not.exist');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON}`).should('be.disabled');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_IMPORT_DASHBOARD_BUTTON}`).should('exist');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 6 hosts');
|
||||
|
||||
changeSpace(testSpaceName);
|
||||
cy.visit(`/s/${testSpaceName}${OVERVIEW_URL}`);
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON}`).should('be.disabled');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 hosts');
|
||||
cy.get(`${OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON}`).should('exist');
|
||||
});
|
||||
|
|
|
@ -158,8 +158,12 @@ export const OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL =
|
|||
'[data-test-subj="risky-hosts-inner-panel-warning"]';
|
||||
export const OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON =
|
||||
'[data-test-subj="risky-hosts-view-dashboard-button"]';
|
||||
export const OVERVIEW_RISKY_HOSTS_IMPORT_DASHBOARD_BUTTON =
|
||||
'[data-test-subj="create-saved-object-button"]';
|
||||
export const OVERVIEW_RISKY_HOSTS_DOC_LINK =
|
||||
'[data-test-subj="risky-hosts-inner-panel-danger-learn-more"]';
|
||||
export const OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT = `${OVERVIEW_RISKY_HOSTS_LINKS} [data-test-subj="header-panel-subtitle"]`;
|
||||
export const OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON =
|
||||
'[data-test-subj="risky-hosts-enable-module-button"]';
|
||||
'[data-test-subj="disabled-open-in-console-button-with-tooltip"]';
|
||||
|
||||
export const OVERVIEW_ALERTS_HISTOGRAM = '[data-test-subj="alerts-histogram-panel"]';
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { HttpStart } from '@kbn/core/public';
|
||||
import { prebuiltSavedObjectsBulkCreateUrl } from '../../../../../common/constants';
|
||||
|
||||
interface Options {
|
||||
templateName: string;
|
||||
}
|
||||
|
||||
export const bulkCreatePrebuiltSavedObjects = async (http: HttpStart, options: Options) => {
|
||||
const res = await http.post(prebuiltSavedObjectsBulkCreateUrl(options.templateName));
|
||||
|
||||
return res;
|
||||
};
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { ImportSavedObjectsButton } from './bulk_create_button';
|
||||
import { bulkCreatePrebuiltSavedObjects } from '../apis/bulk_create_prebuilt_saved_objects';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../hooks/use_app_toasts.mock';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
jest.mock('../apis/bulk_create_prebuilt_saved_objects');
|
||||
jest.mock('../../../hooks/use_app_toasts');
|
||||
|
||||
describe('ImportSavedObjectsButton', () => {
|
||||
const mockBulkCreatePrebuiltSavedObjects = bulkCreatePrebuiltSavedObjects as jest.Mock;
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockBulkCreatePrebuiltSavedObjects.mockReset();
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
});
|
||||
|
||||
it('renders null', () => {
|
||||
const { container } = render(
|
||||
<ImportSavedObjectsButton
|
||||
hide={true}
|
||||
successTitle="Success"
|
||||
templateName="hostRiskScoreDashboards"
|
||||
title="Import saved objects"
|
||||
/>
|
||||
);
|
||||
expect(container.childElementCount).toEqual(0);
|
||||
});
|
||||
|
||||
it('renders bulk create button', () => {
|
||||
render(
|
||||
<ImportSavedObjectsButton
|
||||
hide={false}
|
||||
successTitle="Success"
|
||||
templateName="hostRiskScoreDashboards"
|
||||
title="Import saved objects"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('create-saved-object-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('show loading icon when import saved objects', async () => {
|
||||
render(
|
||||
<ImportSavedObjectsButton
|
||||
hide={false}
|
||||
successTitle="Success"
|
||||
templateName="hostRiskScoreDashboards"
|
||||
title="Import saved objects"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('create-saved-object-button')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('create-saved-object-button'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockBulkCreatePrebuiltSavedObjects).toHaveBeenCalled();
|
||||
expect(screen.getByTestId('creating-saved-objects')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders button with successLink if successLink is given', () => {
|
||||
render(
|
||||
<ImportSavedObjectsButton
|
||||
hide={false}
|
||||
successLink="/test"
|
||||
successTitle="Success"
|
||||
templateName="hostRiskScoreDashboards"
|
||||
title="Import saved objects"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('create-saved-object-success-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders button with successLink on import module success', async () => {
|
||||
mockBulkCreatePrebuiltSavedObjects.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
attributes: {
|
||||
title: 'my saved object title 1',
|
||||
name: 'my saved object name 1',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<ImportSavedObjectsButton
|
||||
hide={false}
|
||||
successTitle="Success"
|
||||
templateName="hostRiskScoreDashboards"
|
||||
title="Import saved objects"
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('create-saved-object-button'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-saved-object-success-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders success toast on import module success', async () => {
|
||||
mockBulkCreatePrebuiltSavedObjects.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
attributes: {
|
||||
title: 'my saved object title 1',
|
||||
name: 'my saved object name 1',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<ImportSavedObjectsButton
|
||||
hide={false}
|
||||
successTitle="Success"
|
||||
templateName="hostRiskScoreDashboards"
|
||||
title="Import saved objects"
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('create-saved-object-button'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(appToastsMock.addSuccess).toHaveBeenCalledWith({
|
||||
text: 'my saved object title 1',
|
||||
title: '1 saved object imported successfully',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders error toast on import module failure', async () => {
|
||||
const mockError = new Error('Template not found!!');
|
||||
mockBulkCreatePrebuiltSavedObjects.mockRejectedValue(mockError);
|
||||
|
||||
render(
|
||||
<ImportSavedObjectsButton
|
||||
hide={false}
|
||||
successTitle="Success"
|
||||
templateName="errorTemplate"
|
||||
title="Import saved objects"
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('create-saved-object-button'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(appToastsMock.addError).toHaveBeenCalledWith(mockError, {
|
||||
title: 'Failed to import saved objects',
|
||||
toastMessage: 'Template not found!!',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { SavedObject, SavedObjectAttributes } from '@kbn/core/public';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
import { bulkCreatePrebuiltSavedObjects } from '../apis/bulk_create_prebuilt_saved_objects';
|
||||
import { IMPORT_SAVED_OBJECTS_FAILURE, IMPORT_SAVED_OBJECTS_SUCCESS } from '../translations';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
|
||||
interface ImportSavedObjectsButtonProps {
|
||||
hide: boolean;
|
||||
onSuccessCallback?: (result: Array<SavedObject<SavedObjectAttributes>>) => void;
|
||||
successLink?: string | undefined;
|
||||
successTitle: string;
|
||||
templateName: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ImportSavedObjectsButtonComponent: React.FC<ImportSavedObjectsButtonProps> = ({
|
||||
hide,
|
||||
onSuccessCallback,
|
||||
successLink,
|
||||
successTitle,
|
||||
templateName,
|
||||
title,
|
||||
}) => {
|
||||
const {
|
||||
services: { http },
|
||||
} = useKibana();
|
||||
|
||||
const { addSuccess, addError } = useAppToasts();
|
||||
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>();
|
||||
|
||||
const importPrebuiltSavedObjects = useCallback(async () => {
|
||||
setStatus('loading');
|
||||
|
||||
try {
|
||||
const res = await bulkCreatePrebuiltSavedObjects(http, {
|
||||
templateName,
|
||||
});
|
||||
const savedObjects: Array<SavedObject<SavedObjectAttributes>> = getOr(
|
||||
[],
|
||||
['saved_objects'],
|
||||
res
|
||||
);
|
||||
setStatus('success');
|
||||
|
||||
addSuccess({
|
||||
title: IMPORT_SAVED_OBJECTS_SUCCESS(savedObjects.length),
|
||||
text: savedObjects.map((o) => o?.attributes?.title ?? o?.attributes?.name).join(', '),
|
||||
});
|
||||
|
||||
if (onSuccessCallback) {
|
||||
onSuccessCallback(savedObjects);
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus('error');
|
||||
addError(e, { title: IMPORT_SAVED_OBJECTS_FAILURE, toastMessage: e.message });
|
||||
}
|
||||
}, [addError, addSuccess, http, onSuccessCallback, templateName]);
|
||||
|
||||
if (successLink || status === 'success') {
|
||||
return (
|
||||
<EuiButton
|
||||
href={successLink}
|
||||
isDisabled={!successLink}
|
||||
data-test-subj="create-saved-object-success-button"
|
||||
target="_blank"
|
||||
>
|
||||
{successTitle}
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hide) {
|
||||
return (
|
||||
<EuiButton
|
||||
onClick={importPrebuiltSavedObjects}
|
||||
target="_blank"
|
||||
isDisabled={status === 'loading'}
|
||||
data-test-subj="create-saved-object-button"
|
||||
>
|
||||
{status === 'loading' && (
|
||||
<EuiLoadingSpinner data-test-subj="creating-saved-objects" size="m" />
|
||||
)}{' '}
|
||||
{title}
|
||||
</EuiButton>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const ImportSavedObjectsButton = React.memo(ImportSavedObjectsButtonComponent);
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const IMPORT_SAVED_OBJECTS_SUCCESS = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkCreateSavedObjects.bulkCreateSuccessTitle', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `{totalCount} {totalCount, plural, =1 {saved object} other {saved objects}} imported successfully`,
|
||||
});
|
||||
|
||||
export const IMPORT_SAVED_OBJECTS_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.bulkCreateSavedObjects.bulkCreateFailureTitle',
|
||||
{
|
||||
defaultMessage: `Failed to import saved objects`,
|
||||
}
|
||||
);
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { OpenInDevConsoleButton } from '.';
|
||||
import { TestProviders } from '../../mock';
|
||||
|
||||
describe('OpenInDevConsoleButton', () => {
|
||||
it('renders open in dev console link', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<OpenInDevConsoleButton
|
||||
enableButton={true}
|
||||
loadFromUrl="http://localhost:1234/test"
|
||||
tooltipContent="popover"
|
||||
title="open in dev console"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(screen.getByTestId('open-in-console-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a disabled button', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<OpenInDevConsoleButton
|
||||
enableButton={false}
|
||||
loadFromUrl="http://localhost:1234/test"
|
||||
title="open in dev console"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(screen.getByTestId('disabled-open-in-console-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a disabled button with popover', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<OpenInDevConsoleButton
|
||||
enableButton={false}
|
||||
loadFromUrl="http://localhost:1234/test"
|
||||
title="open in dev console"
|
||||
tooltipContent="tooltipContent"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(screen.getByTestId('disabled-open-in-console-button-with-tooltip'));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId('disabled-open-in-console-button-with-tooltip')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiButton, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
interface OpenInDevConsoleButtonProps {
|
||||
enableButton: boolean;
|
||||
loadFromUrl: string;
|
||||
tooltipContent?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const OpenInDevConsoleButtonComponent: React.FC<OpenInDevConsoleButtonProps> = ({
|
||||
enableButton,
|
||||
loadFromUrl,
|
||||
tooltipContent,
|
||||
title,
|
||||
}) => {
|
||||
const href = `/app/dev_tools#/console?load_from=${loadFromUrl}`;
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
{enableButton ? (
|
||||
<EuiButton
|
||||
href={href}
|
||||
color="warning"
|
||||
target="_self"
|
||||
isDisabled={false}
|
||||
data-test-subj="open-in-console-button"
|
||||
>
|
||||
{title}
|
||||
</EuiButton>
|
||||
) : tooltipContent ? (
|
||||
<EuiToolTip content={tooltipContent}>
|
||||
<EuiButton
|
||||
href={href}
|
||||
color="warning"
|
||||
isDisabled={true}
|
||||
data-test-subj="disabled-open-in-console-button-with-tooltip"
|
||||
>
|
||||
{title}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<EuiButton
|
||||
href={href}
|
||||
color="warning"
|
||||
isDisabled={true}
|
||||
data-test-subj="disabled-open-in-console-button"
|
||||
>
|
||||
{title}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const OpenInDevConsoleButton = React.memo(OpenInDevConsoleButtonComponent);
|
||||
OpenInDevConsoleButton.displayName = 'OpenInDevConsoleButton';
|
|
@ -19,8 +19,8 @@ export const useDashboardButtonHref = ({
|
|||
from,
|
||||
title,
|
||||
}: {
|
||||
to: string;
|
||||
from: string;
|
||||
to?: string;
|
||||
from?: string;
|
||||
title: string;
|
||||
}) => {
|
||||
const {
|
||||
|
@ -39,7 +39,7 @@ export const useDashboardButtonHref = ({
|
|||
id?: string;
|
||||
}>;
|
||||
}) => {
|
||||
if (DashboardsSO?.savedObjects?.length) {
|
||||
if (DashboardsSO?.savedObjects?.length && to && from) {
|
||||
const dashboardUrl = await dashboard?.locator?.getUrl({
|
||||
dashboardId: DashboardsSO.savedObjects[0].id,
|
||||
timeRange: {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
DETECTION_ENGINE_INDEX_URL,
|
||||
DETECTION_ENGINE_PRIVILEGES_URL,
|
||||
ALERTS_AS_DATA_FIND_URL,
|
||||
DETECTION_ENGINE_ALERTS_INDEX_URL,
|
||||
} from '../../../../../common/constants';
|
||||
import { HOST_METADATA_GET_ROUTE } from '../../../../../common/endpoint/constants';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
|
@ -25,6 +26,7 @@ import type {
|
|||
AlertsIndex,
|
||||
UpdateAlertStatusProps,
|
||||
CasesFromAlertsResponse,
|
||||
CheckSignalIndex,
|
||||
} from './types';
|
||||
import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint_isolation';
|
||||
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
|
||||
|
@ -106,6 +108,19 @@ export const getSignalIndex = async ({ signal }: BasicSignals): Promise<AlertsIn
|
|||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Check Signal Index
|
||||
*
|
||||
* @param signal AbortSignal for cancelling request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const checkSignalIndex = async ({ signal }: BasicSignals): Promise<CheckSignalIndex> =>
|
||||
KibanaServices.get().http.fetch<CheckSignalIndex>(DETECTION_ENGINE_ALERTS_INDEX_URL, {
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Get User Privileges
|
||||
*
|
||||
|
|
|
@ -49,6 +49,12 @@ export interface AlertsIndex {
|
|||
index_mapping_outdated: boolean;
|
||||
}
|
||||
|
||||
export interface CheckSignalIndex {
|
||||
name: string;
|
||||
index_mapping_outdated: boolean;
|
||||
indexExists: boolean;
|
||||
}
|
||||
|
||||
export type CasesFromAlertsResponse = Array<{ id: string; title: string }>;
|
||||
|
||||
export interface Privilege {
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { isSecurityAppError } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { checkSignalIndex } from './api';
|
||||
import * as i18n from './translations';
|
||||
import { useAlertsPrivileges } from './use_alerts_privileges';
|
||||
|
||||
interface ReturnSignalIndex {
|
||||
loading: boolean;
|
||||
signalIndexExists: boolean | null;
|
||||
signalIndexName: string | null;
|
||||
signalIndexMappingOutdated: boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing signal index
|
||||
*
|
||||
*
|
||||
*/
|
||||
export const useChcekSignalIndex = (): ReturnSignalIndex => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [signalIndex, setSignalIndex] = useState<Omit<ReturnSignalIndex, 'loading'>>({
|
||||
signalIndexExists: null,
|
||||
signalIndexName: null,
|
||||
signalIndexMappingOutdated: null,
|
||||
});
|
||||
const { addError } = useAppToasts();
|
||||
const { hasIndexRead } = useAlertsPrivileges();
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const signal = await checkSignalIndex({ signal: abortCtrl.signal });
|
||||
|
||||
if (isSubscribed && signal != null) {
|
||||
setSignalIndex({
|
||||
signalIndexExists: signal?.indexExists,
|
||||
signalIndexName: signal.name,
|
||||
signalIndexMappingOutdated: signal.index_mapping_outdated,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setSignalIndex({
|
||||
signalIndexExists: false,
|
||||
signalIndexName: null,
|
||||
signalIndexMappingOutdated: null,
|
||||
});
|
||||
if (isSecurityAppError(error) && error.body.status_code !== 404) {
|
||||
addError(error, { title: i18n.SIGNAL_GET_NAME_FAILURE });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (hasIndexRead) {
|
||||
fetchData();
|
||||
} else {
|
||||
// Skip data fetching as the current user doesn't have enough priviliges.
|
||||
// Attempt to get the signal index will result in 500 error.
|
||||
setLoading(false);
|
||||
}
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [addError, hasIndexRead]);
|
||||
|
||||
return { loading, ...signalIndex };
|
||||
};
|
|
@ -37,4 +37,19 @@ describe('DisabledLinkPanel', () => {
|
|||
);
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', defaultProps.docLink);
|
||||
});
|
||||
|
||||
it('renders more buttons', () => {
|
||||
const moreButtons: React.ReactElement = <div data-test-subj="more-button">{'More Button'}</div>;
|
||||
const testProps = {
|
||||
...defaultProps,
|
||||
moreButtons,
|
||||
};
|
||||
render(
|
||||
<TestProviders>
|
||||
<DisabledLinkPanel {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('more-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,18 +6,21 @@
|
|||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { InnerLinkPanel } from './inner_link_panel';
|
||||
import type { LinkPanelListItem, LinkPanelViewProps } from './types';
|
||||
import { LinkButton } from '../../../common/components/links';
|
||||
|
||||
interface DisabledLinkPanelProps {
|
||||
bodyCopy: string;
|
||||
buttonCopy: string;
|
||||
buttonCopy?: string;
|
||||
dataTestSubjPrefix: string;
|
||||
docLink: string;
|
||||
docLink?: string;
|
||||
learnMoreUrl?: string;
|
||||
LinkPanelViewComponent: React.ComponentType<LinkPanelViewProps>;
|
||||
listItems: LinkPanelListItem[];
|
||||
moreButtons?: React.ReactElement;
|
||||
titleCopy: string;
|
||||
}
|
||||
|
||||
|
@ -26,32 +29,44 @@ const DisabledLinkPanelComponent: React.FC<DisabledLinkPanelProps> = ({
|
|||
buttonCopy,
|
||||
dataTestSubjPrefix,
|
||||
docLink,
|
||||
learnMoreUrl,
|
||||
LinkPanelViewComponent,
|
||||
listItems,
|
||||
moreButtons,
|
||||
titleCopy,
|
||||
}) => (
|
||||
<LinkPanelViewComponent
|
||||
listItems={listItems}
|
||||
splitPanel={
|
||||
<InnerLinkPanel
|
||||
body={bodyCopy}
|
||||
button={
|
||||
<EuiButton
|
||||
href={docLink}
|
||||
color="warning"
|
||||
target="_blank"
|
||||
data-test-subj={`${dataTestSubjPrefix}-enable-module-button`}
|
||||
>
|
||||
{buttonCopy}
|
||||
</EuiButton>
|
||||
}
|
||||
color="warning"
|
||||
dataTestSubj={`${dataTestSubjPrefix}-inner-panel-danger`}
|
||||
title={titleCopy}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}) => {
|
||||
return (
|
||||
<LinkPanelViewComponent
|
||||
listItems={listItems}
|
||||
splitPanel={
|
||||
<InnerLinkPanel
|
||||
body={bodyCopy}
|
||||
button={
|
||||
<EuiFlexGroup>
|
||||
{buttonCopy && docLink && (
|
||||
<EuiFlexItem>
|
||||
<LinkButton
|
||||
color="warning"
|
||||
href={docLink}
|
||||
target="_blank"
|
||||
data-test-subj={`${dataTestSubjPrefix}-enable-module-button`}
|
||||
>
|
||||
{buttonCopy}
|
||||
</LinkButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{moreButtons && moreButtons}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
color="warning"
|
||||
dataTestSubj={`${dataTestSubjPrefix}-inner-panel-danger`}
|
||||
learnMoreLink={learnMoreUrl}
|
||||
title={titleCopy}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DisabledLinkPanel = memo(DisabledLinkPanelComponent);
|
||||
DisabledLinkPanel.displayName = 'DisabledLinkPanel';
|
||||
|
|
|
@ -30,4 +30,13 @@ describe('InnerLinkPanel', () => {
|
|||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('inner-link-panel-title')).toHaveTextContent(defaultProps.title);
|
||||
});
|
||||
|
||||
it('renders learn more link', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<InnerLinkPanel color="warning" {...defaultProps} learnMoreLink="/learn_more" />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(screen.getByTestId('custom_test_subj-learn-more')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSplitPanel, EuiText } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSplitPanel, EuiText } from '@elastic/eui';
|
||||
import { LEARN_MORE } from '../overview_risky_host_links/translations';
|
||||
|
||||
const ButtonContainer = styled(EuiFlexGroup)`
|
||||
padding: ${({ theme }) => theme.eui.euiSizeS};
|
||||
|
@ -35,12 +36,14 @@ export const InnerLinkPanel = ({
|
|||
button,
|
||||
color,
|
||||
dataTestSubj,
|
||||
learnMoreLink,
|
||||
title,
|
||||
}: {
|
||||
body: string;
|
||||
button?: JSX.Element;
|
||||
color: 'primary' | 'warning';
|
||||
dataTestSubj: string;
|
||||
learnMoreLink?: string;
|
||||
title: string;
|
||||
}) => (
|
||||
<PanelContainer grow={false} color={color}>
|
||||
|
@ -54,7 +57,19 @@ export const InnerLinkPanel = ({
|
|||
</Title>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{body}
|
||||
<p>
|
||||
{body}{' '}
|
||||
{learnMoreLink && (
|
||||
<EuiLink
|
||||
href={learnMoreLink}
|
||||
target="_blank"
|
||||
data-test-subj={`${dataTestSubj}-learn-more`}
|
||||
external
|
||||
>
|
||||
{LEARN_MORE}
|
||||
</EuiLink>
|
||||
)}
|
||||
</p>
|
||||
</EuiFlexItem>
|
||||
{button && (
|
||||
<ButtonContainer>
|
||||
|
|
|
@ -15,11 +15,13 @@ export interface LinkPanelListItem {
|
|||
}
|
||||
|
||||
export interface LinkPanelViewProps {
|
||||
allIntegrationsInstalled?: boolean;
|
||||
buttonHref?: string;
|
||||
from?: string;
|
||||
isInspectEnabled?: boolean;
|
||||
isPluginDisabled?: boolean;
|
||||
listItems: LinkPanelListItem[];
|
||||
splitPanel?: JSX.Element;
|
||||
to?: string;
|
||||
totalCount?: number;
|
||||
allIntegrationsInstalled?: boolean;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
export const DANGER_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.overview.ctiDashboardDangerPanelTitle',
|
||||
{
|
||||
defaultMessage: 'No threat intel data available to display',
|
||||
defaultMessage: 'No threat intelligence data',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -107,6 +107,6 @@ describe('RiskyHostLinks', () => {
|
|||
</Provider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('risky-hosts-enable-module-button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('disabled-open-in-console-button-with-tooltip')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -47,7 +47,8 @@ describe('RiskyHostsModule', () => {
|
|||
);
|
||||
|
||||
expect(screen.getByTestId('risky-hosts-dashboard-links')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('risky-hosts-view-dashboard-button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('risky-hosts-enable-module-button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('risky-hosts-inner-panel-danger-learn-more')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('disabled-open-in-console-button-with-tooltip')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,27 +5,51 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { DisabledLinkPanel } from '../link_panel/disabled_link_panel';
|
||||
import { RiskyHostsPanelView } from './risky_hosts_panel_view';
|
||||
import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module';
|
||||
import { ENABLE_VIA_DEV_TOOLS } from './translations';
|
||||
import { devToolPrebuiltContentUrl } from '../../../../common/constants';
|
||||
import { OpenInDevConsoleButton } from '../../../common/components/open_in_dev_console';
|
||||
import { useChcekSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_check_signal_index';
|
||||
import type { LinkPanelListItem } from '../link_panel';
|
||||
|
||||
export const RISKY_HOSTS_DOC_LINK =
|
||||
'https://www.github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md';
|
||||
|
||||
export const RiskyHostsDisabledModuleComponent = () => (
|
||||
<DisabledLinkPanel
|
||||
bodyCopy={i18n.DANGER_BODY}
|
||||
buttonCopy={i18n.DANGER_BUTTON}
|
||||
dataTestSubjPrefix="risky-hosts"
|
||||
docLink={RISKY_HOSTS_DOC_LINK}
|
||||
listItems={[]}
|
||||
titleCopy={i18n.DANGER_TITLE}
|
||||
LinkPanelViewComponent={RiskyHostsPanelView}
|
||||
/>
|
||||
);
|
||||
const emptyList: LinkPanelListItem[] = [];
|
||||
|
||||
export const RiskyHostsDisabledModuleComponent = () => {
|
||||
const hostRiskScoreConsoleId = 'enable_host_risk_score';
|
||||
const loadFromUrl = useMemo(() => {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
const port = window.location.port;
|
||||
return `${protocol}//${hostname}:${port}${devToolPrebuiltContentUrl(hostRiskScoreConsoleId)}`;
|
||||
}, []);
|
||||
const { signalIndexExists } = useChcekSignalIndex();
|
||||
return (
|
||||
<DisabledLinkPanel
|
||||
bodyCopy={i18n.DANGER_BODY}
|
||||
dataTestSubjPrefix="risky-hosts"
|
||||
learnMoreUrl={RISKY_HOSTS_DOC_LINK}
|
||||
listItems={emptyList}
|
||||
titleCopy={i18n.DANGER_TITLE}
|
||||
LinkPanelViewComponent={RiskyHostsPanelView}
|
||||
moreButtons={
|
||||
<OpenInDevConsoleButton
|
||||
loadFromUrl={loadFromUrl}
|
||||
enableButton={!!signalIndexExists}
|
||||
title={ENABLE_VIA_DEV_TOOLS}
|
||||
tooltipContent={i18n.ENABLE_RISK_SCORE_POPOVER}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RiskyHostsDisabledModule = React.memo(RiskyHostsDisabledModuleComponent);
|
||||
RiskyHostsEnabledModule.displayName = 'RiskyHostsDisabledModule';
|
||||
|
|
|
@ -75,5 +75,6 @@ describe('RiskyHostsEnabledModule', () => {
|
|||
</Provider>
|
||||
);
|
||||
expect(screen.getByTestId('risky-hosts-dashboard-links')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('create-saved-object-success-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,8 +10,6 @@ import { RiskyHostsPanelView } from './risky_hosts_panel_view';
|
|||
import type { LinkPanelListItem } from '../link_panel';
|
||||
import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links';
|
||||
import type { HostsRiskScore } from '../../../../common/search_strategy';
|
||||
import { useDashboardButtonHref } from '../../../common/hooks/use_dashboard_button_href';
|
||||
import { RISKY_HOSTS_DASHBOARD_TITLE } from '../../../hosts/pages/navigation/constants';
|
||||
|
||||
const getListItemsFromHits = (items: HostsRiskScore[]): LinkPanelListItem[] => {
|
||||
return items.map(({ host, risk_stats: riskStats, risk: copy }) => ({
|
||||
|
@ -28,15 +26,15 @@ const RiskyHostsEnabledModuleComponent: React.FC<{
|
|||
to: string;
|
||||
}> = ({ hostRiskScore, to, from }) => {
|
||||
const listItems = useMemo(() => getListItemsFromHits(hostRiskScore || []), [hostRiskScore]);
|
||||
const { buttonHref } = useDashboardButtonHref({ to, from, title: RISKY_HOSTS_DASHBOARD_TITLE });
|
||||
const { listItemsWithLinks } = useRiskyHostsDashboardLinks(to, from, listItems);
|
||||
|
||||
return (
|
||||
<RiskyHostsPanelView
|
||||
buttonHref={buttonHref}
|
||||
isInspectEnabled
|
||||
listItems={listItemsWithLinks}
|
||||
totalCount={listItems.length}
|
||||
to={to}
|
||||
from={from}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { State } from '../../../common/store';
|
||||
import { createStore } from '../../../common/store';
|
||||
import {
|
||||
createSecuritySolutionStorageMock,
|
||||
kibanaObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
TestProviders,
|
||||
} from '../../../common/mock';
|
||||
|
||||
import { RiskyHostsPanelView } from './risky_hosts_panel_view';
|
||||
import { useDashboardButtonHref } from '../../../common/hooks/use_dashboard_button_href';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
jest.mock('../../../common/hooks/use_dashboard_button_href');
|
||||
const useRiskyHostsDashboardButtonHrefMock = useDashboardButtonHref as jest.Mock;
|
||||
useRiskyHostsDashboardButtonHrefMock.mockReturnValue({ buttonHref: '/test' });
|
||||
|
||||
describe('RiskyHostsPanelView', () => {
|
||||
const state: State = mockGlobalState;
|
||||
|
||||
beforeEach(() => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
<RiskyHostsPanelView
|
||||
isInspectEnabled={true}
|
||||
listItems={[{ title: 'a', count: 1, path: '/test' }]}
|
||||
totalCount={1}
|
||||
to="now"
|
||||
from="now-30d"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders title', () => {
|
||||
expect(screen.getByTestId('header-section-title')).toHaveTextContent(
|
||||
'Current host risk scores'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders host number', () => {
|
||||
expect(screen.getByTestId('header-panel-subtitle')).toHaveTextContent('Showing: 1 host');
|
||||
});
|
||||
|
||||
it('renders view dashboard button', () => {
|
||||
expect(screen.getByTestId('create-saved-object-success-button')).toHaveAttribute(
|
||||
'href',
|
||||
'/test'
|
||||
);
|
||||
expect(screen.getByTestId('create-saved-object-success-button')).toHaveTextContent(
|
||||
'View dashboard'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -5,19 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import type { EuiTableFieldDataColumnType } from '@elastic/eui';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { SavedObject, SavedObjectAttributes } from '@kbn/core/types';
|
||||
import type { LinkPanelListItem } from '../link_panel';
|
||||
import { InnerLinkPanel, LinkPanel } from '../link_panel';
|
||||
import type { LinkPanelViewProps } from '../link_panel/types';
|
||||
import { Link } from '../link_panel/link';
|
||||
import * as i18n from './translations';
|
||||
import { VIEW_DASHBOARD } from '../overview_cti_links/translations';
|
||||
import { NavigateToHost } from './navigate_to_host';
|
||||
import { HostRiskScoreQueryId } from '../../../risk_score/containers';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { RISKY_HOSTS_DASHBOARD_TITLE } from '../../../hosts/pages/navigation/constants';
|
||||
import { useDashboardButtonHref } from '../../../common/hooks/use_dashboard_button_href';
|
||||
import { ImportSavedObjectsButton } from '../../../common/components/create_prebuilt_saved_objects/components/bulk_create_button';
|
||||
import { VIEW_DASHBOARD } from '../overview_cti_links/translations';
|
||||
|
||||
const columns: Array<EuiTableFieldDataColumnType<LinkPanelListItem>> = [
|
||||
{
|
||||
|
@ -63,12 +67,13 @@ const warningPanel = (
|
|||
/>
|
||||
);
|
||||
|
||||
export const RiskyHostsPanelView: React.FC<LinkPanelViewProps> = ({
|
||||
buttonHref = '',
|
||||
const RiskyHostsPanelViewComponent: React.FC<LinkPanelViewProps> = ({
|
||||
isInspectEnabled,
|
||||
listItems,
|
||||
splitPanel,
|
||||
totalCount = 0,
|
||||
to,
|
||||
from,
|
||||
}) => {
|
||||
const splitPanelElement =
|
||||
typeof splitPanel === 'undefined'
|
||||
|
@ -76,21 +81,54 @@ export const RiskyHostsPanelView: React.FC<LinkPanelViewProps> = ({
|
|||
? warningPanel
|
||||
: undefined
|
||||
: splitPanel;
|
||||
|
||||
const [dashboardUrl, setDashboardUrl] = useState<string>();
|
||||
const { buttonHref } = useDashboardButtonHref({
|
||||
to,
|
||||
from,
|
||||
title: RISKY_HOSTS_DASHBOARD_TITLE,
|
||||
});
|
||||
const {
|
||||
services: { dashboard },
|
||||
} = useKibana();
|
||||
|
||||
const onImportDashboardSuccessCallback = useCallback(
|
||||
(response: Array<SavedObject<SavedObjectAttributes>>) => {
|
||||
const targetDashboard = response.find(
|
||||
(obj) => obj.type === 'dashboard' && obj?.attributes?.title === RISKY_HOSTS_DASHBOARD_TITLE
|
||||
);
|
||||
|
||||
const fetchDashboardUrl = (targetDashboardId: string | null | undefined) => {
|
||||
if (to && from && targetDashboardId) {
|
||||
const targetUrl = dashboard?.locator?.getRedirectUrl({
|
||||
dashboardId: targetDashboardId,
|
||||
timeRange: {
|
||||
to,
|
||||
from,
|
||||
},
|
||||
});
|
||||
|
||||
setDashboardUrl(targetUrl);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDashboardUrl(targetDashboard?.id);
|
||||
},
|
||||
[dashboard?.locator, from, to]
|
||||
);
|
||||
|
||||
return (
|
||||
<LinkPanel
|
||||
{...{
|
||||
button: useMemo(
|
||||
() => (
|
||||
<EuiButton
|
||||
href={buttonHref}
|
||||
isDisabled={!buttonHref}
|
||||
data-test-subj="risky-hosts-view-dashboard-button"
|
||||
target="_blank"
|
||||
>
|
||||
{VIEW_DASHBOARD}
|
||||
</EuiButton>
|
||||
),
|
||||
[buttonHref]
|
||||
button: (
|
||||
<ImportSavedObjectsButton
|
||||
hide={listItems == null || listItems.length === 0}
|
||||
onSuccessCallback={onImportDashboardSuccessCallback}
|
||||
successLink={buttonHref || dashboardUrl}
|
||||
successTitle={VIEW_DASHBOARD}
|
||||
templateName="hostRiskScoreDashboards"
|
||||
title={i18n.IMPORT_DASHBOARD}
|
||||
/>
|
||||
),
|
||||
columns,
|
||||
dataTestSubj: 'risky-hosts-dashboard-links',
|
||||
|
@ -115,3 +153,5 @@ export const RiskyHostsPanelView: React.FC<LinkPanelViewProps> = ({
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RiskyHostsPanelView = React.memo(RiskyHostsPanelViewComponent);
|
||||
|
|
|
@ -24,22 +24,28 @@ export const WARNING_BODY = i18n.translate(
|
|||
export const DANGER_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.overview.riskyHostsDashboardDangerPanelTitle',
|
||||
{
|
||||
defaultMessage: 'No host risk score data to display',
|
||||
defaultMessage: 'No host risk score data',
|
||||
}
|
||||
);
|
||||
|
||||
export const DANGER_BODY = i18n.translate(
|
||||
'xpack.securitySolution.overview.riskyHostsDashboardEnableThreatIntel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Please enable the host risk score module in order to view the list of risky hosts.',
|
||||
defaultMessage: 'You must enable the host risk module to view risky hosts.',
|
||||
}
|
||||
);
|
||||
|
||||
export const DANGER_BUTTON = i18n.translate(
|
||||
export const ENABLE_VIA_DEV_TOOLS = i18n.translate(
|
||||
'xpack.securitySolution.overview.riskyHostsDashboardDangerPanelButton',
|
||||
{
|
||||
defaultMessage: 'Enable Risk Score',
|
||||
defaultMessage: 'Enable via Dev Tools',
|
||||
}
|
||||
);
|
||||
|
||||
export const LEARN_MORE = i18n.translate(
|
||||
'xpack.securitySolution.overview.riskyHostsDashboardLearnMoreButton',
|
||||
{
|
||||
defaultMessage: 'Learn More',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -53,3 +59,14 @@ export const PANEL_TITLE = i18n.translate(
|
|||
defaultMessage: 'Current host risk scores',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_DASHBOARD = i18n.translate('xpack.securitySolution.overview.importDasboard', {
|
||||
defaultMessage: 'Import dashboard',
|
||||
});
|
||||
|
||||
export const ENABLE_RISK_SCORE_POPOVER = i18n.translate(
|
||||
'xpack.securitySolution.overview.enableRiskScorePopoverTitle',
|
||||
{
|
||||
defaultMessage: 'Alerts need to be available before enabling module',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { transformError, getIndexExists } from '@kbn/securitysolution-es-utils';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../../types';
|
||||
import { DETECTION_ENGINE_ALERTS_INDEX_URL } from '../../../../../common/constants';
|
||||
|
||||
import { buildSiemResponse } from '../utils';
|
||||
|
||||
export const readAlertsIndexExistsRoute = (router: SecuritySolutionPluginRouter) => {
|
||||
router.get(
|
||||
{
|
||||
path: DETECTION_ENGINE_ALERTS_INDEX_URL,
|
||||
validate: false,
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
},
|
||||
async (context, _, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
try {
|
||||
const core = await context.core;
|
||||
const securitySolution = await context.securitySolution;
|
||||
const siemClient = securitySolution?.getAppClient();
|
||||
|
||||
if (!siemClient) {
|
||||
return siemResponse.error({ statusCode: 404 });
|
||||
}
|
||||
|
||||
const index = siemClient.getSignalsIndex();
|
||||
|
||||
const indexExists = await getIndexExists(core.elasticsearch.client.asInternalUser, index);
|
||||
return response.ok({
|
||||
body: {
|
||||
indexExists,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 consoleMappings = {
|
||||
enable_host_risk_score: 'enable_host_risk_score.console',
|
||||
};
|
|
@ -0,0 +1,262 @@
|
|||
# Click the run button of each step to enable the module
|
||||
# Upload scripts
|
||||
# 1. Script to assign risk level based on risk score
|
||||
PUT _scripts/ml_hostriskscore_levels_script
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "double risk_score = (def)ctx.getByPath(params.risk_score);\nif (risk_score < 20) {\n ctx['risk'] = 'Unknown'\n}\nelse if (risk_score >= 20 && risk_score < 40) {\n ctx['risk'] = 'Low'\n}\nelse if (risk_score >= 40 && risk_score < 70) {\n ctx['risk'] = 'Moderate'\n}\nelse if (risk_score >= 70 && risk_score < 90) {\n ctx['risk'] = 'High'\n}\nelse if (risk_score >= 90) {\n ctx['risk'] = 'Critical'\n}"
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Map script for the Host Risk Score transform
|
||||
PUT _scripts/ml_hostriskscore_map_script
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "// Get the host variant\nif (state.host_variant_set == false) {\n if (doc.containsKey(\"host.os.full\") && doc[\"host.os.full\"].size() != 0) {\n state.host_variant = doc[\"host.os.full\"].value;\n state.host_variant_set = true;\n }\n}\n// Aggregate all the tactics seen on the host\nif (doc.containsKey(\"signal.rule.threat.tactic.id\") && doc[\"signal.rule.threat.tactic.id\"].size() != 0) {\n state.tactic_ids.add(doc[\"signal.rule.threat.tactic.id\"].value);\n}\n// Get running sum of time-decayed risk score per rule name per shard\nString rule_name = doc[\"signal.rule.name\"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, [0.0,\"\",false]);\nint time_diff = (int)((System.currentTimeMillis() - doc[\"@timestamp\"].value.toInstant().toEpochMilli()) / (1000.0 * 60.0 * 60.0));\ndouble risk_derate = Math.min(1, Math.exp((params.lookback_time - time_diff) / params.time_decay_constant));\nstats[0] = Math.max(stats[0], doc[\"signal.rule.risk_score\"].value * risk_derate);\nif (stats[2] == false) {\n stats[1] = doc[\"kibana.alert.rule.uuid\"].value;\n stats[2] = true;\n}\nstate.rule_risk_stats.put(rule_name, stats);"
|
||||
}
|
||||
}
|
||||
|
||||
# 3. Reduce script for the Host Risk Score transform
|
||||
PUT _scripts/ml_hostriskscore_reduce_script
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "// Consolidating time decayed risks and tactics from across all shards\nMap total_risk_stats = new HashMap();\nString host_variant = new String();\ndef tactic_ids = new HashSet();\nfor (state in states) {\n for (key in state.rule_risk_stats.keySet()) {\n def rule_stats = state.rule_risk_stats.get(key);\n def stats = total_risk_stats.getOrDefault(key, [0.0,\"\",false]);\n stats[0] = Math.max(stats[0], rule_stats[0]);\n if (stats[2] == false) {\n stats[1] = rule_stats[1];\n stats[2] = true;\n } \n total_risk_stats.put(key, stats);\n }\n if (host_variant.length() == 0) {\n host_variant = state.host_variant;\n }\n tactic_ids.addAll(state.tactic_ids);\n}\n// Consolidating individual rule risks and arranging them in decreasing order\nList risks = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n risks.add(total_risk_stats[key][0])\n}\nCollections.sort(risks, Collections.reverseOrder());\n// Calculating total host risk score\ndouble total_risk = 0.0;\ndouble risk_cap = params.max_risk * params.zeta_constant;\nfor (int i=0;i<risks.length;i++) {\n total_risk += risks[i] / Math.pow((1+i), params.p);\n}\n// Normalizing the host risk score\ndouble total_norm_risk = 100 * total_risk / risk_cap;\nif (total_norm_risk < 40) {\n total_norm_risk = 2.125 * total_norm_risk;\n}\nelse if (total_norm_risk >= 40 && total_norm_risk < 50) {\n total_norm_risk = 85 + (total_norm_risk - 40);\n}\nelse {\n total_norm_risk = 95 + (total_norm_risk - 50) / 10;\n}\n// Calculating multipliers to the host risk score\ndouble risk_multiplier = 1.0;\nList multipliers = new ArrayList();\n// Add a multiplier if host is a server\nif (host_variant.toLowerCase().contains(\"server\")) {\n risk_multiplier *= params.server_multiplier;\n multipliers.add(\"Host is a server\");\n}\n// Add multipliers based on number and diversity of tactics seen on the host\nfor (String tactic : tactic_ids) {\n multipliers.add(\"Tactic \"+tactic);\n risk_multiplier *= 1 + params.tactic_base_multiplier * params.tactic_weights.getOrDefault(tactic, 0);\n}\n// Calculating final risk\ndouble final_risk = total_norm_risk;\nif (risk_multiplier > 1.0) {\n double prior_odds = (total_norm_risk) / (100 - total_norm_risk);\n double updated_odds = prior_odds * risk_multiplier; \n final_risk = 100 * updated_odds / (1 + updated_odds);\n}\n// Adding additional metadata\nList rule_stats = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n Map temp = new HashMap();\n temp[\"rule_name\"] = key;\n temp[\"rule_risk\"] = total_risk_stats[key][0];\n temp[\"rule_id\"] = total_risk_stats[key][1];\n rule_stats.add(temp);\n}\n\nreturn [\"risk_score\": final_risk, \"rule_risks\": rule_stats, \"risk_multipliers\": multipliers];"
|
||||
}
|
||||
}
|
||||
|
||||
# 4. Script to setup initial state for the Host Risk Score scripted metric aggregation
|
||||
PUT _scripts/ml_hostriskscore_init_script
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "state.rule_risk_stats = new HashMap();\nstate.host_variant_set = false;\nstate.host_variant = new String();\nstate.tactic_ids = new HashSet();"
|
||||
}
|
||||
}
|
||||
|
||||
# 5. Upload the ingest pipeline
|
||||
# Ingest pipeline to add ingest timestamp and risk level to documents
|
||||
PUT _ingest/pipeline/ml_hostriskscore_ingest_pipeline
|
||||
{
|
||||
"processors": [
|
||||
{
|
||||
"set": {
|
||||
"field": "ingest_timestamp",
|
||||
"value": "{{_ingest.timestamp}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fingerprint": {
|
||||
"fields": [
|
||||
"@timestamp",
|
||||
"_id"
|
||||
],
|
||||
"method": "SHA-256",
|
||||
"target_field": "_id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"id": "ml_hostriskscore_levels_script",
|
||||
"params": {
|
||||
"risk_score": "risk_stats.risk_score"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 6. Create mappings for the destination index of the Host Risk Score pivot transform
|
||||
PUT ml_host_risk_score_{{space_name}}
|
||||
{
|
||||
"mappings":{
|
||||
"properties":{
|
||||
"host.name":{
|
||||
"type":"keyword"
|
||||
},
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"ingest_timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"risk": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"risk_stats": {
|
||||
"properties": {
|
||||
"risk_score": {
|
||||
"type": "float"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 7. Upload the Host Risk Score pivot transform
|
||||
# This transform runs hourly and calculates a risk score and risk level for hosts in a Kibana space
|
||||
PUT _transform/ml_hostriskscore_pivot_transform_{{space_name}}
|
||||
{
|
||||
"dest": {
|
||||
"index": "ml_host_risk_score_{{space_name}}",
|
||||
"pipeline": "ml_hostriskscore_ingest_pipeline"
|
||||
},
|
||||
"frequency": "1h",
|
||||
"pivot": {
|
||||
"aggregations": {
|
||||
"@timestamp": {
|
||||
"max": {
|
||||
"field": "@timestamp"
|
||||
}
|
||||
},
|
||||
"risk_stats": {
|
||||
"scripted_metric": {
|
||||
"combine_script": "return state",
|
||||
"init_script": {
|
||||
"id": "ml_hostriskscore_init_script"
|
||||
},
|
||||
"map_script": {
|
||||
"id": "ml_hostriskscore_map_script"
|
||||
},
|
||||
"params": {
|
||||
"lookback_time": 72,
|
||||
"max_risk": 100,
|
||||
"p": 1.5,
|
||||
"server_multiplier": 1.5,
|
||||
"tactic_base_multiplier": 0.25,
|
||||
"tactic_weights": {
|
||||
"TA0001": 1,
|
||||
"TA0002": 2,
|
||||
"TA0003": 3,
|
||||
"TA0004": 4,
|
||||
"TA0005": 4,
|
||||
"TA0006": 4,
|
||||
"TA0007": 4,
|
||||
"TA0008": 5,
|
||||
"TA0009": 6,
|
||||
"TA0010": 7,
|
||||
"TA0011": 6,
|
||||
"TA0040": 8,
|
||||
"TA0042": 1,
|
||||
"TA0043": 1
|
||||
},
|
||||
"time_decay_constant": 6,
|
||||
"zeta_constant": 2.612
|
||||
},
|
||||
"reduce_script": {
|
||||
"id": "ml_hostriskscore_reduce_script"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"group_by": {
|
||||
"host.name": {
|
||||
"terms": {
|
||||
"field": "host.name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"index": [
|
||||
".alerts-security.alerts-{{space_name}}"
|
||||
],
|
||||
"query": {
|
||||
"bool": {
|
||||
"filter": [
|
||||
{
|
||||
"range": {
|
||||
"@timestamp": {
|
||||
"gte": "now-5d"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"sync": {
|
||||
"time": {
|
||||
"delay": "120s",
|
||||
"field": "@timestamp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 8. Start the pivot transform
|
||||
POST _transform/ml_hostriskscore_pivot_transform_{{space_name}}/_start
|
||||
|
||||
# 9. Create mappings for the destination index of the Host Risk Score latest transform
|
||||
PUT ml_host_risk_score_latest_{{space_name}}
|
||||
{
|
||||
"mappings":{
|
||||
"properties":{
|
||||
"host.name":{
|
||||
"type":"keyword"
|
||||
},
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"ingest_timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"risk": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"risk_stats": {
|
||||
"properties": {
|
||||
"risk_score": {
|
||||
"type": "float"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 10. Upload the latest transform
|
||||
# This transform gets the latest risk information about hosts in a Kibana space
|
||||
PUT _transform/ml_hostriskscore_latest_transform_{{space_name}}
|
||||
{
|
||||
"dest": {
|
||||
"index": "ml_host_risk_score_latest_{{space_name}}"
|
||||
},
|
||||
"frequency": "1h",
|
||||
"latest": {
|
||||
"sort": "@timestamp",
|
||||
"unique_key": [
|
||||
"host.name"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"index": [
|
||||
"ml_host_risk_score_{{space_name}}"
|
||||
]
|
||||
},
|
||||
"sync": {
|
||||
"time": {
|
||||
"delay": "2s",
|
||||
"field": "ingest_timestamp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 11. Start the latest transform
|
||||
POST _transform/ml_hostriskscore_latest_transform_{{space_name}}/_start
|
||||
|
||||
# Hint: If you don't see data after running any of the transforms, stop and restart the transforms
|
||||
# Stop the pivot transform
|
||||
POST _transform/ml_hostriskscore_pivot_transform_{{space_name}}/_stop
|
||||
|
||||
# Start the pivot transform
|
||||
POST _transform/ml_hostriskscore_pivot_transform_{{space_name}}/_start
|
||||
|
||||
# Stop the latest transform
|
||||
POST _transform/ml_hostriskscore_latest_transform_{{space_name}}/_stop
|
||||
|
||||
# Start the latest transform
|
||||
POST _transform/ml_hostriskscore_latest_transform_{{space_name}}/_start
|
|
@ -0,0 +1,267 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`readPrebuiltDevToolContentRoute should read content from enable_host_risk_score template 1`] = `
|
||||
"# Click the run button of each step to enable the module
|
||||
# Upload scripts
|
||||
# 1. Script to assign risk level based on risk score
|
||||
PUT _scripts/ml_hostriskscore_levels_script
|
||||
{
|
||||
\\"script\\": {
|
||||
\\"lang\\": \\"painless\\",
|
||||
\\"source\\": \\"double risk_score = (def)ctx.getByPath(params.risk_score);\\\\nif (risk_score < 20) {\\\\n ctx['risk'] = 'Unknown'\\\\n}\\\\nelse if (risk_score >= 20 && risk_score < 40) {\\\\n ctx['risk'] = 'Low'\\\\n}\\\\nelse if (risk_score >= 40 && risk_score < 70) {\\\\n ctx['risk'] = 'Moderate'\\\\n}\\\\nelse if (risk_score >= 70 && risk_score < 90) {\\\\n ctx['risk'] = 'High'\\\\n}\\\\nelse if (risk_score >= 90) {\\\\n ctx['risk'] = 'Critical'\\\\n}\\"
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Map script for the Host Risk Score transform
|
||||
PUT _scripts/ml_hostriskscore_map_script
|
||||
{
|
||||
\\"script\\": {
|
||||
\\"lang\\": \\"painless\\",
|
||||
\\"source\\": \\"// Get the host variant\\\\nif (state.host_variant_set == false) {\\\\n if (doc.containsKey(\\\\\\"host.os.full\\\\\\") && doc[\\\\\\"host.os.full\\\\\\"].size() != 0) {\\\\n state.host_variant = doc[\\\\\\"host.os.full\\\\\\"].value;\\\\n state.host_variant_set = true;\\\\n }\\\\n}\\\\n// Aggregate all the tactics seen on the host\\\\nif (doc.containsKey(\\\\\\"signal.rule.threat.tactic.id\\\\\\") && doc[\\\\\\"signal.rule.threat.tactic.id\\\\\\"].size() != 0) {\\\\n state.tactic_ids.add(doc[\\\\\\"signal.rule.threat.tactic.id\\\\\\"].value);\\\\n}\\\\n// Get running sum of time-decayed risk score per rule name per shard\\\\nString rule_name = doc[\\\\\\"signal.rule.name\\\\\\"].value;\\\\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, [0.0,\\\\\\"\\\\\\",false]);\\\\nint time_diff = (int)((System.currentTimeMillis() - doc[\\\\\\"@timestamp\\\\\\"].value.toInstant().toEpochMilli()) / (1000.0 * 60.0 * 60.0));\\\\ndouble risk_derate = Math.min(1, Math.exp((params.lookback_time - time_diff) / params.time_decay_constant));\\\\nstats[0] = Math.max(stats[0], doc[\\\\\\"signal.rule.risk_score\\\\\\"].value * risk_derate);\\\\nif (stats[2] == false) {\\\\n stats[1] = doc[\\\\\\"kibana.alert.rule.uuid\\\\\\"].value;\\\\n stats[2] = true;\\\\n}\\\\nstate.rule_risk_stats.put(rule_name, stats);\\"
|
||||
}
|
||||
}
|
||||
|
||||
# 3. Reduce script for the Host Risk Score transform
|
||||
PUT _scripts/ml_hostriskscore_reduce_script
|
||||
{
|
||||
\\"script\\": {
|
||||
\\"lang\\": \\"painless\\",
|
||||
\\"source\\": \\"// Consolidating time decayed risks and tactics from across all shards\\\\nMap total_risk_stats = new HashMap();\\\\nString host_variant = new String();\\\\ndef tactic_ids = new HashSet();\\\\nfor (state in states) {\\\\n for (key in state.rule_risk_stats.keySet()) {\\\\n def rule_stats = state.rule_risk_stats.get(key);\\\\n def stats = total_risk_stats.getOrDefault(key, [0.0,\\\\\\"\\\\\\",false]);\\\\n stats[0] = Math.max(stats[0], rule_stats[0]);\\\\n if (stats[2] == false) {\\\\n stats[1] = rule_stats[1];\\\\n stats[2] = true;\\\\n } \\\\n total_risk_stats.put(key, stats);\\\\n }\\\\n if (host_variant.length() == 0) {\\\\n host_variant = state.host_variant;\\\\n }\\\\n tactic_ids.addAll(state.tactic_ids);\\\\n}\\\\n// Consolidating individual rule risks and arranging them in decreasing order\\\\nList risks = new ArrayList();\\\\nfor (key in total_risk_stats.keySet()) {\\\\n risks.add(total_risk_stats[key][0])\\\\n}\\\\nCollections.sort(risks, Collections.reverseOrder());\\\\n// Calculating total host risk score\\\\ndouble total_risk = 0.0;\\\\ndouble risk_cap = params.max_risk * params.zeta_constant;\\\\nfor (int i=0;i<risks.length;i++) {\\\\n total_risk += risks[i] / Math.pow((1+i), params.p);\\\\n}\\\\n// Normalizing the host risk score\\\\ndouble total_norm_risk = 100 * total_risk / risk_cap;\\\\nif (total_norm_risk < 40) {\\\\n total_norm_risk = 2.125 * total_norm_risk;\\\\n}\\\\nelse if (total_norm_risk >= 40 && total_norm_risk < 50) {\\\\n total_norm_risk = 85 + (total_norm_risk - 40);\\\\n}\\\\nelse {\\\\n total_norm_risk = 95 + (total_norm_risk - 50) / 10;\\\\n}\\\\n// Calculating multipliers to the host risk score\\\\ndouble risk_multiplier = 1.0;\\\\nList multipliers = new ArrayList();\\\\n// Add a multiplier if host is a server\\\\nif (host_variant.toLowerCase().contains(\\\\\\"server\\\\\\")) {\\\\n risk_multiplier *= params.server_multiplier;\\\\n multipliers.add(\\\\\\"Host is a server\\\\\\");\\\\n}\\\\n// Add multipliers based on number and diversity of tactics seen on the host\\\\nfor (String tactic : tactic_ids) {\\\\n multipliers.add(\\\\\\"Tactic \\\\\\"+tactic);\\\\n risk_multiplier *= 1 + params.tactic_base_multiplier * params.tactic_weights.getOrDefault(tactic, 0);\\\\n}\\\\n// Calculating final risk\\\\ndouble final_risk = total_norm_risk;\\\\nif (risk_multiplier > 1.0) {\\\\n double prior_odds = (total_norm_risk) / (100 - total_norm_risk);\\\\n double updated_odds = prior_odds * risk_multiplier; \\\\n final_risk = 100 * updated_odds / (1 + updated_odds);\\\\n}\\\\n// Adding additional metadata\\\\nList rule_stats = new ArrayList();\\\\nfor (key in total_risk_stats.keySet()) {\\\\n Map temp = new HashMap();\\\\n temp[\\\\\\"rule_name\\\\\\"] = key;\\\\n temp[\\\\\\"rule_risk\\\\\\"] = total_risk_stats[key][0];\\\\n temp[\\\\\\"rule_id\\\\\\"] = total_risk_stats[key][1];\\\\n rule_stats.add(temp);\\\\n}\\\\n\\\\nreturn [\\\\\\"risk_score\\\\\\": final_risk, \\\\\\"rule_risks\\\\\\": rule_stats, \\\\\\"risk_multipliers\\\\\\": multipliers];\\"
|
||||
}
|
||||
}
|
||||
|
||||
# 4. Script to setup initial state for the Host Risk Score scripted metric aggregation
|
||||
PUT _scripts/ml_hostriskscore_init_script
|
||||
{
|
||||
\\"script\\": {
|
||||
\\"lang\\": \\"painless\\",
|
||||
\\"source\\": \\"state.rule_risk_stats = new HashMap();\\\\nstate.host_variant_set = false;\\\\nstate.host_variant = new String();\\\\nstate.tactic_ids = new HashSet();\\"
|
||||
}
|
||||
}
|
||||
|
||||
# 5. Upload the ingest pipeline
|
||||
# Ingest pipeline to add ingest timestamp and risk level to documents
|
||||
PUT _ingest/pipeline/ml_hostriskscore_ingest_pipeline
|
||||
{
|
||||
\\"processors\\": [
|
||||
{
|
||||
\\"set\\": {
|
||||
\\"field\\": \\"ingest_timestamp\\",
|
||||
\\"value\\": \\"{{_ingest.timestamp}}\\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"fingerprint\\": {
|
||||
\\"fields\\": [
|
||||
\\"@timestamp\\",
|
||||
\\"_id\\"
|
||||
],
|
||||
\\"method\\": \\"SHA-256\\",
|
||||
\\"target_field\\": \\"_id\\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"script\\": {
|
||||
\\"id\\": \\"ml_hostriskscore_levels_script\\",
|
||||
\\"params\\": {
|
||||
\\"risk_score\\": \\"risk_stats.risk_score\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 6. Create mappings for the destination index of the Host Risk Score pivot transform
|
||||
PUT ml_host_risk_score_default
|
||||
{
|
||||
\\"mappings\\":{
|
||||
\\"properties\\":{
|
||||
\\"host.name\\":{
|
||||
\\"type\\":\\"keyword\\"
|
||||
},
|
||||
\\"@timestamp\\": {
|
||||
\\"type\\": \\"date\\"
|
||||
},
|
||||
\\"ingest_timestamp\\": {
|
||||
\\"type\\": \\"date\\"
|
||||
},
|
||||
\\"risk\\": {
|
||||
\\"type\\": \\"keyword\\"
|
||||
},
|
||||
\\"risk_stats\\": {
|
||||
\\"properties\\": {
|
||||
\\"risk_score\\": {
|
||||
\\"type\\": \\"float\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 7. Upload the Host Risk Score pivot transform
|
||||
# This transform runs hourly and calculates a risk score and risk level for hosts in a Kibana space
|
||||
PUT _transform/ml_hostriskscore_pivot_transform_default
|
||||
{
|
||||
\\"dest\\": {
|
||||
\\"index\\": \\"ml_host_risk_score_default\\",
|
||||
\\"pipeline\\": \\"ml_hostriskscore_ingest_pipeline\\"
|
||||
},
|
||||
\\"frequency\\": \\"1h\\",
|
||||
\\"pivot\\": {
|
||||
\\"aggregations\\": {
|
||||
\\"@timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
},
|
||||
\\"risk_stats\\": {
|
||||
\\"scripted_metric\\": {
|
||||
\\"combine_script\\": \\"return state\\",
|
||||
\\"init_script\\": {
|
||||
\\"id\\": \\"ml_hostriskscore_init_script\\"
|
||||
},
|
||||
\\"map_script\\": {
|
||||
\\"id\\": \\"ml_hostriskscore_map_script\\"
|
||||
},
|
||||
\\"params\\": {
|
||||
\\"lookback_time\\": 72,
|
||||
\\"max_risk\\": 100,
|
||||
\\"p\\": 1.5,
|
||||
\\"server_multiplier\\": 1.5,
|
||||
\\"tactic_base_multiplier\\": 0.25,
|
||||
\\"tactic_weights\\": {
|
||||
\\"TA0001\\": 1,
|
||||
\\"TA0002\\": 2,
|
||||
\\"TA0003\\": 3,
|
||||
\\"TA0004\\": 4,
|
||||
\\"TA0005\\": 4,
|
||||
\\"TA0006\\": 4,
|
||||
\\"TA0007\\": 4,
|
||||
\\"TA0008\\": 5,
|
||||
\\"TA0009\\": 6,
|
||||
\\"TA0010\\": 7,
|
||||
\\"TA0011\\": 6,
|
||||
\\"TA0040\\": 8,
|
||||
\\"TA0042\\": 1,
|
||||
\\"TA0043\\": 1
|
||||
},
|
||||
\\"time_decay_constant\\": 6,
|
||||
\\"zeta_constant\\": 2.612
|
||||
},
|
||||
\\"reduce_script\\": {
|
||||
\\"id\\": \\"ml_hostriskscore_reduce_script\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"group_by\\": {
|
||||
\\"host.name\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"host.name\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"source\\": {
|
||||
\\"index\\": [
|
||||
\\".alerts-security.alerts-default\\"
|
||||
],
|
||||
\\"query\\": {
|
||||
\\"bool\\": {
|
||||
\\"filter\\": [
|
||||
{
|
||||
\\"range\\": {
|
||||
\\"@timestamp\\": {
|
||||
\\"gte\\": \\"now-5d\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"sync\\": {
|
||||
\\"time\\": {
|
||||
\\"delay\\": \\"120s\\",
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 8. Start the pivot transform
|
||||
POST _transform/ml_hostriskscore_pivot_transform_default/_start
|
||||
|
||||
# 9. Create mappings for the destination index of the Host Risk Score latest transform
|
||||
PUT ml_host_risk_score_latest_default
|
||||
{
|
||||
\\"mappings\\":{
|
||||
\\"properties\\":{
|
||||
\\"host.name\\":{
|
||||
\\"type\\":\\"keyword\\"
|
||||
},
|
||||
\\"@timestamp\\": {
|
||||
\\"type\\": \\"date\\"
|
||||
},
|
||||
\\"ingest_timestamp\\": {
|
||||
\\"type\\": \\"date\\"
|
||||
},
|
||||
\\"risk\\": {
|
||||
\\"type\\": \\"keyword\\"
|
||||
},
|
||||
\\"risk_stats\\": {
|
||||
\\"properties\\": {
|
||||
\\"risk_score\\": {
|
||||
\\"type\\": \\"float\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 10. Upload the latest transform
|
||||
# This transform gets the latest risk information about hosts in a Kibana space
|
||||
PUT _transform/ml_hostriskscore_latest_transform_default
|
||||
{
|
||||
\\"dest\\": {
|
||||
\\"index\\": \\"ml_host_risk_score_latest_default\\"
|
||||
},
|
||||
\\"frequency\\": \\"1h\\",
|
||||
\\"latest\\": {
|
||||
\\"sort\\": \\"@timestamp\\",
|
||||
\\"unique_key\\": [
|
||||
\\"host.name\\"
|
||||
]
|
||||
},
|
||||
\\"source\\": {
|
||||
\\"index\\": [
|
||||
\\"ml_host_risk_score_default\\"
|
||||
]
|
||||
},
|
||||
\\"sync\\": {
|
||||
\\"time\\": {
|
||||
\\"delay\\": \\"2s\\",
|
||||
\\"field\\": \\"ingest_timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 11. Start the latest transform
|
||||
POST _transform/ml_hostriskscore_latest_transform_default/_start
|
||||
|
||||
# Hint: If you don't see data after running any of the transforms, stop and restart the transforms
|
||||
# Stop the pivot transform
|
||||
POST _transform/ml_hostriskscore_pivot_transform_default/_stop
|
||||
|
||||
# Start the pivot transform
|
||||
POST _transform/ml_hostriskscore_pivot_transform_default/_start
|
||||
|
||||
# Stop the latest transform
|
||||
POST _transform/ml_hostriskscore_latest_transform_default/_stop
|
||||
|
||||
# Start the latest transform
|
||||
POST _transform/ml_hostriskscore_latest_transform_default/_start
|
||||
"
|
||||
`;
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 {
|
||||
serverMock,
|
||||
requestContextMock,
|
||||
requestMock,
|
||||
} from '../../detection_engine/routes/__mocks__';
|
||||
import { DEV_TOOL_PREBUILT_CONTENT } from '../../../../common/constants';
|
||||
import { readPrebuiltDevToolContentRoute } from './read_prebuilt_dev_tool_content_route';
|
||||
|
||||
const readPrebuiltDevToolContentRequest = (consoleId: string) =>
|
||||
requestMock.create({
|
||||
method: 'get',
|
||||
path: DEV_TOOL_PREBUILT_CONTENT,
|
||||
params: { console_id: consoleId },
|
||||
});
|
||||
|
||||
describe('readPrebuiltDevToolContentRoute', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { context } = requestContextMock.createTools();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
|
||||
server = serverMock.create();
|
||||
({ context } = requestContextMock.createTools());
|
||||
|
||||
readPrebuiltDevToolContentRoute(server.router);
|
||||
});
|
||||
|
||||
test('should read content from enable_host_risk_score template', async () => {
|
||||
const response = await server.inject(
|
||||
readPrebuiltDevToolContentRequest('enable_host_risk_score'),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 path, { join, resolve } from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { CustomHttpResponseOptions, KibanaResponseFactory } from '@kbn/core/server';
|
||||
import { DEV_TOOL_PREBUILT_CONTENT } from '../../../../common/constants';
|
||||
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
import { consoleMappings } from '../console_mappings';
|
||||
import { ReadConsoleRequestSchema } from '../schema';
|
||||
|
||||
const getReadables = (dataPath: string) => fs.promises.readFile(dataPath, { encoding: 'utf-8' });
|
||||
|
||||
class ConsoleResponseFactory {
|
||||
constructor(private response: KibanaResponseFactory) {}
|
||||
|
||||
error<T>({ statusCode, body, headers }: CustomHttpResponseOptions<T>) {
|
||||
const contentType: CustomHttpResponseOptions<T>['headers'] = {
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
};
|
||||
const defaultedHeaders: CustomHttpResponseOptions<T>['headers'] = {
|
||||
...contentType,
|
||||
...(headers ?? {}),
|
||||
};
|
||||
|
||||
return this.response.custom({
|
||||
headers: defaultedHeaders,
|
||||
statusCode,
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const buildConsoleResponse = (response: KibanaResponseFactory) =>
|
||||
new ConsoleResponseFactory(response);
|
||||
|
||||
export const readPrebuiltDevToolContentRoute = (router: SecuritySolutionPluginRouter) => {
|
||||
router.get(
|
||||
{
|
||||
path: DEV_TOOL_PREBUILT_CONTENT,
|
||||
validate: ReadConsoleRequestSchema,
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildConsoleResponse(response);
|
||||
const { console_id: consoleId } = request.params;
|
||||
|
||||
try {
|
||||
const securitySolution = await context.securitySolution;
|
||||
const spaceId = securitySolution.getSpaceId();
|
||||
|
||||
const fileName = consoleMappings[consoleId] ?? null;
|
||||
|
||||
if (!fileName) {
|
||||
return siemResponse.error({ statusCode: 500, body: 'No such file or directory' });
|
||||
}
|
||||
|
||||
const filePath = '../console_templates';
|
||||
const dir = resolve(join(__dirname, filePath));
|
||||
|
||||
const dataPath = path.join(dir, fileName);
|
||||
const res = await getReadables(dataPath);
|
||||
const regex = /{{space_name}}/g;
|
||||
return response.ok({ body: res.replace(regex, spaceId) });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 { ReadConsoleRequestSchema } from './schema';
|
||||
|
||||
describe('ReadConsoleRequestSchema', () => {
|
||||
it('should throw error', () => {
|
||||
expect(() => ReadConsoleRequestSchema.params.validate({ console_id: '123' })).toThrow();
|
||||
});
|
||||
|
||||
it.each([['enable_host_risk_score']])('should allow console_id %p', async (template) => {
|
||||
expect(ReadConsoleRequestSchema.params.validate({ console_id: template })).toEqual({
|
||||
console_id: template,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const ReadConsoleRequestSchema = {
|
||||
params: schema.object({
|
||||
console_id: schema.oneOf([schema.literal('enable_host_risk_score')]),
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { FrameworkRequest } from '../../framework';
|
||||
import * as savedObjectsToCreate from '../saved_object';
|
||||
import type { SavedObjectTemplate } from '../types';
|
||||
|
||||
export const bulkCreateSavedObjects = async ({
|
||||
request,
|
||||
spaceId,
|
||||
savedObjectTemplate,
|
||||
}: {
|
||||
request: FrameworkRequest;
|
||||
spaceId?: string;
|
||||
savedObjectTemplate: SavedObjectTemplate;
|
||||
}) => {
|
||||
const savedObjectsClient = (await request.context.core).savedObjects.client;
|
||||
const regex = /<REPLACE-WITH-SPACE>/g;
|
||||
|
||||
const savedObjects = JSON.stringify(savedObjectsToCreate[savedObjectTemplate]);
|
||||
|
||||
if (savedObjects == null) {
|
||||
return new Error('Template not found.');
|
||||
}
|
||||
|
||||
const replacedSO = spaceId ? savedObjects.replace(regex, spaceId) : savedObjects;
|
||||
const createSO = await savedObjectsClient.bulkCreate(JSON.parse(replacedSO), {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
return createSO;
|
||||
};
|
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* 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 expectedSavedObjectTemplate = [
|
||||
{
|
||||
attributes: {
|
||||
fieldAttrs: '{}',
|
||||
fields: '[]',
|
||||
runtimeFieldMap: '{}',
|
||||
timeFieldName: '@timestamp',
|
||||
title: 'ml_host_risk_score_default',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'ml-host-risk-score-default-index-pattern',
|
||||
migrationVersion: { 'index-pattern': '7.11.0' },
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
updated_at: '2021-08-18T18:37:41.486Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: null,
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
'b885eaad-3c68-49ad-9891-70158d912dbd': {
|
||||
columnOrder: [
|
||||
'8dcda7ec-1a1a-43b3-b0b8-e702943eed5c',
|
||||
'e82aed80-ee04-4ad1-9b9d-fde4a25be58a',
|
||||
'aa4ad9b2-8829-4517-aaa8-7ed7e5793e9b',
|
||||
],
|
||||
columns: {
|
||||
'8dcda7ec-1a1a-43b3-b0b8-e702943eed5c': {
|
||||
customLabel: true,
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'Host Name',
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
missingBucket: false,
|
||||
orderBy: { columnId: 'aa4ad9b2-8829-4517-aaa8-7ed7e5793e9b', type: 'column' },
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
size: 20,
|
||||
},
|
||||
scale: 'ordinal',
|
||||
sourceField: 'host.name',
|
||||
},
|
||||
'aa4ad9b2-8829-4517-aaa8-7ed7e5793e9b': {
|
||||
customLabel: true,
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Cumulative Risk Score',
|
||||
operationType: 'max',
|
||||
scale: 'ratio',
|
||||
sourceField: 'risk_stats.risk_score',
|
||||
},
|
||||
'e82aed80-ee04-4ad1-9b9d-fde4a25be58a': {
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
label: '@timestamp',
|
||||
operationType: 'date_histogram',
|
||||
params: { interval: '1h' },
|
||||
scale: 'interval',
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [],
|
||||
query: { language: 'kuery', query: '' },
|
||||
visualization: {
|
||||
layers: [
|
||||
{
|
||||
accessors: ['aa4ad9b2-8829-4517-aaa8-7ed7e5793e9b'],
|
||||
layerId: 'b885eaad-3c68-49ad-9891-70158d912dbd',
|
||||
palette: { name: 'default', type: 'palette' },
|
||||
position: 'top',
|
||||
seriesType: 'bar_stacked',
|
||||
showGridlines: false,
|
||||
splitAccessor: '8dcda7ec-1a1a-43b3-b0b8-e702943eed5c',
|
||||
xAccessor: 'e82aed80-ee04-4ad1-9b9d-fde4a25be58a',
|
||||
},
|
||||
],
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
preferredSeriesType: 'bar_stacked',
|
||||
title: 'Empty XY chart',
|
||||
valueLabels: 'hide',
|
||||
},
|
||||
},
|
||||
title: 'Host Risk Score (Max Risk Score Histogram)',
|
||||
visualizationType: 'lnsXY',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'd3f72670-d3a0-11eb-bd37-7bb50422e346',
|
||||
migrationVersion: { lens: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'ml-host-risk-score-default-index-pattern',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'ml-host-risk-score-default-index-pattern',
|
||||
name: 'indexpattern-datasource-layer-b885eaad-3c68-49ad-9891-70158d912dbd',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'lens',
|
||||
updated_at: '2021-08-18T18:48:30.689Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
fieldAttrs: '{"signal.rule.type":{"count":1}}',
|
||||
fields: '[]',
|
||||
runtimeFieldMap: '{}',
|
||||
timeFieldName: '@timestamp',
|
||||
title: '.siem-signals-default',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'siem-signals-default-index-pattern',
|
||||
migrationVersion: { 'index-pattern': '7.11.0' },
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
'{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
|
||||
},
|
||||
title: 'Host Risk Score (Rule Breakdown)',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
visState:
|
||||
'{"title":"Host Risk Score (Rule Breakdown)","type":"table","aggs":[{"id":"2","enabled":true,"type":"sum","params":{"field":"signal.rule.risk_score","customLabel":"Total Risk Score"},"schema":"metric"},{"id":"1","enabled":true,"type":"count","params":{"customLabel":"Number of Hits"},"schema":"metric"},{"id":"3","enabled":true,"type":"terms","params":{"field":"host.name","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Host"},"schema":"split"},{"id":"4","enabled":true,"type":"terms","params":{"field":"signal.rule.name","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule Name"},"schema":"bucket"},{"id":"5","enabled":true,"type":"terms","params":{"field":"signal.rule.type","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule Type"},"schema":"bucket"}],"params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"showTotal":false,"showToolbar":false,"totalFunc":"sum","percentageCol":"","row":true}}',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: '42371d00-cf7a-11eb-9a96-05d89f94ad96',
|
||||
migrationVersion: { visualization: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'siem-signals-default-index-pattern',
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'visualization',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
'{"query":{"query":"not user.name: *$","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
|
||||
},
|
||||
title: 'Associated Users (Rule Breakdown)',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
visState:
|
||||
'{"title":"Associated Users (Rule Breakdown)","type":"table","aggs":[{"id":"2","enabled":true,"type":"sum","params":{"field":"signal.rule.risk_score","customLabel":"Total Risk Score"},"schema":"metric"},{"id":"1","enabled":true,"type":"count","params":{"customLabel":"Number of Hits"},"schema":"metric"},{"id":"3","enabled":true,"type":"terms","params":{"field":"user.name","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"User"},"schema":"split"},{"id":"4","enabled":true,"type":"terms","params":{"field":"signal.rule.name","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule Name"},"schema":"bucket"},{"id":"5","enabled":true,"type":"terms","params":{"field":"signal.rule.type","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule Type"},"schema":"bucket"}],"params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"showTotal":false,"showToolbar":false,"totalFunc":"sum","percentageCol":"","row":true}}',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'a62d3ed0-cf92-11eb-a0ff-1763d16cbda7',
|
||||
migrationVersion: { visualization: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'siem-signals-default-index-pattern',
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'visualization',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
'{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
|
||||
},
|
||||
title: 'Host Risk Score (Tactic Breakdown)- Verbose',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
visState:
|
||||
'{"title":"Host Risk Score (Tactic Breakdown)- Verbose","type":"table","aggs":[{"id":"1","enabled":true,"type":"sum","params":{"field":"signal.rule.risk_score","customLabel":"Total Risk Score"},"schema":"metric"},{"id":"3","enabled":true,"type":"terms","params":{"field":"host.name","orderBy":"1","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Host"},"schema":"split"},{"id":"5","enabled":true,"type":"terms","params":{"field":"signal.rule.threat.tactic.name","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":true,"missingBucketLabel":"Other","customLabel":"Tactic"},"schema":"bucket"},{"id":"6","enabled":true,"type":"terms","params":{"field":"signal.rule.threat.technique.name","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":true,"missingBucketLabel":"Other","customLabel":"Technique"},"schema":"bucket"},{"id":"7","enabled":true,"type":"count","params":{"customLabel":"Number of Hits"},"schema":"metric"}],"params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"showTotal":false,"showToolbar":false,"totalFunc":"sum","percentageCol":"","row":true}}',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'b2dbc9b0-cf94-11eb-bd37-7bb50422e346',
|
||||
migrationVersion: { visualization: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'siem-signals-default-index-pattern',
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'visualization',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: { color: '#D36086', description: '', name: 'experimental' },
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: '1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
references: [],
|
||||
type: 'tag',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description:
|
||||
'This dashboard allows users to drill down further into the details of the risk components associated with a particular host.',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}',
|
||||
},
|
||||
optionsJSON: '{"hidePanelTitles":false,"useMargins":true}',
|
||||
panelsJSON:
|
||||
'[{"version":"7.13.4","type":"visualization","gridData":{"x":0,"y":0,"w":48,"h":3,"i":"eaa57cf4-7ca3-4919-ab76-dbac0eb6a195"},"panelIndex":"eaa57cf4-7ca3-4919-ab76-dbac0eb6a195","embeddableConfig":{"savedVis":{"title":"","description":"","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"The Host Risk Score capability is an experimental feature released in 7.14. You can read further about it [here](https://github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md)."},"uiState":{},"data":{"aggs":[],"searchSource":{"query":{"query":"","language":"kuery"},"filter":[]}}},"hidePanelTitles":true,"enhancements":{}}},{"version":"7.13.4","type":"lens","gridData":{"x":0,"y":3,"w":48,"h":15,"i":"e11ed08e-70d0-4c69-991a-12e20dc89440"},"panelIndex":"e11ed08e-70d0-4c69-991a-12e20dc89440","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"339da811-5c23-4432-9649-53cb066e6aaf","triggers":["VALUE_CLICK_TRIGGER"],"action":{"factoryId":"URL_DRILLDOWN","name":"Go to Host View","config":{"url":{"template":"{{kibanaUrl}}/app/security/hosts/{{context.panel.filters.[0].meta.params.query}}"},"openInNewTab":true,"encodeUrl":true}}}]}},"hidePanelTitles":false},"title":"Cumulative Host Risk Score (multiple hosts)","panelRefName":"panel_e11ed08e-70d0-4c69-991a-12e20dc89440"},{"version":"7.13.4","type":"visualization","gridData":{"x":0,"y":18,"w":24,"h":28,"i":"cae82aa1-20c8-4354-94ab-3934ac53b8fe"},"panelIndex":"cae82aa1-20c8-4354-94ab-3934ac53b8fe","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"0fd43778-bd5d-4b2b-85c3-47ac3b756434","triggers":["VALUE_CLICK_TRIGGER"],"action":{"factoryId":"URL_DRILLDOWN","name":"Go to Host View","config":{"url":{"template":"{{kibanaUrl}}/app/security/hosts/{{context.panel.filters.[0].meta.params.query}}"},"openInNewTab":true,"encodeUrl":true}}}]}},"hidePanelTitles":false},"title":"Associated Rules of Risky Hosts","panelRefName":"panel_cae82aa1-20c8-4354-94ab-3934ac53b8fe"},{"version":"7.13.4","type":"visualization","gridData":{"x":24,"y":18,"w":24,"h":28,"i":"8d09b97c-a023-4b7e-9e9d-1c46e726a487"},"panelIndex":"8d09b97c-a023-4b7e-9e9d-1c46e726a487","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"593ff0e6-25da-47ad-b81d-9a0106c0e9aa","triggers":["VALUE_CLICK_TRIGGER"],"action":{"factoryId":"URL_DRILLDOWN","name":"Go to Host View","config":{"url":{"template":"{{kibanaUrl}}/app/security/hosts/{{context.panel.filters.[0].meta.params.query}}"},"openInNewTab":true,"encodeUrl":true}}}]}},"hidePanelTitles":false},"title":"Associated Users of Risky Hosts","panelRefName":"panel_8d09b97c-a023-4b7e-9e9d-1c46e726a487"},{"version":"7.13.4","type":"visualization","gridData":{"x":0,"y":46,"w":48,"h":16,"i":"0c9c8318-ebb0-47fb-919a-1836ebf232ae"},"panelIndex":"0c9c8318-ebb0-47fb-919a-1836ebf232ae","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"a76ea63c-da92-4bad-b3d6-6df823e1c04b","triggers":["VALUE_CLICK_TRIGGER"],"action":{"factoryId":"URL_DRILLDOWN","name":"Go to Host View","config":{"url":{"template":"{{kibanaUrl}}/app/security/hosts/{{context.panel.filters.[0].meta.params.query}}"},"openInNewTab":true,"encodeUrl":true}}}]}},"hidePanelTitles":false},"title":"Tactic Breakdown of Risky Hosts (Verbose)","panelRefName":"panel_0c9c8318-ebb0-47fb-919a-1836ebf232ae"}]',
|
||||
timeRestore: false,
|
||||
title: 'Drilldown of Host Risk Score',
|
||||
version: 1,
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: '6f05c8c0-cf77-11eb-9a96-05d89f94ad96',
|
||||
migrationVersion: { dashboard: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'd3f72670-d3a0-11eb-bd37-7bb50422e346',
|
||||
name: 'e11ed08e-70d0-4c69-991a-12e20dc89440:panel_e11ed08e-70d0-4c69-991a-12e20dc89440',
|
||||
type: 'lens',
|
||||
},
|
||||
{
|
||||
id: '42371d00-cf7a-11eb-9a96-05d89f94ad96',
|
||||
name: 'cae82aa1-20c8-4354-94ab-3934ac53b8fe:panel_cae82aa1-20c8-4354-94ab-3934ac53b8fe',
|
||||
type: 'visualization',
|
||||
},
|
||||
{
|
||||
id: 'a62d3ed0-cf92-11eb-a0ff-1763d16cbda7',
|
||||
name: '8d09b97c-a023-4b7e-9e9d-1c46e726a487:panel_8d09b97c-a023-4b7e-9e9d-1c46e726a487',
|
||||
type: 'visualization',
|
||||
},
|
||||
{
|
||||
id: 'b2dbc9b0-cf94-11eb-bd37-7bb50422e346',
|
||||
name: '0c9c8318-ebb0-47fb-919a-1836ebf232ae:panel_0c9c8318-ebb0-47fb-919a-1836ebf232ae',
|
||||
type: 'visualization',
|
||||
},
|
||||
{
|
||||
id: '1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
name: 'tag-1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2021-08-18T17:09:15.576Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
fieldAttrs: '{}',
|
||||
fields: '[]',
|
||||
runtimeFieldMap: '{}',
|
||||
timeFieldName: '@timestamp',
|
||||
title: 'ml_host_risk_score_latest_default',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'ml-host-risk-score-latest-default-index-pattern',
|
||||
migrationVersion: { 'index-pattern': '7.11.0' },
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
updated_at: '2021-08-18T18:47:22.500Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: null,
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
'2f34d626-d0ee-4ade-9e75-13c480699485': {
|
||||
columnOrder: [
|
||||
'9c8c581f-6cb8-4ecf-8eb3-4c6df33edc5d',
|
||||
'c547501b-fe04-4073-8b4e-dbbdc3a4ff04',
|
||||
'e2444d64-721a-4532-9633-5b206eee76d6',
|
||||
],
|
||||
columns: {
|
||||
'9c8c581f-6cb8-4ecf-8eb3-4c6df33edc5d': {
|
||||
customLabel: true,
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'Host Name',
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
missingBucket: false,
|
||||
orderBy: { columnId: 'c547501b-fe04-4073-8b4e-dbbdc3a4ff04', type: 'column' },
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
size: 20,
|
||||
},
|
||||
scale: 'ordinal',
|
||||
sourceField: 'host.name',
|
||||
},
|
||||
'c547501b-fe04-4073-8b4e-dbbdc3a4ff04': {
|
||||
customLabel: true,
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Risk Score',
|
||||
operationType: 'sum',
|
||||
scale: 'ratio',
|
||||
sourceField: 'risk_stats.risk_score',
|
||||
},
|
||||
'e2444d64-721a-4532-9633-5b206eee76d6': {
|
||||
customLabel: true,
|
||||
dataType: 'string',
|
||||
isBucketed: false,
|
||||
label: 'Current Risk',
|
||||
operationType: 'last_value',
|
||||
params: { sortField: '@timestamp' },
|
||||
scale: 'ordinal',
|
||||
sourceField: 'risk',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [],
|
||||
query: { language: 'kuery', query: '' },
|
||||
visualization: {
|
||||
columns: [
|
||||
{ columnId: '9c8c581f-6cb8-4ecf-8eb3-4c6df33edc5d', isTransposed: false },
|
||||
{
|
||||
alignment: 'left',
|
||||
columnId: 'c547501b-fe04-4073-8b4e-dbbdc3a4ff04',
|
||||
hidden: true,
|
||||
isTransposed: false,
|
||||
},
|
||||
{ columnId: 'e2444d64-721a-4532-9633-5b206eee76d6', isTransposed: false },
|
||||
],
|
||||
layerId: '2f34d626-d0ee-4ade-9e75-13c480699485',
|
||||
},
|
||||
},
|
||||
title: 'Current Risk Score for Hosts',
|
||||
visualizationType: 'lnsDatatable',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'dc289c10-d4ff-11eb-a0ff-1763d16cbda7',
|
||||
migrationVersion: { lens: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'ml-host-risk-score-latest-default-index-pattern',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'ml-host-risk-score-latest-default-index-pattern',
|
||||
name: 'indexpattern-datasource-layer-2f34d626-d0ee-4ade-9e75-13c480699485',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'lens',
|
||||
updated_at: '2021-08-18T17:07:41.806Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description:
|
||||
'This dashboard shows the most current list of risky hosts (Top 20) in an environment. ',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}',
|
||||
},
|
||||
optionsJSON: '{"hidePanelTitles":false,"useMargins":true}',
|
||||
panelsJSON:
|
||||
'[{"version":"7.13.4","type":"visualization","gridData":{"x":0,"y":0,"w":48,"h":3,"i":"287b65e9-0aaa-42ee-ab7b-d60b3937d37a"},"panelIndex":"287b65e9-0aaa-42ee-ab7b-d60b3937d37a","embeddableConfig":{"savedVis":{"title":"","description":"","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"The Host Risk Score capability is an experimental feature released in 7.14. You can read further about it [here](https://github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md)."},"uiState":{},"data":{"aggs":[],"searchSource":{"query":{"query":"","language":"kuery"},"filter":[]}}},"hidePanelTitles":true,"enhancements":{}},"title":"Note:"},{"version":"7.13.4","type":"lens","gridData":{"x":16,"y":3,"w":16,"h":15,"i":"654d55f8-f873-4348-96cd-5dce0b56ac32"},"panelIndex":"654d55f8-f873-4348-96cd-5dce0b56ac32","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"b04e60d5-4e34-4589-af2e-8e9c3a15936f","triggers":["FILTER_TRIGGER"],"action":{"factoryId":"DASHBOARD_TO_DASHBOARD_DRILLDOWN","name":"Go to Dashboard","config":{"useCurrentFilters":true,"useCurrentDateRange":true}}}]}},"hidePanelTitles":false},"title":"Current Risk Scores for Hosts","panelRefName":"panel_654d55f8-f873-4348-96cd-5dce0b56ac32"}]',
|
||||
timeRestore: false,
|
||||
title: 'Current Risk Score for Hosts',
|
||||
version: 1,
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: '27b483b0-d500-11eb-a0ff-1763d16cbda7',
|
||||
migrationVersion: { dashboard: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'dc289c10-d4ff-11eb-a0ff-1763d16cbda7',
|
||||
name: '654d55f8-f873-4348-96cd-5dce0b56ac32:panel_654d55f8-f873-4348-96cd-5dce0b56ac32',
|
||||
type: 'lens',
|
||||
},
|
||||
{
|
||||
id: '6f05c8c0-cf77-11eb-9a96-05d89f94ad96',
|
||||
name: '654d55f8-f873-4348-96cd-5dce0b56ac32:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:b04e60d5-4e34-4589-af2e-8e9c3a15936f:dashboardId',
|
||||
type: 'dashboard',
|
||||
},
|
||||
{
|
||||
id: '1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
name: 'tag-1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2021-08-18T17:08:00.467Z',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||
import { PREBUILT_SAVED_OBJECTS_BULK_CREATE } from '../../../../common/constants';
|
||||
import {
|
||||
serverMock,
|
||||
requestContextMock,
|
||||
mockGetCurrentUser,
|
||||
requestMock,
|
||||
} from '../../detection_engine/routes/__mocks__';
|
||||
import { getEmptySavedObjectsResponse } from '../../detection_engine/routes/__mocks__/request_responses';
|
||||
import { expectedSavedObjectTemplate } from '../mocks';
|
||||
import { createPrebuiltSavedObjectsRoute } from './create_prebuilt_saved_objects';
|
||||
|
||||
const createPrebuiltSavedObjectsRequest = (savedObjectTemplate: string) =>
|
||||
requestMock.create({
|
||||
method: 'post',
|
||||
path: PREBUILT_SAVED_OBJECTS_BULK_CREATE,
|
||||
params: { template_name: savedObjectTemplate },
|
||||
});
|
||||
|
||||
describe('createPrebuiltSavedObjects', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let securitySetup: SecurityPluginSetup;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
|
||||
server = serverMock.create();
|
||||
({ clients, context } = requestContextMock.createTools());
|
||||
|
||||
securitySetup = {
|
||||
authc: {
|
||||
getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser),
|
||||
},
|
||||
authz: {},
|
||||
} as unknown as SecurityPluginSetup;
|
||||
|
||||
clients.savedObjectsClient.bulkCreate.mockResolvedValue(getEmptySavedObjectsResponse()); // rule status request
|
||||
|
||||
createPrebuiltSavedObjectsRoute(server.router, securitySetup);
|
||||
});
|
||||
|
||||
test('should create saved objects from given template', async () => {
|
||||
const response = await server.inject(
|
||||
createPrebuiltSavedObjectsRequest('hostRiskScoreDashboards'),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
|
||||
expect(clients.savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
expectedSavedObjectTemplate,
|
||||
{ overwrite: true }
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
|
||||
import { PREBUILT_SAVED_OBJECTS_BULK_CREATE } from '../../../../common/constants';
|
||||
|
||||
import type { SetupPlugins } from '../../../plugin';
|
||||
|
||||
import { buildSiemResponse } from '../../detection_engine/routes/utils';
|
||||
|
||||
import { buildFrameworkRequest } from '../../timeline/utils/common';
|
||||
import { bulkCreateSavedObjects } from '../helpers/bulk_create_saved_objects';
|
||||
import { createPrebuiltSavedObjectsSchema } from '../schema';
|
||||
|
||||
export const createPrebuiltSavedObjectsRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: PREBUILT_SAVED_OBJECTS_BULK_CREATE,
|
||||
validate: createPrebuiltSavedObjectsSchema,
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const { template_name: templateName } = request.params;
|
||||
|
||||
try {
|
||||
const securitySolution = await context.securitySolution;
|
||||
|
||||
const spaceId = securitySolution?.getSpaceId();
|
||||
|
||||
const frameworkRequest = await buildFrameworkRequest(context, security, request);
|
||||
|
||||
const res = await bulkCreateSavedObjects({
|
||||
request: frameworkRequest,
|
||||
spaceId,
|
||||
savedObjectTemplate: templateName,
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
body: res,
|
||||
});
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,416 @@
|
|||
/*
|
||||
* 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 } from '@kbn/core/types';
|
||||
|
||||
export const hostRiskScoreDashboards: SavedObject[] = [
|
||||
{
|
||||
attributes: {
|
||||
fieldAttrs: '{}',
|
||||
fields: '[]',
|
||||
runtimeFieldMap: '{}',
|
||||
timeFieldName: '@timestamp',
|
||||
title: 'ml_host_risk_score_<REPLACE-WITH-SPACE>',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'ml-host-risk-score-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
migrationVersion: { 'index-pattern': '7.11.0' },
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
updated_at: '2021-08-18T18:37:41.486Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: null,
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
'b885eaad-3c68-49ad-9891-70158d912dbd': {
|
||||
columnOrder: [
|
||||
'8dcda7ec-1a1a-43b3-b0b8-e702943eed5c',
|
||||
'e82aed80-ee04-4ad1-9b9d-fde4a25be58a',
|
||||
'aa4ad9b2-8829-4517-aaa8-7ed7e5793e9b',
|
||||
],
|
||||
columns: {
|
||||
'8dcda7ec-1a1a-43b3-b0b8-e702943eed5c': {
|
||||
customLabel: true,
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'Host Name',
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
missingBucket: false,
|
||||
orderBy: { columnId: 'aa4ad9b2-8829-4517-aaa8-7ed7e5793e9b', type: 'column' },
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
size: 20,
|
||||
},
|
||||
scale: 'ordinal',
|
||||
sourceField: 'host.name',
|
||||
},
|
||||
'aa4ad9b2-8829-4517-aaa8-7ed7e5793e9b': {
|
||||
customLabel: true,
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Cumulative Risk Score',
|
||||
operationType: 'max',
|
||||
scale: 'ratio',
|
||||
sourceField: 'risk_stats.risk_score',
|
||||
},
|
||||
'e82aed80-ee04-4ad1-9b9d-fde4a25be58a': {
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
label: '@timestamp',
|
||||
operationType: 'date_histogram',
|
||||
params: { interval: '1h' },
|
||||
scale: 'interval',
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [],
|
||||
query: { language: 'kuery', query: '' },
|
||||
visualization: {
|
||||
layers: [
|
||||
{
|
||||
accessors: ['aa4ad9b2-8829-4517-aaa8-7ed7e5793e9b'],
|
||||
layerId: 'b885eaad-3c68-49ad-9891-70158d912dbd',
|
||||
palette: { name: 'default', type: 'palette' },
|
||||
position: 'top',
|
||||
seriesType: 'bar_stacked',
|
||||
showGridlines: false,
|
||||
splitAccessor: '8dcda7ec-1a1a-43b3-b0b8-e702943eed5c',
|
||||
xAccessor: 'e82aed80-ee04-4ad1-9b9d-fde4a25be58a',
|
||||
},
|
||||
],
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
preferredSeriesType: 'bar_stacked',
|
||||
title: 'Empty XY chart',
|
||||
valueLabels: 'hide',
|
||||
},
|
||||
},
|
||||
title: 'Host Risk Score (Max Risk Score Histogram)',
|
||||
visualizationType: 'lnsXY',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'd3f72670-d3a0-11eb-bd37-7bb50422e346',
|
||||
migrationVersion: { lens: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'ml-host-risk-score-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'ml-host-risk-score-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
name: 'indexpattern-datasource-layer-b885eaad-3c68-49ad-9891-70158d912dbd',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'lens',
|
||||
updated_at: '2021-08-18T18:48:30.689Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
fieldAttrs: '{"signal.rule.type":{"count":1}}',
|
||||
fields: '[]',
|
||||
runtimeFieldMap: '{}',
|
||||
timeFieldName: '@timestamp',
|
||||
title: '.siem-signals-<REPLACE-WITH-SPACE>',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'siem-signals-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
migrationVersion: { 'index-pattern': '7.11.0' },
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
'{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
|
||||
},
|
||||
title: 'Host Risk Score (Rule Breakdown)',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
visState:
|
||||
'{"title":"Host Risk Score (Rule Breakdown)","type":"table","aggs":[{"id":"2","enabled":true,"type":"sum","params":{"field":"signal.rule.risk_score","customLabel":"Total Risk Score"},"schema":"metric"},{"id":"1","enabled":true,"type":"count","params":{"customLabel":"Number of Hits"},"schema":"metric"},{"id":"3","enabled":true,"type":"terms","params":{"field":"host.name","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Host"},"schema":"split"},{"id":"4","enabled":true,"type":"terms","params":{"field":"signal.rule.name","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule Name"},"schema":"bucket"},{"id":"5","enabled":true,"type":"terms","params":{"field":"signal.rule.type","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule Type"},"schema":"bucket"}],"params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"showTotal":false,"showToolbar":false,"totalFunc":"sum","percentageCol":"","row":true}}',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: '42371d00-cf7a-11eb-9a96-05d89f94ad96',
|
||||
migrationVersion: { visualization: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'siem-signals-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'visualization',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
'{"query":{"query":"not user.name: *$","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
|
||||
},
|
||||
title: 'Associated Users (Rule Breakdown)',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
visState:
|
||||
'{"title":"Associated Users (Rule Breakdown)","type":"table","aggs":[{"id":"2","enabled":true,"type":"sum","params":{"field":"signal.rule.risk_score","customLabel":"Total Risk Score"},"schema":"metric"},{"id":"1","enabled":true,"type":"count","params":{"customLabel":"Number of Hits"},"schema":"metric"},{"id":"3","enabled":true,"type":"terms","params":{"field":"user.name","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"User"},"schema":"split"},{"id":"4","enabled":true,"type":"terms","params":{"field":"signal.rule.name","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule Name"},"schema":"bucket"},{"id":"5","enabled":true,"type":"terms","params":{"field":"signal.rule.type","orderBy":"2","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule Type"},"schema":"bucket"}],"params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"showTotal":false,"showToolbar":false,"totalFunc":"sum","percentageCol":"","row":true}}',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'a62d3ed0-cf92-11eb-a0ff-1763d16cbda7',
|
||||
migrationVersion: { visualization: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'siem-signals-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'visualization',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
'{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
|
||||
},
|
||||
title: 'Host Risk Score (Tactic Breakdown)- Verbose',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
visState:
|
||||
'{"title":"Host Risk Score (Tactic Breakdown)- Verbose","type":"table","aggs":[{"id":"1","enabled":true,"type":"sum","params":{"field":"signal.rule.risk_score","customLabel":"Total Risk Score"},"schema":"metric"},{"id":"3","enabled":true,"type":"terms","params":{"field":"host.name","orderBy":"1","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Host"},"schema":"split"},{"id":"5","enabled":true,"type":"terms","params":{"field":"signal.rule.threat.tactic.name","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":true,"missingBucketLabel":"Other","customLabel":"Tactic"},"schema":"bucket"},{"id":"6","enabled":true,"type":"terms","params":{"field":"signal.rule.threat.technique.name","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":true,"missingBucketLabel":"Other","customLabel":"Technique"},"schema":"bucket"},{"id":"7","enabled":true,"type":"count","params":{"customLabel":"Number of Hits"},"schema":"metric"}],"params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"showTotal":false,"showToolbar":false,"totalFunc":"sum","percentageCol":"","row":true}}',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'b2dbc9b0-cf94-11eb-bd37-7bb50422e346',
|
||||
migrationVersion: { visualization: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'siem-signals-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'visualization',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: { color: '#D36086', description: '', name: 'experimental' },
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: '1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
references: [],
|
||||
type: 'tag',
|
||||
updated_at: '2021-08-18T16:27:39.980Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description:
|
||||
'This dashboard allows users to drill down further into the details of the risk components associated with a particular host.',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}',
|
||||
},
|
||||
optionsJSON: '{"hidePanelTitles":false,"useMargins":true}',
|
||||
panelsJSON:
|
||||
'[{"version":"7.13.4","type":"visualization","gridData":{"x":0,"y":0,"w":48,"h":3,"i":"eaa57cf4-7ca3-4919-ab76-dbac0eb6a195"},"panelIndex":"eaa57cf4-7ca3-4919-ab76-dbac0eb6a195","embeddableConfig":{"savedVis":{"title":"","description":"","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"The Host Risk Score capability is an experimental feature released in 7.14. You can read further about it [here](https://github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md)."},"uiState":{},"data":{"aggs":[],"searchSource":{"query":{"query":"","language":"kuery"},"filter":[]}}},"hidePanelTitles":true,"enhancements":{}}},{"version":"7.13.4","type":"lens","gridData":{"x":0,"y":3,"w":48,"h":15,"i":"e11ed08e-70d0-4c69-991a-12e20dc89440"},"panelIndex":"e11ed08e-70d0-4c69-991a-12e20dc89440","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"339da811-5c23-4432-9649-53cb066e6aaf","triggers":["VALUE_CLICK_TRIGGER"],"action":{"factoryId":"URL_DRILLDOWN","name":"Go to Host View","config":{"url":{"template":"{{kibanaUrl}}/app/security/hosts/{{context.panel.filters.[0].meta.params.query}}"},"openInNewTab":true,"encodeUrl":true}}}]}},"hidePanelTitles":false},"title":"Cumulative Host Risk Score (multiple hosts)","panelRefName":"panel_e11ed08e-70d0-4c69-991a-12e20dc89440"},{"version":"7.13.4","type":"visualization","gridData":{"x":0,"y":18,"w":24,"h":28,"i":"cae82aa1-20c8-4354-94ab-3934ac53b8fe"},"panelIndex":"cae82aa1-20c8-4354-94ab-3934ac53b8fe","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"0fd43778-bd5d-4b2b-85c3-47ac3b756434","triggers":["VALUE_CLICK_TRIGGER"],"action":{"factoryId":"URL_DRILLDOWN","name":"Go to Host View","config":{"url":{"template":"{{kibanaUrl}}/app/security/hosts/{{context.panel.filters.[0].meta.params.query}}"},"openInNewTab":true,"encodeUrl":true}}}]}},"hidePanelTitles":false},"title":"Associated Rules of Risky Hosts","panelRefName":"panel_cae82aa1-20c8-4354-94ab-3934ac53b8fe"},{"version":"7.13.4","type":"visualization","gridData":{"x":24,"y":18,"w":24,"h":28,"i":"8d09b97c-a023-4b7e-9e9d-1c46e726a487"},"panelIndex":"8d09b97c-a023-4b7e-9e9d-1c46e726a487","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"593ff0e6-25da-47ad-b81d-9a0106c0e9aa","triggers":["VALUE_CLICK_TRIGGER"],"action":{"factoryId":"URL_DRILLDOWN","name":"Go to Host View","config":{"url":{"template":"{{kibanaUrl}}/app/security/hosts/{{context.panel.filters.[0].meta.params.query}}"},"openInNewTab":true,"encodeUrl":true}}}]}},"hidePanelTitles":false},"title":"Associated Users of Risky Hosts","panelRefName":"panel_8d09b97c-a023-4b7e-9e9d-1c46e726a487"},{"version":"7.13.4","type":"visualization","gridData":{"x":0,"y":46,"w":48,"h":16,"i":"0c9c8318-ebb0-47fb-919a-1836ebf232ae"},"panelIndex":"0c9c8318-ebb0-47fb-919a-1836ebf232ae","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"a76ea63c-da92-4bad-b3d6-6df823e1c04b","triggers":["VALUE_CLICK_TRIGGER"],"action":{"factoryId":"URL_DRILLDOWN","name":"Go to Host View","config":{"url":{"template":"{{kibanaUrl}}/app/security/hosts/{{context.panel.filters.[0].meta.params.query}}"},"openInNewTab":true,"encodeUrl":true}}}]}},"hidePanelTitles":false},"title":"Tactic Breakdown of Risky Hosts (Verbose)","panelRefName":"panel_0c9c8318-ebb0-47fb-919a-1836ebf232ae"}]',
|
||||
timeRestore: false,
|
||||
title: 'Drilldown of Host Risk Score',
|
||||
version: 1,
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: '6f05c8c0-cf77-11eb-9a96-05d89f94ad96',
|
||||
migrationVersion: { dashboard: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'd3f72670-d3a0-11eb-bd37-7bb50422e346',
|
||||
name: 'e11ed08e-70d0-4c69-991a-12e20dc89440:panel_e11ed08e-70d0-4c69-991a-12e20dc89440',
|
||||
type: 'lens',
|
||||
},
|
||||
{
|
||||
id: '42371d00-cf7a-11eb-9a96-05d89f94ad96',
|
||||
name: 'cae82aa1-20c8-4354-94ab-3934ac53b8fe:panel_cae82aa1-20c8-4354-94ab-3934ac53b8fe',
|
||||
type: 'visualization',
|
||||
},
|
||||
{
|
||||
id: 'a62d3ed0-cf92-11eb-a0ff-1763d16cbda7',
|
||||
name: '8d09b97c-a023-4b7e-9e9d-1c46e726a487:panel_8d09b97c-a023-4b7e-9e9d-1c46e726a487',
|
||||
type: 'visualization',
|
||||
},
|
||||
{
|
||||
id: 'b2dbc9b0-cf94-11eb-bd37-7bb50422e346',
|
||||
name: '0c9c8318-ebb0-47fb-919a-1836ebf232ae:panel_0c9c8318-ebb0-47fb-919a-1836ebf232ae',
|
||||
type: 'visualization',
|
||||
},
|
||||
{
|
||||
id: '1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
name: 'tag-1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2021-08-18T17:09:15.576Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
fieldAttrs: '{}',
|
||||
fields: '[]',
|
||||
runtimeFieldMap: '{}',
|
||||
timeFieldName: '@timestamp',
|
||||
title: 'ml_host_risk_score_latest_<REPLACE-WITH-SPACE>',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'ml-host-risk-score-latest-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
migrationVersion: { 'index-pattern': '7.11.0' },
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
updated_at: '2021-08-18T18:47:22.500Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description: null,
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
'2f34d626-d0ee-4ade-9e75-13c480699485': {
|
||||
columnOrder: [
|
||||
'9c8c581f-6cb8-4ecf-8eb3-4c6df33edc5d',
|
||||
'c547501b-fe04-4073-8b4e-dbbdc3a4ff04',
|
||||
'e2444d64-721a-4532-9633-5b206eee76d6',
|
||||
],
|
||||
columns: {
|
||||
'9c8c581f-6cb8-4ecf-8eb3-4c6df33edc5d': {
|
||||
customLabel: true,
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'Host Name',
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
missingBucket: false,
|
||||
orderBy: { columnId: 'c547501b-fe04-4073-8b4e-dbbdc3a4ff04', type: 'column' },
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
size: 20,
|
||||
},
|
||||
scale: 'ordinal',
|
||||
sourceField: 'host.name',
|
||||
},
|
||||
'c547501b-fe04-4073-8b4e-dbbdc3a4ff04': {
|
||||
customLabel: true,
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Risk Score',
|
||||
operationType: 'sum',
|
||||
scale: 'ratio',
|
||||
sourceField: 'risk_stats.risk_score',
|
||||
},
|
||||
'e2444d64-721a-4532-9633-5b206eee76d6': {
|
||||
customLabel: true,
|
||||
dataType: 'string',
|
||||
isBucketed: false,
|
||||
label: 'Current Risk',
|
||||
operationType: 'last_value',
|
||||
params: { sortField: '@timestamp' },
|
||||
scale: 'ordinal',
|
||||
sourceField: 'risk',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [],
|
||||
query: { language: 'kuery', query: '' },
|
||||
visualization: {
|
||||
columns: [
|
||||
{ columnId: '9c8c581f-6cb8-4ecf-8eb3-4c6df33edc5d', isTransposed: false },
|
||||
{
|
||||
alignment: 'left',
|
||||
columnId: 'c547501b-fe04-4073-8b4e-dbbdc3a4ff04',
|
||||
hidden: true,
|
||||
isTransposed: false,
|
||||
},
|
||||
{ columnId: 'e2444d64-721a-4532-9633-5b206eee76d6', isTransposed: false },
|
||||
],
|
||||
layerId: '2f34d626-d0ee-4ade-9e75-13c480699485',
|
||||
},
|
||||
},
|
||||
title: 'Current Risk Score for Hosts',
|
||||
visualizationType: 'lnsDatatable',
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: 'dc289c10-d4ff-11eb-a0ff-1763d16cbda7',
|
||||
migrationVersion: { lens: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'ml-host-risk-score-latest-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'ml-host-risk-score-latest-<REPLACE-WITH-SPACE>-index-pattern',
|
||||
name: 'indexpattern-datasource-layer-2f34d626-d0ee-4ade-9e75-13c480699485',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
type: 'lens',
|
||||
updated_at: '2021-08-18T17:07:41.806Z',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
description:
|
||||
'This dashboard shows the most current list of risky hosts (Top 20) in an environment. ',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}',
|
||||
},
|
||||
optionsJSON: '{"hidePanelTitles":false,"useMargins":true}',
|
||||
panelsJSON:
|
||||
'[{"version":"7.13.4","type":"visualization","gridData":{"x":0,"y":0,"w":48,"h":3,"i":"287b65e9-0aaa-42ee-ab7b-d60b3937d37a"},"panelIndex":"287b65e9-0aaa-42ee-ab7b-d60b3937d37a","embeddableConfig":{"savedVis":{"title":"","description":"","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"The Host Risk Score capability is an experimental feature released in 7.14. You can read further about it [here](https://github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md)."},"uiState":{},"data":{"aggs":[],"searchSource":{"query":{"query":"","language":"kuery"},"filter":[]}}},"hidePanelTitles":true,"enhancements":{}},"title":"Note:"},{"version":"7.13.4","type":"lens","gridData":{"x":16,"y":3,"w":16,"h":15,"i":"654d55f8-f873-4348-96cd-5dce0b56ac32"},"panelIndex":"654d55f8-f873-4348-96cd-5dce0b56ac32","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[{"eventId":"b04e60d5-4e34-4589-af2e-8e9c3a15936f","triggers":["FILTER_TRIGGER"],"action":{"factoryId":"DASHBOARD_TO_DASHBOARD_DRILLDOWN","name":"Go to Dashboard","config":{"useCurrentFilters":true,"useCurrentDateRange":true}}}]}},"hidePanelTitles":false},"title":"Current Risk Scores for Hosts","panelRefName":"panel_654d55f8-f873-4348-96cd-5dce0b56ac32"}]',
|
||||
timeRestore: false,
|
||||
title: 'Current Risk Score for Hosts',
|
||||
version: 1,
|
||||
},
|
||||
coreMigrationVersion: '7.13.4',
|
||||
id: '27b483b0-d500-11eb-a0ff-1763d16cbda7',
|
||||
migrationVersion: { dashboard: '7.13.1' },
|
||||
references: [
|
||||
{
|
||||
id: 'dc289c10-d4ff-11eb-a0ff-1763d16cbda7',
|
||||
name: '654d55f8-f873-4348-96cd-5dce0b56ac32:panel_654d55f8-f873-4348-96cd-5dce0b56ac32',
|
||||
type: 'lens',
|
||||
},
|
||||
{
|
||||
id: '6f05c8c0-cf77-11eb-9a96-05d89f94ad96',
|
||||
name: '654d55f8-f873-4348-96cd-5dce0b56ac32:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:b04e60d5-4e34-4589-af2e-8e9c3a15936f:dashboardId',
|
||||
type: 'dashboard',
|
||||
},
|
||||
{
|
||||
id: '1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
name: 'tag-1d00ebe0-f3b2-11eb-beb2-b91666445a94',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2021-08-18T17:08:00.467Z',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { hostRiskScoreDashboards } from './host_risk_score_dashboards';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { createPrebuiltSavedObjectsSchema } from './schema';
|
||||
|
||||
describe('createPrebuiltSavedObjectsSchema', () => {
|
||||
it('should throw error', () => {
|
||||
expect(() =>
|
||||
createPrebuiltSavedObjectsSchema.params.validate({ template_name: '123' })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it.each([['hostRiskScoreDashboards']])('should allow template %p', async (template) => {
|
||||
expect(createPrebuiltSavedObjectsSchema.params.validate({ template_name: template })).toEqual({
|
||||
template_name: template,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const createPrebuiltSavedObjectsSchema = {
|
||||
params: schema.object({
|
||||
template_name: schema.oneOf([schema.literal('hostRiskScoreDashboards')]),
|
||||
}),
|
||||
};
|
|
@ -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 type SavedObjectTemplate = 'hostRiskScoreDashboards';
|
|
@ -69,6 +69,9 @@ import { legacyCreateLegacyNotificationRoute } from '../lib/detection_engine/rou
|
|||
import { createSourcererDataViewRoute, getSourcererDataViewRoute } from '../lib/sourcerer/routes';
|
||||
import type { ITelemetryReceiver } from '../lib/telemetry/receiver';
|
||||
import { telemetryDetectionRulesPreviewRoute } from '../lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route';
|
||||
import { readPrebuiltDevToolContentRoute } from '../lib/prebuilt_dev_tool_content/routes/read_prebuilt_dev_tool_content_route';
|
||||
import { createPrebuiltSavedObjectsRoute } from '../lib/prebuilt_saved_objects/routes/create_prebuilt_saved_objects';
|
||||
import { readAlertsIndexExistsRoute } from '../lib/detection_engine/routes/index/read_alerts_index_exists_route';
|
||||
import { getInstalledIntegrationsRoute } from '../lib/detection_engine/routes/fleet/get_installed_integrations/get_installed_integrations_route';
|
||||
|
||||
export const initRoutes = (
|
||||
|
@ -155,8 +158,11 @@ export const initRoutes = (
|
|||
// All REST index creation, policy management for spaces
|
||||
createIndexRoute(router);
|
||||
readIndexRoute(router, ruleDataService);
|
||||
readAlertsIndexExistsRoute(router);
|
||||
deleteIndexRoute(router);
|
||||
|
||||
readPrebuiltDevToolContentRoute(router);
|
||||
createPrebuiltSavedObjectsRoute(router, security);
|
||||
// Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags
|
||||
readTagsRoute(router);
|
||||
|
||||
|
|
|
@ -26925,7 +26925,6 @@
|
|||
"xpack.securitySolution.overview.auditBeatSocketTitle": "Socket",
|
||||
"xpack.securitySolution.overview.auditBeatUserTitle": "Utilisateur",
|
||||
"xpack.securitySolution.overview.ctiDashboardDangerButton": "Activer les sources",
|
||||
"xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "Aucune donnée de Threat Intelligence disponible à afficher",
|
||||
"xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "Vous devez activer les sources de Threat Intelligence pour afficher les données.",
|
||||
"xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle": "Autres",
|
||||
"xpack.securitySolution.overview.ctiDashboardSubtitle": "Affichage : {totalCount} {totalCount, plural, one {indicateur} other {indicateurs}}",
|
||||
|
@ -26979,9 +26978,7 @@
|
|||
"xpack.securitySolution.overview.packetbeatTLSTitle": "TLS",
|
||||
"xpack.securitySolution.overview.recentTimelinesSidebarTitle": "Chronologies récentes",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardDangerPanelButton": "Activer le score de risque",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardDangerPanelTitle": "Aucune donnée de score de risque de l'hôte à afficher",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardEnableThreatIntel": "Veuillez activer le module de score de risque de l'hôte pour afficher la liste des hôtes à risque.",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardSubtitle": "Affichage : {totalCount} {totalCount, plural, one {hôte} other {hôtes}}",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardSubtitle": "Affichage : {totalCount} {totalCount, plural, one {hôte} other {hôtes}}",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardTitle": "Scores de risque de l'hôte actuel",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardWarningPanelBody": "Nous n'avons détecté aucune donnée de score de risque de l'hôte provenant des hôtes de votre environnement pour la plage temporelle sélectionnée.",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardWarningPanelTitle": "Aucune donnée de score de risque de l'hôte disponible pour l'affichage",
|
||||
|
|
|
@ -27005,7 +27005,6 @@
|
|||
"xpack.securitySolution.overview.auditBeatSocketTitle": "ソケット",
|
||||
"xpack.securitySolution.overview.auditBeatUserTitle": "ユーザー",
|
||||
"xpack.securitySolution.overview.ctiDashboardDangerButton": "ソースを有効にする",
|
||||
"xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "表示する脅威インテリジェンスデータがありません",
|
||||
"xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "データを表示するには、脅威インテリジェンスソースを有効にする必要があります。",
|
||||
"xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle": "その他",
|
||||
"xpack.securitySolution.overview.ctiDashboardSubtitle": "{totalCount} {totalCount, plural, other {個の指標}}を表示しています",
|
||||
|
@ -27059,8 +27058,6 @@
|
|||
"xpack.securitySolution.overview.packetbeatTLSTitle": "TLS",
|
||||
"xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近のタイムライン",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardDangerPanelButton": "リスクスコアを有効にする",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardDangerPanelTitle": "表示するホストリスクスコアデータがありません",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardEnableThreatIntel": "リスクの高いホストのリストを表示するには、ホストリスクスコアモジュールを有効にしてください。",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardSubtitle": "{totalCount} {totalCount, plural, other {個のホスト}}を表示しています",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardTitle": "現在のホストリスクスコア",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardWarningPanelBody": "選択した期間では、ご使用の環境のホストからホストリスクスコアデータが検出されませんでした。",
|
||||
|
|
|
@ -27032,7 +27032,6 @@
|
|||
"xpack.securitySolution.overview.auditBeatSocketTitle": "套接字",
|
||||
"xpack.securitySolution.overview.auditBeatUserTitle": "用户",
|
||||
"xpack.securitySolution.overview.ctiDashboardDangerButton": "启用源",
|
||||
"xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "没有可显示的威胁情报数据",
|
||||
"xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "您需要启用威胁情报源才能查看数据。",
|
||||
"xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle": "其他",
|
||||
"xpack.securitySolution.overview.ctiDashboardSubtitle": "正在显示:{totalCount} 个{totalCount, plural, other {指标}}",
|
||||
|
@ -27086,8 +27085,6 @@
|
|||
"xpack.securitySolution.overview.packetbeatTLSTitle": "TLS",
|
||||
"xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近的时间线",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardDangerPanelButton": "启用风险分数",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardDangerPanelTitle": "没有要显示的主机风险分数数据",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardEnableThreatIntel": "请启用主机风险分数模块以查看有风险的主机列表。",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardSubtitle": "正在显示:{totalCount} 台{totalCount, plural, other {主机}}",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardTitle": "当前主机风险分数",
|
||||
"xpack.securitySolution.overview.riskyHostsDashboardWarningPanelBody": "对于选定时间范围,我们尚未从您环境中的主机中检测到任何主机风险分数数据。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue