mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:

### 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:
parent
382be7ea3f
commit
abb8f6bb31
24 changed files with 444 additions and 566 deletions
|
@ -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
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "すべてのリスクインプットを表示",
|
||||
|
|
|
@ -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": "显示所有风险输入",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue