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:
Angela Chuang 2022-07-25 22:41:41 +01:00 committed by GitHub
parent 141b765568
commit 5b465b9f99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2648 additions and 92 deletions

View file

@ -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.

View file

@ -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');
});

View file

@ -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"]';

View file

@ -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;
};

View file

@ -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!!',
});
});
});
});

View file

@ -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);

View file

@ -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`,
}
);

View file

@ -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();
});
});
});

View file

@ -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';

View file

@ -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: {

View file

@ -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
*

View file

@ -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 {

View file

@ -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 };
};

View file

@ -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();
});
});

View file

@ -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';

View file

@ -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();
});
});

View file

@ -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>

View file

@ -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;
}

View file

@ -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',
}
);

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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';

View file

@ -75,5 +75,6 @@ describe('RiskyHostsEnabledModule', () => {
</Provider>
);
expect(screen.getByTestId('risky-hosts-dashboard-links')).toBeInTheDocument();
expect(screen.getByTestId('create-saved-object-success-button')).toBeInTheDocument();
});
});

View file

@ -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}
/>
);
};

View file

@ -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'
);
});
});

View file

@ -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);

View file

@ -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',
}
);

View file

@ -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,
});
}
}
);
};

View file

@ -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',
};

View file

@ -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

View file

@ -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
"
`;

View file

@ -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();
});
});

View file

@ -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,
});
}
}
);
};

View file

@ -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,
});
});
});

View file

@ -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')]),
}),
};

View file

@ -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;
};

View file

@ -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',
},
];

View file

@ -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);
});
});

View file

@ -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,
});
}
}
);
};

View file

@ -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',
},
];

View file

@ -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';

View file

@ -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,
});
});
});

View file

@ -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')]),
}),
};

View file

@ -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';

View file

@ -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);

View file

@ -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",

View file

@ -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": "選択した期間では、ご使用の環境のホストからホストリスクスコアデータが検出されませんでした。",

View file

@ -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": "对于选定时间范围,我们尚未从您环境中的主机中检测到任何主机风险分数数据。",