[Security Solution][Entity Analytics] Show contributions on risk explainability expanded tab (#179657)

This PR adds the contribution scores used when calculating an entity's
risk score.
These can be viewed by opening an entity's details flyout and clicking
on `View risk contributions` which will open the explainability tab.

The new tab now shows the contribution from different contexts and
individual alerts. Currently, there's only one context: Asset
Criticality.
The alert inputs shown are the top 10 (at most) alerts used as an input
at the time of the scoring calculation. If the final score used more
than 10 alerts, we display a message indicating the leftover sum
contribution.

This work also updates the server side in order to store the
contribution of each individual alert in the risk document itself. We
now query the document to retrieve the inputs and then fetch the
respective alerts on opening the tab.

Example:
![Screenshot 2024-04-05 at 14 34
24](a4efed46-05cd-4e31-9345-f46472358544)

### How to test

1. Generate some alerts - you can use
https://github.com/elastic/security-documents-generator
2. Enable risk scoring via `Security > Manage > Entity Risk Score`  
3. Open the entity details flyout and click `View risk contributions`

Make sure to enable Asset Criticality in `Stack Management > Advanced
Settings` if you want to see the criticality contributions.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tiago Vila Verde 2024-04-15 13:32:43 +02:00 committed by GitHub
parent 382be7ea3f
commit abb8f6bb31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 444 additions and 566 deletions

View file

@ -27,13 +27,14 @@ export interface InitRiskEngineResult {
errors: string[];
}
export interface SimpleRiskInput {
export interface EntityRiskInput {
id: string;
index: string;
category: RiskCategories;
description: string;
risk_score: string | number | undefined;
timestamp: string | undefined;
contribution_score?: number;
}
export interface EcsRiskScore {
@ -48,7 +49,7 @@ export interface EcsRiskScore {
};
}
export type RiskInputs = SimpleRiskInput[];
export type RiskInputs = EntityRiskInput[];
/**
* The API response object representing a risk score

View file

@ -1,105 +0,0 @@
/*
* 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 { getAlertsQueryForRiskScore } from './get_alerts_query_for_risk_score';
import type { RiskStats } from '../../../common/search_strategy/security_solution/risk_score/all';
import { RiskSeverity } from '../../../common/search_strategy/security_solution/risk_score/all';
const risk: RiskStats = {
calculated_level: RiskSeverity.critical,
calculated_score_norm: 70,
rule_risks: [],
multipliers: [],
'@timestamp': '',
id_field: '',
id_value: '',
calculated_score: 0,
category_1_score: 0,
category_1_count: 0,
category_2_score: 0,
category_2_count: 0,
notes: [],
inputs: [],
};
describe('getAlertsQueryForRiskScore', () => {
it('should return query from host risk score', () => {
expect(
getAlertsQueryForRiskScore({
riskScore: {
host: {
name: 'host-1',
risk,
},
'@timestamp': '2023-08-10T14:00:00.000Z',
},
riskRangeStart: 'now-30d',
})
).toEqual({
_source: false,
size: 1000,
fields: ['*'],
query: {
bool: {
filter: [
{ term: { 'host.name': 'host-1' } },
{
range: {
'@timestamp': { gte: '2023-07-11T14:00:00.000Z', lte: '2023-08-10T14:00:00.000Z' },
},
},
],
},
},
});
});
it('should return query from user risk score', () => {
expect(
getAlertsQueryForRiskScore({
riskScore: {
user: {
name: 'user-1',
risk,
},
'@timestamp': '2023-08-10T14:00:00.000Z',
},
riskRangeStart: 'now-30d',
})
).toEqual({
_source: false,
size: 1000,
fields: ['*'],
query: {
bool: {
filter: [
{ term: { 'user.name': 'user-1' } },
{
range: {
'@timestamp': { gte: '2023-07-11T14:00:00.000Z', lte: '2023-08-10T14:00:00.000Z' },
},
},
],
},
},
});
});
it('should return query with custom fields', () => {
const query = getAlertsQueryForRiskScore({
riskScore: {
user: {
name: 'user-1',
risk,
},
'@timestamp': '2023-08-10T14:00:00.000Z',
},
riskRangeStart: 'now-30d',
fields: ['event.category', 'event.action'],
});
expect(query.fields).toEqual(['event.category', 'event.action']);
});
});

View file

@ -1,69 +0,0 @@
/*
* 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 {
isUserRiskScore,
RiskScoreFields,
} from '../../../common/search_strategy/security_solution/risk_score/all';
import type {
UserRiskScore,
HostRiskScore,
} from '../../../common/search_strategy/security_solution/risk_score/all';
import { getStartDateFromRiskScore } from './get_start_date_from_risk_score';
const ALERTS_SIZE = 1000;
/**
* return query to fetch alerts related to the risk score
*/
export const getAlertsQueryForRiskScore = ({
riskRangeStart,
riskScore,
fields,
}: {
riskRangeStart: string;
riskScore: UserRiskScore | HostRiskScore;
fields?: string[];
}) => {
let entityField: string;
let entityValue: string;
if (isUserRiskScore(riskScore)) {
entityField = RiskScoreFields.userName;
entityValue = riskScore.user.name;
} else {
entityField = RiskScoreFields.hostName;
entityValue = riskScore.host.name;
}
const from = getStartDateFromRiskScore({
riskScoreTimestamp: riskScore['@timestamp'],
riskRangeStart,
});
const riskScoreTimestamp = riskScore['@timestamp'];
return {
fields: fields || ['*'],
size: ALERTS_SIZE,
_source: false,
query: {
bool: {
filter: [
{ term: { [entityField]: entityValue } },
{
range: {
'@timestamp': {
gte: from,
lte: riskScoreTimestamp,
},
},
},
],
},
},
};
};

View file

@ -8,14 +8,14 @@
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { alertDataMock } from '../mocks';
import { alertInputDataMock } from '../mocks';
import { ActionColumn } from './action_column';
describe('ActionColumn', () => {
it('renders', () => {
const { getByTestId } = render(
<TestProviders>
<ActionColumn alert={alertDataMock} />
<ActionColumn input={alertInputDataMock} />
</TestProviders>
);
@ -25,7 +25,7 @@ describe('ActionColumn', () => {
it('toggles the popover when button is clicked', () => {
const { getByRole } = render(
<TestProviders>
<ActionColumn alert={alertDataMock} />
<ActionColumn input={alertInputDataMock} />
</TestProviders>
);

View file

@ -7,20 +7,20 @@
import { EuiButtonIcon, EuiContextMenu, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo, useState } from 'react';
import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab';
import React, { useCallback, useState } from 'react';
import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts';
import { useRiskInputActionsPanels } from '../hooks/use_risk_input_actions_panels';
interface ActionColumnProps {
alert: AlertRawData;
input: InputAlert;
}
export const ActionColumn: React.FC<ActionColumnProps> = ({ alert }) => {
export const ActionColumn: React.FC<ActionColumnProps> = ({ input }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
const togglePopover = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), []);
const alerts = useMemo(() => [alert], [alert]);
const panels = useRiskInputActionsPanels(alerts, closePopover);
const panels = useRiskInputActionsPanels([input], closePopover);
return (
<EuiPopover

View file

@ -8,87 +8,34 @@
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { alertDataMock } from '../mocks';
import { alertInputDataMock } from '../mocks';
import { RiskInputsUtilityBar } from './utility_bar';
describe('RiskInputsUtilityBar', () => {
it('renders', () => {
it('renders when at least one item is selected', () => {
const { getByTestId } = render(
<TestProviders>
<RiskInputsUtilityBar
selectedAlerts={[]}
pagination={{
pageIndex: 0,
totalItemCount: 0,
}}
/>
<RiskInputsUtilityBar riskInputs={[alertInputDataMock]} />
</TestProviders>
);
expect(getByTestId('risk-input-utility-bar')).toBeInTheDocument();
});
it('renders current page message when totalItemCount is 1', () => {
const { getByTestId } = render(
it('is hidden by default', () => {
const { queryByTestId } = render(
<TestProviders>
<RiskInputsUtilityBar
selectedAlerts={[]}
pagination={{
pageIndex: 0,
totalItemCount: 1,
}}
/>
<RiskInputsUtilityBar riskInputs={[]} />
</TestProviders>
);
expect(getByTestId('risk-input-utility-bar')).toHaveTextContent('Showing 1 Risk contribution');
});
it('renders current page message when totalItemCount is 20', () => {
const { getByTestId } = render(
<TestProviders>
<RiskInputsUtilityBar
selectedAlerts={[]}
pagination={{
pageIndex: 0,
totalItemCount: 20,
}}
/>
</TestProviders>
);
expect(getByTestId('risk-input-utility-bar')).toHaveTextContent(
'Showing 1-10 of 20 Risk contribution'
);
});
it('renders current page message when totalItemCount is 20 and on the second page', () => {
const { getByTestId } = render(
<TestProviders>
<RiskInputsUtilityBar
selectedAlerts={[]}
pagination={{
pageIndex: 1,
totalItemCount: 20,
}}
/>
</TestProviders>
);
expect(getByTestId('risk-input-utility-bar')).toHaveTextContent(
'Showing 11-20 of 20 Risk contribution'
);
expect(queryByTestId('risk-input-utility-bar')).toBeNull();
});
it('renders selected risk input message', () => {
const { getByTestId } = render(
<TestProviders>
<RiskInputsUtilityBar
selectedAlerts={[alertDataMock, alertDataMock, alertDataMock]}
pagination={{
pageIndex: 0,
totalItemCount: 0,
}}
riskInputs={[alertInputDataMock, alertInputDataMock, alertInputDataMock]}
/>
</TestProviders>
);
@ -100,11 +47,7 @@ describe('RiskInputsUtilityBar', () => {
const { getByRole } = render(
<TestProviders>
<RiskInputsUtilityBar
selectedAlerts={[alertDataMock, alertDataMock, alertDataMock]}
pagination={{
pageIndex: 0,
totalItemCount: 0,
}}
riskInputs={[alertInputDataMock, alertInputDataMock, alertInputDataMock]}
/>
</TestProviders>
);

View file

@ -7,121 +7,79 @@
import type { FunctionComponent } from 'react';
import React, { useCallback, useState } from 'react';
import type { Pagination } from '@elastic/eui';
import {
EuiButtonEmpty,
EuiContextMenu,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiText,
EuiSpacer,
useEuiTheme,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { useRiskInputActionsPanels } from '../hooks/use_risk_input_actions_panels';
import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab';
import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts';
interface Props {
selectedAlerts: AlertRawData[];
pagination: Pagination;
riskInputs: InputAlert[];
}
export const RiskInputsUtilityBar: FunctionComponent<Props> = React.memo(
({ selectedAlerts, pagination }) => {
const { euiTheme } = useEuiTheme();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
const panels = useRiskInputActionsPanels(selectedAlerts, closePopover);
const displayedCurrentPage = pagination.pageIndex + 1;
const pageSize = pagination.pageSize ?? 10;
const fromItem: number = pagination.pageIndex * pageSize + 1;
const toItem: number = Math.min(pagination.totalItemCount, pageSize * displayedCurrentPage);
export const RiskInputsUtilityBar: FunctionComponent<Props> = React.memo(({ riskInputs }) => {
const { euiTheme } = useEuiTheme();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
const panels = useRiskInputActionsPanels(riskInputs, closePopover);
return (
<>
<EuiFlexGroup
data-test-subj="risk-input-utility-bar"
alignItems="center"
justifyContent="flexStart"
gutterSize="m"
>
<EuiFlexItem
grow={false}
css={css`
padding: ${euiTheme.size.s} 0;
`}
>
<EuiText size="xs">
{pagination.totalItemCount <= 1 ? (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextSingle"
defaultMessage="Showing {totalContributions} {riskInputs}"
values={{
totalContributions: pagination.totalItemCount,
riskInputs: (
<b>
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInput"
defaultMessage="Risk contribution"
/>
</b>
),
}}
/>
) : (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextRange"
defaultMessage="Showing {displayedRange} of {totalContributions} {riskContributions}"
values={{
displayedRange: <b>{`${fromItem}-${toItem}`}</b>,
totalContributions: pagination.totalItemCount,
riskContributions: (
<b>
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInputs"
defaultMessage="Risk contributions"
/>
</b>
),
}}
/>
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{selectedAlerts.length > 0 && (
<EuiPopover
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
button={
<EuiButtonEmpty
onClick={togglePopover}
size="xs"
iconSide="right"
iconType="arrowDown"
flush="left"
>
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.text"
defaultMessage="{totalSelectedContributions} selected risk contribution"
values={{
totalSelectedContributions: selectedAlerts.length,
}}
/>
</EuiButtonEmpty>
}
>
<EuiContextMenu panels={panels} initialPanelId={0} />
</EuiPopover>
)}
</EuiFlexItem>
</EuiFlexGroup>
</>
);
if (riskInputs.length === 0) {
return null;
}
);
return (
<>
<EuiFlexGroup
data-test-subj="risk-input-utility-bar"
alignItems="center"
justifyContent="flexStart"
gutterSize="m"
>
<EuiFlexItem
grow={false}
css={css`
padding: ${euiTheme.size.s} 0;
`}
/>
<EuiFlexItem grow={false}>
<EuiPopover
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
button={
<EuiButtonEmpty
onClick={togglePopover}
size="xs"
iconSide="right"
iconType="arrowDown"
flush="left"
>
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.text"
defaultMessage="{totalSelectedContributions} selected risk contribution"
values={{
totalSelectedContributions: riskInputs.length,
}}
/>
</EuiButtonEmpty>
}
>
<EuiContextMenu panels={panels} initialPanelId={0} />
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</>
);
});
RiskInputsUtilityBar.displayName = 'RiskInputsUtilityBar';

View file

@ -11,16 +11,17 @@ import { get, noop } from 'lodash/fp';
import { AttachmentType } from '@kbn/cases-plugin/common';
import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { useAddBulkToTimelineAction } from '../../../../detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab';
import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts';
/**
* The returned actions only support alerts risk inputs.
*/
export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () => void) => {
export const useRiskInputActions = (inputs: InputAlert[], closePopover: () => void) => {
const { from, to } = useGlobalTime();
const timelineAction = useAddBulkToTimelineAction({
localFilters: [],
@ -36,16 +37,16 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () =>
const caseAttachments: CaseAttachmentsWithoutOwner = useMemo(
() =>
alerts.map((alert: AlertRawData) => ({
alertId: alert._id,
index: alert._index,
inputs.map(({ input, alert }: InputAlert) => ({
alertId: input.id,
index: input.index,
type: AttachmentType.alert,
rule: {
id: get(ALERT_RULE_UUID, alert.fields)[0],
name: get(ALERT_RULE_NAME, alert.fields)[0],
id: get(ALERT_RULE_UUID, alert),
name: get(ALERT_RULE_NAME, alert),
},
})),
[alerts]
[inputs]
);
return useMemo(
@ -61,19 +62,19 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () =>
addToNewTimeline: () => {
telemetry.reportAddRiskInputToTimelineClicked({
quantity: alerts.length,
quantity: inputs.length,
});
closePopover();
timelineAction.onClick(
alerts.map((alert: AlertRawData) => {
inputs.map(({ input }: InputAlert) => {
return {
_id: alert._id,
_index: alert._index,
_id: input.id,
_index: input.index,
data: [],
ecs: {
_id: alert._id,
_index: alert._index,
_id: input.id,
_index: input.index,
},
};
}),
@ -85,7 +86,7 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () =>
},
}),
[
alerts,
inputs,
caseAttachments,
closePopover,
createCaseFlyout,

View file

@ -12,7 +12,7 @@ import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { alertDataMock } from '../mocks';
import { alertInputDataMock } from '../mocks';
import { useRiskInputActionsPanels } from './use_risk_input_actions_panels';
const casesServiceMock = casesPluginMock.createStartContract();
@ -47,7 +47,7 @@ const TestMenu = ({ panels }: { panels: EuiContextMenuPanelDescriptor[] }) => (
<EuiContextMenu initialPanelId={0} panels={panels} />
);
const customRender = (alerts = [alertDataMock]) => {
const customRender = (alerts = [alertInputDataMock]) => {
const { result } = renderHook(() => useRiskInputActionsPanels(alerts, () => {}), {
wrapper: TestProviders,
});
@ -67,7 +67,7 @@ describe('useRiskInputActionsPanels', () => {
});
it('displays number of selected alerts when more than one alert is selected', () => {
const { getByTestId } = customRender([alertDataMock, alertDataMock]);
const { getByTestId } = customRender([alertInputDataMock, alertInputDataMock]);
expect(getByTestId('contextMenuPanelTitle')).toHaveTextContent('2 selected');
});

View file

@ -15,12 +15,12 @@ import { i18n } from '@kbn/i18n';
import { get } from 'lodash/fp';
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import { useRiskInputActions } from './use_risk_input_actions';
import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab';
import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts';
export const useRiskInputActionsPanels = (alerts: AlertRawData[], closePopover: () => void) => {
export const useRiskInputActionsPanels = (inputs: InputAlert[], closePopover: () => void) => {
const { cases: casesService } = useKibana<{ cases?: CasesService }>().services;
const { addToExistingCase, addToNewCaseClick, addToNewTimeline } = useRiskInputActions(
alerts,
inputs,
closePopover
);
const userCasesPermissions = casesService?.helpers.canUseCases([SECURITY_SOLUTION_OWNER]);
@ -37,21 +37,21 @@ export const useRiskInputActionsPanels = (alerts: AlertRawData[], closePopover:
onClick: addToNewTimeline,
};
const ruleName = get(['fields', ALERT_RULE_NAME], alerts[0]) ?? [''];
const ruleName = get(['alert', ALERT_RULE_NAME], inputs[0]) ?? '';
const title = i18n.translate(
'xpack.securitySolution.flyout.entityDetails.riskInputs.actions.title',
{
defaultMessage: 'Risk input: {description}',
values: {
description:
alerts.length === 1
? ruleName[0]
inputs.length === 1
? ruleName
: i18n.translate(
'xpack.securitySolution.flyout.entityDetails.riskInputs.actions.titleDescription',
{
defaultMessage: '{quantity} selected',
values: {
quantity: alerts.length,
quantity: inputs.length,
},
}
),
@ -96,5 +96,5 @@ export const useRiskInputActionsPanels = (alerts: AlertRawData[], closePopover:
: [timelinePanel],
},
];
}, [addToExistingCase, addToNewCaseClick, addToNewTimeline, alerts, hasCasesPermissions]);
}, [addToExistingCase, addToNewCaseClick, addToNewTimeline, inputs, hasCasesPermissions]);
};

View file

@ -6,14 +6,22 @@
*/
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab';
import { RiskCategories } from '../../../../../common/entity_analytics/risk_engine';
import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts';
export const alertDataMock: AlertRawData = {
export const alertInputDataMock: InputAlert = {
_id: 'test-id',
_index: 'test-index',
fields: {
[ALERT_RULE_UUID]: ['2e051244-b3c6-4779-a241-e1b4f0beceb9'],
'@timestamp': ['2023-07-20T20:31:24.896Z'],
[ALERT_RULE_NAME]: ['Rule Name'],
input: {
id: 'test-id',
index: 'test-index',
category: RiskCategories.category_1,
description: 'test-description',
timestamp: '2023-07-20T20:31:24.896Z',
risk_score: 50,
contribution_score: 20,
},
alert: {
[ALERT_RULE_UUID]: '2e051244-b3c6-4779-a241-e1b4f0beceb9',
[ALERT_RULE_NAME]: 'Rule Name',
},
};

View file

@ -5,12 +5,12 @@
* 2.0.
*/
import { fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../../../common/mock';
import { times } from 'lodash/fp';
import { RiskInputsTab } from './risk_inputs_tab';
import { alertDataMock } from '../../mocks';
import { alertInputDataMock } from '../../mocks';
import { RiskSeverity } from '../../../../../../common/search_strategy';
import { RiskScoreEntity } from '../../../../../../common/entity_analytics/risk_engine';
@ -58,7 +58,7 @@ describe('RiskInputsTab', () => {
mockUseRiskContributingAlerts.mockReturnValue({
loading: false,
error: false,
data: [alertDataMock],
data: [alertInputDataMock],
});
mockUseRiskScore.mockReturnValue({
loading: false,
@ -93,7 +93,7 @@ describe('RiskInputsTab', () => {
it('Renders the context section if enabled and risks contains asset criticality', () => {
mockUseUiSetting.mockReturnValue([true]);
const riskScorewWithAssetCriticality = {
const riskScoreWithAssetCriticality = {
'@timestamp': '2021-08-19T16:00:00.000Z',
user: {
name: 'elastic',
@ -107,7 +107,7 @@ describe('RiskInputsTab', () => {
mockUseRiskScore.mockReturnValue({
loading: false,
error: false,
data: [riskScorewWithAssetCriticality],
data: [riskScoreWithAssetCriticality],
});
const { queryByTestId } = render(
@ -116,13 +116,13 @@ describe('RiskInputsTab', () => {
</TestProviders>
);
expect(queryByTestId('risk-input-asset-criticality-title')).toBeInTheDocument();
expect(queryByTestId('risk-input-contexts-title')).toBeInTheDocument();
});
it('paginates', () => {
it('shows extra alerts contribution message', () => {
const alerts = times(
(number) => ({
...alertDataMock,
...alertInputDataMock,
_id: number.toString(),
}),
11
@ -139,16 +139,12 @@ describe('RiskInputsTab', () => {
data: [riskScore],
});
const { getAllByTestId, getByLabelText } = render(
const { queryByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" />
</TestProviders>
);
expect(getAllByTestId('risk-input-table-description-cell')).toHaveLength(10);
fireEvent.click(getByLabelText('Next page'));
expect(getAllByTestId('risk-input-table-description-cell')).toHaveLength(1);
expect(queryByTestId('risk-input-extra-alerts-message')).toBeInTheDocument();
});
});

View file

@ -5,18 +5,23 @@
* 2.0.
*/
import type { EuiBasicTableColumn, Pagination } from '@elastic/eui';
import { EuiSpacer, EuiInMemoryTable, EuiTitle, EuiCallOut, EuiText } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiSpacer, EuiInMemoryTable, EuiTitle, EuiCallOut } from '@elastic/eui';
import type { ReactNode } from 'react';
import React, { useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { get } from 'lodash/fp';
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import { get } from 'lodash/fp';
import type {
InputAlert,
UseRiskContributingAlertsResult,
} from '../../../../hooks/use_risk_contributing_alerts';
import { useRiskContributingAlerts } from '../../../../hooks/use_risk_contributing_alerts';
import { ENABLE_ASSET_CRITICALITY_SETTING } from '../../../../../../common/constants';
import { PreferenceFormattedDate } from '../../../../../common/components/formatted_date';
import { ActionColumn } from '../../components/action_column';
import { RiskInputsUtilityBar } from '../../components/utility_bar';
import { useRiskContributingAlerts } from '../../../../hooks/use_risk_contributing_alerts';
import { useRiskScore } from '../../../../api/hooks/use_risk_score';
import type { HostRiskScore, UserRiskScore } from '../../../../../../common/search_strategy';
import {
@ -26,25 +31,21 @@ import {
} from '../../../../../../common/search_strategy';
import { RiskScoreEntity } from '../../../../../../common/entity_analytics/risk_engine';
import { AssetCriticalityBadge } from '../../../asset_criticality';
import { RiskInputsUtilityBar } from '../../components/utility_bar';
import { ActionColumn } from '../../components/action_column';
export interface RiskInputsTabProps extends Record<string, unknown> {
entityType: RiskScoreEntity;
entityName: string;
}
export interface AlertRawData {
fields: Record<string, string[]>;
_index: string;
_id: string;
}
const FIRST_RECORD_PAGINATION = {
cursorStart: 0,
querySize: 1,
};
export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) => {
const [selectedItems, setSelectedItems] = useState<AlertRawData[]>([]);
const [selectedItems, setSelectedItems] = useState<InputAlert[]>([]);
const nameFilterQuery = useMemo(() => {
if (entityType === RiskScoreEntity.host) {
@ -67,24 +68,19 @@ export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) =>
});
const riskScore = riskScoreData && riskScoreData.length > 0 ? riskScoreData[0] : undefined;
const {
loading: loadingAlerts,
data: alertsData,
error: riskAlertsError,
} = useRiskContributingAlerts({ riskScore });
const alerts = useRiskContributingAlerts({ riskScore });
const euiTableSelectionProps = useMemo(
() => ({
onSelectionChange: (selected: AlertRawData[]) => {
setSelectedItems(selected);
},
initialSelected: [],
selectable: () => true,
onSelectionChange: setSelectedItems,
}),
[]
);
const alertsColumns: Array<EuiBasicTableColumn<AlertRawData>> = useMemo(
const inputColumns: Array<EuiBasicTableColumn<InputAlert>> = useMemo(
() => [
{
name: (
@ -94,12 +90,10 @@ export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) =>
/>
),
width: '80px',
render: (alert: AlertRawData) => {
return <ActionColumn alert={alert} />;
},
render: (data: InputAlert) => <ActionColumn input={data} />,
},
{
field: 'fields.@timestamp',
field: 'input.timestamp',
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.dateColumn"
@ -113,7 +107,7 @@ export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) =>
render: (timestamp: string) => <PreferenceFormattedDate value={new Date(timestamp)} />,
},
{
field: 'fields',
field: 'alert',
'data-test-subj': 'risk-input-table-description-cell',
name: (
<FormattedMessage
@ -124,33 +118,30 @@ export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) =>
truncateText: true,
mobileOptions: { show: true },
sortable: true,
render: (fields: AlertRawData['fields']) => get(ALERT_RULE_NAME, fields),
render: (alert: InputAlert['alert']) => get(ALERT_RULE_NAME, alert),
},
{
field: 'input.contribution_score',
'data-test-subj': 'risk-input-table-contribution-cell',
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.contributionColumn"
defaultMessage="Contribution"
/>
),
truncateText: false,
mobileOptions: { show: true },
sortable: true,
align: 'right',
render: (contribution: number) => contribution.toFixed(2),
},
],
[]
);
const [currentPage, setCurrentPage] = useState<{
index: number;
size: number;
}>({ index: 0, size: 10 });
const onTableChange = useCallback(({ page }) => {
setCurrentPage(page);
}, []);
const pagination: Pagination = useMemo(
() => ({
totalItemCount: alertsData?.length ?? 0,
pageIndex: currentPage.index,
pageSize: currentPage.size,
}),
[currentPage.index, currentPage.size, alertsData?.length]
);
const [isAssetCriticalityEnabled] = useUiSetting$<boolean>(ENABLE_ASSET_CRITICALITY_SETTING);
if (riskScoreError || riskAlertsError) {
if (riskScoreError) {
return (
<EuiCallOut
title={
@ -183,79 +174,175 @@ export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) =>
</h3>
</EuiTitle>
<EuiSpacer size="xs" />
<RiskInputsUtilityBar pagination={pagination} selectedAlerts={selectedItems} />
<EuiSpacer size="xs" />
<RiskInputsUtilityBar riskInputs={selectedItems} />
<EuiInMemoryTable
compressed={true}
loading={loadingRiskScore || loadingAlerts}
items={alertsData ?? []}
columns={alertsColumns}
pagination
loading={loadingRiskScore || alerts.loading}
items={alerts.data || []}
columns={inputColumns}
sorting
selection={euiTableSelectionProps}
onTableChange={onTableChange}
isSelectable
itemId="_id"
/>
<EuiSpacer size="s" />
<ExtraAlertsMessage riskScore={riskScore} alerts={alerts} />
</>
);
return (
<>
{isAssetCriticalityEnabled && (
<RiskInputsAssetCriticalitySection loading={loadingRiskScore} riskScore={riskScore} />
<ContextsSection loading={loadingRiskScore} riskScore={riskScore} />
)}
<EuiSpacer size="m" />
{riskInputsAlertSection}
</>
);
};
const RiskInputsAssetCriticalitySection: React.FC<{
RiskInputsTab.displayName = 'RiskInputsTab';
const ContextsSection: React.FC<{
riskScore?: UserRiskScore | HostRiskScore;
loading: boolean;
}> = ({ riskScore, loading }) => {
const criticalityLevel = useMemo(() => {
const criticality = useMemo(() => {
if (!riskScore) {
return undefined;
}
if (isUserRiskScore(riskScore)) {
return riskScore.user.risk.criticality_level;
return {
level: riskScore.user.risk.criticality_level,
contribution: riskScore.user.risk.category_2_score,
};
}
return riskScore.host.risk.criticality_level;
return {
level: riskScore.host.risk.criticality_level,
contribution: riskScore.host.risk.category_2_score,
};
}, [riskScore]);
if (loading || criticalityLevel === undefined) {
if (loading || criticality === undefined) {
return null;
}
return (
<>
<EuiTitle size="xs" data-test-subj="risk-input-asset-criticality-title">
<EuiTitle size="xs" data-test-subj="risk-input-contexts-title">
<h3>
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityTitle"
defaultMessage="Asset Criticality"
id="xpack.securitySolution.flyout.entityDetails.riskInputs.contextsTitle"
defaultMessage="Contexts"
/>
</h3>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityDescription"
defaultMessage="The criticality assigned at the time of the risk score calculation."
/>
</EuiText>
<EuiSpacer size="s" />
<AssetCriticalityBadge
criticalityLevel={criticalityLevel}
dataTestSubj="risk-inputs-asset-criticality-badge"
<EuiInMemoryTable
compressed={true}
loading={loading}
columns={contextColumns}
items={[
{
field: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityField"
defaultMessage="Asset Criticality Level"
/>
),
value: (
<AssetCriticalityBadge
criticalityLevel={criticality.level}
dataTestSubj="risk-inputs-asset-criticality-badge"
/>
),
contribution: (criticality.contribution || 0).toFixed(2),
},
]}
/>
<EuiSpacer size="m" />
</>
);
};
RiskInputsTab.displayName = 'RiskInputsTab';
interface ContextRow {
field: ReactNode;
value: ReactNode;
contribution: string;
}
const contextColumns: Array<EuiBasicTableColumn<ContextRow>> = [
{
field: 'field',
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.fieldColumn"
defaultMessage="Field"
/>
),
width: '30%',
render: (field: ContextRow['field']) => field,
},
{
field: 'value',
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.valueColumn"
defaultMessage="Value"
/>
),
width: '30%',
render: (val: ContextRow['value']) => val,
},
{
field: 'contribution',
width: '30%',
align: 'right',
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.contributionColumn"
defaultMessage="Contribution"
/>
),
render: (score: ContextRow['contribution']) => score,
},
];
interface ExtraAlertsMessageProps {
riskScore?: UserRiskScore | HostRiskScore;
alerts: UseRiskContributingAlertsResult;
}
const ExtraAlertsMessage: React.FC<ExtraAlertsMessageProps> = ({ riskScore, alerts }) => {
const totals = !riskScore
? { count: 0, score: 0 }
: isUserRiskScore(riskScore)
? { count: riskScore.user.risk.category_1_count, score: riskScore.user.risk.category_1_score }
: { count: riskScore.host.risk.category_1_count, score: riskScore.host.risk.category_1_score };
const displayed = {
count: alerts.data?.length || 0,
score: alerts.data?.reduce((sum, { input }) => sum + (input.contribution_score || 0), 0) || 0,
};
if (displayed.count >= totals.count) {
return null;
}
return (
<EuiCallOut
data-test-subj="risk-input-extra-alerts-message"
size="s"
title={
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.extraAlertsMessage"
defaultMessage="{count} more alerts contributed {score} to the calculated risk score"
values={{
count: totals.count - displayed.count,
score: (totals.score - displayed.score).toFixed(2),
}}
/>
}
iconType="annotation"
/>
);
};

View file

@ -6,6 +6,9 @@
*/
import { useEffect } from 'react';
import type { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import type { EntityRiskInput } from '../../../common/entity_analytics/risk_engine';
import { useQueryAlerts } from '../../detections/containers/detection_engine/alerts/use_query';
import { ALERTS_QUERY_NAMES } from '../../detections/containers/detection_engine/alerts/constants';
@ -13,25 +16,33 @@ import type {
UserRiskScore,
HostRiskScore,
} from '../../../common/search_strategy/security_solution/risk_score/all';
import { getAlertsQueryForRiskScore } from '../common/get_alerts_query_for_risk_score';
import { useRiskEngineSettings } from '../api/hooks/use_risk_engine_settings';
import { isUserRiskScore } from '../../../common/search_strategy/security_solution/risk_score/all';
interface UseRiskContributingAlerts {
riskScore: UserRiskScore | HostRiskScore | undefined;
fields?: string[];
}
interface Hit {
fields: Record<string, string[]>;
interface AlertData {
[ALERT_RULE_UUID]: string;
[ALERT_RULE_NAME]: string;
}
interface AlertHit {
_id: string;
_index: string;
_source: AlertData;
}
export interface InputAlert {
alert: AlertData;
input: EntityRiskInput;
_id: string;
}
interface UseRiskContributingAlertsResult {
export interface UseRiskContributingAlertsResult {
loading: boolean;
error: boolean;
data?: Hit[];
data?: InputAlert[];
}
/**
@ -39,33 +50,48 @@ interface UseRiskContributingAlertsResult {
*/
export const useRiskContributingAlerts = ({
riskScore,
fields,
}: UseRiskContributingAlerts): UseRiskContributingAlertsResult => {
const { data: riskEngineSettings } = useRiskEngineSettings();
const { loading, data, setQuery } = useQueryAlerts<Hit, unknown>({
// is empty query, to skip fetching alert, until we have risk engine settings
const { loading, data, setQuery } = useQueryAlerts<AlertHit, unknown>({
query: {},
queryName: ALERTS_QUERY_NAMES.BY_ID,
});
useEffect(() => {
if (!riskEngineSettings?.range?.start || !riskScore) return;
const inputs = getInputs(riskScore);
setQuery(
getAlertsQueryForRiskScore({
riskRangeStart: riskEngineSettings.range.start,
riskScore,
fields,
})
);
}, [setQuery, riskScore, riskEngineSettings?.range?.start, fields]);
useEffect(() => {
if (!riskScore) return;
setQuery({
query: {
ids: {
values: inputs.map((input) => input.id),
},
},
});
}, [riskScore, inputs, setQuery]);
const error = !loading && data === undefined;
const alerts = inputs.map((input) => ({
_id: input.id,
input,
alert: (data?.hits.hits.find((alert) => alert._id === input.id)?._source || {}) as AlertData,
}));
return {
loading,
error,
data: data?.hits.hits,
data: alerts,
};
};
const getInputs = (riskScore?: UserRiskScore | HostRiskScore) => {
if (!riskScore) {
return [];
}
if (isUserRiskScore(riskScore)) {
return riskScore.user.risk.inputs;
}
return riskScore.host.risk.inputs;
};

View file

@ -10,17 +10,26 @@ import { render } from '@testing-library/react';
import React from 'react';
import { HostDetailsPanel } from '.';
import { TestProviders } from '../../../common/mock';
import type { HostRiskScore } from '../../../../common/search_strategy';
import { RiskSeverity } from '../../../../common/search_strategy';
const riskScore = {
const riskScore: HostRiskScore = {
'@timestamp': '2021-08-19T16:00:00.000Z',
host: {
name: 'elastic',
risk: {
'@timestamp': '2021-08-19T16:00:00.000Z',
id_field: 'host.name',
id_value: 'elastic',
rule_risks: [],
calculated_score_norm: 100,
calculated_score: 150,
category_1_score: 150,
category_1_count: 1,
multipliers: [],
calculated_level: RiskSeverity.critical,
inputs: [],
notes: [],
},
},
};

View file

@ -5,10 +5,6 @@
* 2.0.
*/
import {
ALERT_RISK_SCORE,
ALERT_RULE_NAME,
} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
import { RiskCategories, RiskLevels } from '../../../../common/entity_analytics/risk_engine';
import type { RiskScore } from '../../../../common/entity_analytics/risk_engine';
import type {
@ -28,36 +24,19 @@ const buildRiskScoreBucketMock = (overrides: Partial<RiskScoreBucket> = {}): Ris
notes: [],
category_1_score: 30,
category_1_count: 1,
},
},
inputs: {
took: 17,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 1,
relation: 'eq',
},
hits: [
risk_inputs: [
{
_id: '_id',
_index: '_index',
fields: {
'@timestamp': ['2023-07-20T20:31:24.896Z'],
[ALERT_RISK_SCORE]: [21],
[ALERT_RULE_NAME]: ['Rule Name'],
},
sort: [21],
id: 'test_id',
index: '_index',
rule_name: 'Test rule',
time: '2021-08-19T18:55:59.000Z',
score: 30,
contribution: 20,
},
],
},
},
doc_count: 2,
},
...overrides,
@ -108,6 +87,7 @@ const buildResponseMock = (
description: 'Alert from Rule: My rule',
risk_score: 30,
timestamp: '2021-08-19T18:55:59.000Z',
contribution_score: 20,
},
],
},

View file

@ -14,6 +14,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import {
ALERT_RISK_SCORE,
ALERT_RULE_NAME,
ALERT_UUID,
ALERT_WORKFLOW_STATUS,
EVENT_KIND,
} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
@ -48,6 +49,7 @@ import type {
RiskScoreBucket,
} from '../types';
import {
MAX_INPUTS_COUNT,
RISK_SCORING_INPUTS_COUNT_MAX,
RISK_SCORING_SUM_MAX,
RISK_SCORING_SUM_VALUE,
@ -67,7 +69,6 @@ const formatForResponse = ({
includeNewFields: boolean;
}): RiskScore => {
const riskDetails = bucket.top_inputs.risk_details;
const inputs = bucket.top_inputs.inputs;
const criticalityModifier = getCriticalityModifier(criticality?.criticality_level);
const normalizedScoreWithCriticality = applyCriticalityToScore({
@ -98,15 +99,14 @@ const formatForResponse = ({
}),
category_1_count: riskDetails.value.category_1_count,
notes: riskDetails.value.notes,
inputs: inputs.hits.hits.map((riskInput) => ({
id: riskInput._id,
index: riskInput._index,
description: `Alert from Rule: ${
riskInput.fields?.[ALERT_RULE_NAME]?.[0] ?? 'RULE_NOT_FOUND'
}`,
inputs: riskDetails.value.risk_inputs.map((riskInput) => ({
id: riskInput.id,
index: riskInput.index,
description: `Alert from Rule: ${riskInput.rule_name ?? 'RULE_NOT_FOUND'}`,
category: RiskCategories.category_1,
risk_score: riskInput.fields?.[ALERT_RISK_SCORE]?.[0] ?? undefined,
timestamp: riskInput.fields?.['@timestamp']?.[0] ?? undefined,
risk_score: riskInput.score,
timestamp: riskInput.time,
contribution_score: riskInput.contribution,
})),
...(includeNewFields ? newFields : {}),
};
@ -140,9 +140,15 @@ const buildReduceScript = ({
double total_score = 0;
double current_score = 0;
List risk_inputs = [];
for (int i = 0; i < num_inputs_to_score; i++) {
current_score = inputs[i].weighted_score / Math.pow(i + 1, params.p);
if (i < ${MAX_INPUTS_COUNT}) {
inputs[i]["contribution"] = 100 * current_score / params.risk_cap;
risk_inputs.add(inputs[i]);
}
${buildCategoryAssignment()}
total_score += current_score;
}
@ -151,6 +157,7 @@ const buildReduceScript = ({
double score_norm = 100 * total_score / params.risk_cap;
results['score'] = total_score;
results['normalized_score'] = score_norm;
results['risk_inputs'] = risk_inputs;
return results;
`;
@ -191,14 +198,8 @@ const buildIdentifierTypeAggregation = ({
sampler: {
shard_size: alertSampleSizePerShard,
},
aggs: {
inputs: {
top_hits: {
size: 5,
_source: false,
docvalue_fields: ['@timestamp', ALERT_RISK_SCORE, ALERT_RULE_NAME],
},
},
risk_details: {
scripted_metric: {
init_script: 'state.inputs = []',
@ -209,8 +210,13 @@ const buildIdentifierTypeAggregation = ({
double weighted_score = 0.0;
fields.put('time', doc['@timestamp'].value);
fields.put('rule_name', doc['${ALERT_RULE_NAME}'].value);
fields.put('category', category);
fields.put('index', doc['_index'].value);
fields.put('id', doc['${ALERT_UUID}'].value);
fields.put('score', score);
${buildWeightingOfScoreByCategory({ userWeights: weights, identifierType })}
fields.put('weighted_score', weighted_score);
@ -308,7 +314,6 @@ export const calculateRiskScores = async ({
filter.push(userFilter as QueryDslQueryContainer);
}
const identifierTypes: IdentifierType[] = identifierType ? [identifierType] : ['host', 'user'];
const request = {
size: 0,
_source: false,

View file

@ -25,3 +25,8 @@ export const RISK_SCORING_INPUTS_COUNT_MAX = 999999;
* This value represents the maximum possible risk score after normalization.
*/
export const RISK_SCORING_NORMALIZATION_MAX = 100;
/**
* This value represents the max amount of alert inputs we store, per entity, in the risk document.
*/
export const MAX_INPUTS_COUNT = 10;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { MappingRuntimeFields, SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import type {
AfterKey,
AfterKeys,
@ -113,6 +113,15 @@ export interface CalculateRiskScoreAggregations {
};
}
export interface SearchHitRiskInput {
id: string;
index: string;
rule_name?: string;
time?: string;
score?: number;
contribution?: number;
}
export interface RiskScoreBucket {
key: { [identifierField: string]: string };
doc_count: number;
@ -125,9 +134,9 @@ export interface RiskScoreBucket {
notes: string[];
category_1_score: number;
category_1_count: number;
risk_inputs: SearchHitRiskInput[];
};
};
inputs: SearchResponse;
};
}

View file

@ -33024,8 +33024,6 @@
"xpack.securitySolution.flyout.entityDetails.observedEntityUpdatedTime": "Mis à jour le {time}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.title": "Entrée des risques : {description}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.titleDescription": "{quantity} sélectionnée",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextRange": "Affichage de {displayedRange} sur {totalContributions} {riskContributions}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextSingle": "Affichage de {totalContributions} {riskInputs}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.text": "{totalSelectedContributions} contributions de risque sélectionnées",
"xpack.securitySolution.flyout.entityDetails.riskUpdatedTime": "Mis à jour le {time}",
"xpack.securitySolution.flyout.entityDetails.title": "Sommaire des risques de {entity} ",
@ -36390,14 +36388,10 @@
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.ariaLabel": "Actions",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actionsColumn": "Actions",
"xpack.securitySolution.flyout.entityDetails.riskInputs.alertsTitle": "Alertes",
"xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityDescription": "La criticité affectée au moment du calcul du score de risque.",
"xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityTitle": "Criticité des ressources",
"xpack.securitySolution.flyout.entityDetails.riskInputs.dateColumn": "Date",
"xpack.securitySolution.flyout.entityDetails.riskInputs.errorBody": "Erreur lors de la récupération des entrées des risques. Réessayez plus tard.",
"xpack.securitySolution.flyout.entityDetails.riskInputs.errorTitle": "Un problème est survenu",
"xpack.securitySolution.flyout.entityDetails.riskInputs.riskInputColumn": "Contribution au risque",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInput": "Contribution au risque",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInputs": "Contributions au risque",
"xpack.securitySolution.flyout.entityDetails.riskSummary.casesAttachmentLabel": "Score de risque pour {entityType, select, host {host} user {user}} {entityName}",
"xpack.securitySolution.flyout.entityDetails.scoreColumnLabel": "Score",
"xpack.securitySolution.flyout.entityDetails.showAllRiskInputs": "Montrer toutes les entrées des risques",

View file

@ -32991,8 +32991,6 @@
"xpack.securitySolution.flyout.entityDetails.observedEntityUpdatedTime": "更新日時{time}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.title": "リスクインプット:{description}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.titleDescription": "{quantity}選択済み",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextRange": "{displayedRange}/{totalContributions} {riskContributions}を表示しています",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextSingle": "{totalContributions} {riskInputs}を表示しています",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.text": "{totalSelectedContributions}選択されたリスク寄与",
"xpack.securitySolution.flyout.entityDetails.riskUpdatedTime": "更新日時{time}",
"xpack.securitySolution.flyout.entityDetails.title": "{entity}リスク概要",
@ -36359,14 +36357,10 @@
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.ariaLabel": "アクション",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actionsColumn": "アクション",
"xpack.securitySolution.flyout.entityDetails.riskInputs.alertsTitle": "アラート",
"xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityDescription": "リスクスコア計算時に割り当てられた重要度。",
"xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityTitle": "アセット重要度",
"xpack.securitySolution.flyout.entityDetails.riskInputs.dateColumn": "日付",
"xpack.securitySolution.flyout.entityDetails.riskInputs.errorBody": "リスク情報の取得中にエラーが発生しました。しばらくたってから再試行してください。",
"xpack.securitySolution.flyout.entityDetails.riskInputs.errorTitle": "問題が発生しました",
"xpack.securitySolution.flyout.entityDetails.riskInputs.riskInputColumn": "リスク寄与",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInput": "リスク寄与",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInputs": "リスク寄与",
"xpack.securitySolution.flyout.entityDetails.riskSummary.casesAttachmentLabel": "{entityType, select, host {ホスト} user {ユーザー}} {entityName}のリスクスコア",
"xpack.securitySolution.flyout.entityDetails.scoreColumnLabel": "スコア",
"xpack.securitySolution.flyout.entityDetails.showAllRiskInputs": "すべてのリスクインプットを表示",

View file

@ -33035,8 +33035,6 @@
"xpack.securitySolution.flyout.entityDetails.observedEntityUpdatedTime": "已更新 {time}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.title": "风险输入:{description}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.titleDescription": "{quantity} 个已选定",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextRange": "正在显示 {displayedRange} 个(共 {totalContributions} 个){riskContributions}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextSingle": "正在显示 {totalContributions} 个{riskInputs}",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.text": "{totalSelectedContributions} 个选定的风险贡献",
"xpack.securitySolution.flyout.entityDetails.riskUpdatedTime": "已更新 {time}",
"xpack.securitySolution.flyout.entityDetails.title": "{entity} 风险摘要",
@ -36402,14 +36400,10 @@
"xpack.securitySolution.flyout.entityDetails.riskInputs.actions.ariaLabel": "操作",
"xpack.securitySolution.flyout.entityDetails.riskInputs.actionsColumn": "操作",
"xpack.securitySolution.flyout.entityDetails.riskInputs.alertsTitle": "告警",
"xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityDescription": "在计算风险分数时分配的关键度。",
"xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityTitle": "资产关键度",
"xpack.securitySolution.flyout.entityDetails.riskInputs.dateColumn": "日期",
"xpack.securitySolution.flyout.entityDetails.riskInputs.errorBody": "提取风险输入时出错。请稍后重试。",
"xpack.securitySolution.flyout.entityDetails.riskInputs.errorTitle": "出问题了",
"xpack.securitySolution.flyout.entityDetails.riskInputs.riskInputColumn": "风险贡献",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInput": "风险贡献",
"xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInputs": "风险贡献",
"xpack.securitySolution.flyout.entityDetails.riskSummary.casesAttachmentLabel": "{entityType, select, host {主机} user {用户}} {entityName} 的风险分数",
"xpack.securitySolution.flyout.entityDetails.scoreColumnLabel": "分数",
"xpack.securitySolution.flyout.entityDetails.showAllRiskInputs": "显示所有风险输入",

View file

@ -22,6 +22,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "New Rule Test",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
},
@ -30,6 +31,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "New Rule Test",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -64,6 +66,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -99,6 +102,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -133,6 +137,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -167,6 +172,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -201,6 +207,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -238,6 +245,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Endpoint Security",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -273,6 +281,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -307,6 +316,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -341,6 +351,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -375,6 +386,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -409,6 +421,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -446,6 +459,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Endpoint Security",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -480,6 +494,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -516,6 +531,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "New Rule Test",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
},
@ -524,6 +540,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "New Rule Test",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -559,6 +576,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -595,6 +613,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -630,6 +649,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -665,6 +685,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -700,6 +721,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -738,6 +760,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Endpoint Security",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -774,6 +797,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -809,6 +833,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -844,6 +869,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -879,6 +905,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -914,6 +941,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -952,6 +980,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Endpoint Security",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
@ -987,6 +1016,7 @@
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"contribution_score": 50,
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}

View file

@ -64,6 +64,9 @@
},
"timestamp": {
"type": "date"
},
"contribution_score": {
"type": "float"
}
}
},
@ -130,6 +133,9 @@
},
"timestamp": {
"type": "date"
},
"contribution_score": {
"type": "float"
}
}
},
@ -222,6 +228,9 @@
},
"timestamp": {
"type": "date"
},
"contribution_score": {
"type": "float"
}
}
},
@ -279,6 +288,9 @@
},
"timestamp": {
"type": "date"
},
"contribution_score": {
"type": "float"
}
}
},