Skip to content

Commit

Permalink
[Security Solution] Asset criticality UI (#173512)
Browse files Browse the repository at this point in the history
This PR adds the UI to assign asset criticality in the old host flyout.
[#8086](elastic/security-team#8086)
  • Loading branch information
tiansivive authored Dec 27, 2023
1 parent 392d53c commit 121fb3a
Show file tree
Hide file tree
Showing 12 changed files with 622 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import type { AssetCriticalityRecord } from '../../../common/api/entity_analytics/asset_criticality';
import type { RiskScoreEntity } from '../../../common/search_strategy';
import {
RISK_ENGINE_STATUS_URL,
Expand All @@ -14,6 +15,7 @@ import {
RISK_ENGINE_INIT_URL,
RISK_ENGINE_PRIVILEGES_URL,
ASSET_CRITICALITY_PRIVILEGES_URL,
ASSET_CRITICALITY_URL,
RISK_SCORE_INDEX_STATUS_API_URL,
} from '../../../common/constants';

Expand All @@ -26,6 +28,8 @@ import type {
} from '../../../server/lib/entity_analytics/types';
import type { RiskScorePreviewRequestSchema } from '../../../common/entity_analytics/risk_engine/risk_score_preview/request_schema';
import type { EntityAnalyticsPrivileges } from '../../../common/api/entity_analytics/common';
import type { SnakeToCamelCase } from '../common/utils';

import { useKibana } from '../../common/lib/kibana/kibana_react';

export const useEntityAnalyticsRoutes = () => {
Expand Down Expand Up @@ -103,6 +107,35 @@ export const useEntityAnalyticsRoutes = () => {
method: 'GET',
});

/**
* Create asset criticality
*/
const createAssetCriticality = async (
params: Pick<AssetCriticality, 'idField' | 'idValue' | 'criticalityLevel'>
): Promise<AssetCriticalityRecord> =>
http.fetch<AssetCriticalityRecord>(ASSET_CRITICALITY_URL, {
version: '1',
method: 'POST',
body: JSON.stringify({
id_value: params.idValue,
id_field: params.idField,
criticality_level: params.criticalityLevel,
}),
});

/**
* Get asset criticality
*/
const fetchAssetCriticality = async (
params: Pick<AssetCriticality, 'idField' | 'idValue'>
): Promise<AssetCriticalityRecord> => {
return http.fetch<AssetCriticalityRecord>(ASSET_CRITICALITY_URL, {
version: '1',
method: 'GET',
query: { id_value: params.idValue, id_field: params.idField },
});
};

const getRiskScoreIndexStatus = ({
query,
signal,
Expand Down Expand Up @@ -132,6 +165,10 @@ export const useEntityAnalyticsRoutes = () => {
disableRiskEngine,
fetchRiskEnginePrivileges,
fetchAssetCriticalityPrivileges,
createAssetCriticality,
fetchAssetCriticality,
getRiskScoreIndexStatus,
};
};

export type AssetCriticality = SnakeToCamelCase<AssetCriticalityRecord>;
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ export const RISK_SCORE_RANGES = {
[RiskSeverity.critical]: { start: 90, stop: 100 },
};

type SnakeToCamelCaseString<S extends string> = S extends `${infer T}_${infer U}`
? `${T}${Capitalize<SnakeToCamelCaseString<U>>}`
: S;

type SnakeToCamelCaseArray<T> = T extends Array<infer ArrayItem>
? Array<SnakeToCamelCase<ArrayItem>>
: T;

// TODO #173073 @tiansivive Add to utilities in `packages/kbn-utility-types`
export type SnakeToCamelCase<T> = T extends Record<string, unknown>
? {
[K in keyof T as SnakeToCamelCaseString<K & string>]: SnakeToCamelCase<T[K]>;
}
: T extends unknown[]
? SnakeToCamelCaseArray<T>
: T;

export enum UserRiskScoreQueryId {
USERS_BY_RISK = 'UsersByRisk',
USER_DETAILS_RISK_SCORE = 'UserDetailsRiskScore',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { EuiSuperSelectOption } from '@elastic/eui';

import {
EuiAccordion,
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiLoadingSpinner,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiSuperSelect,
EuiText,
EuiHorizontalRule,
} from '@elastic/eui';

import React, { useState } from 'react';

import { FormattedMessage } from '@kbn/i18n-react';

import {
CRITICALITY_LEVEL_DESCRIPTION,
CRITICALITY_LEVEL_TITLE,
PICK_ASSET_CRITICALITY,
} from './translations';
import type { Entity, ModalState, State } from './use_asset_criticality';
import { useAssetCriticalityData, useCriticalityModal } from './use_asset_criticality';
import type { CriticalityLevel } from './common';
import { CRITICALITY_LEVEL_COLOR } from './common';

interface Props {
entity: Entity;
}
export const AssetCriticalitySelector: React.FC<Props> = ({ entity }) => {
const modal = useCriticalityModal();
const criticality = useAssetCriticalityData(entity, modal);

if (criticality.privileges.isLoading || !criticality.privileges.data?.has_all_required) {
return null;
}

return (
<>
<EuiHorizontalRule />
<EuiAccordion
id="asset-criticality-selector"
buttonContent={
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.accordionTitle"
defaultMessage="Asset Criticality"
/>
}
data-test-subj="asset-criticality-selector"
>
{criticality.query.isLoading || criticality.mutation.isLoading ? (
<EuiLoadingSpinner size="s" />
) : (
<EuiFlexGroup
direction="row"
alignItems="center"
justifyContent="spaceBetween"
wrap={false}
>
<EuiFlexItem>
<EuiText size="s">
{criticality.status === 'update' && criticality.query.data?.criticality_level ? (
<EuiHealth
data-test-subj="asset-criticality-level"
color={CRITICALITY_LEVEL_COLOR[criticality.query.data.criticality_level]}
>
{CRITICALITY_LEVEL_TITLE[criticality.query.data.criticality_level]}
</EuiHealth>
) : (
<EuiHealth color="subdued">
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.noCriticality"
defaultMessage="No criticality assigned yet"
/>
</EuiHealth>
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem css={{ flexGrow: 'unset' }}>
<EuiButtonEmpty
data-test-subj="asset-criticality-change-btn"
iconType="arrowStart"
iconSide="left"
flush="right"
onClick={() => modal.toggle(true)}
>
{criticality.status === 'update' ? (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.changeButton"
defaultMessage="Change"
/>
) : (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.createButton"
defaultMessage="Create"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiAccordion>
<EuiHorizontalRule />
{modal.visible ? (
<AssetCriticalityModal entity={entity} criticality={criticality} modal={modal} />
) : null}
</>
);
};

interface ModalProps {
criticality: State;
modal: ModalState;
entity: Entity;
}
const AssetCriticalityModal: React.FC<ModalProps> = ({ criticality, modal, entity }) => {
const [value, setNewValue] = useState<CriticalityLevel>(
criticality.query.data?.criticality_level ?? 'normal'
);

return (
<EuiModal onClose={() => modal.toggle(false)}>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="asset-criticality-modal-title">
{PICK_ASSET_CRITICALITY}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiSuperSelect
id={modal.basicSelectId}
options={options}
valueOfSelected={value}
onChange={setNewValue}
aria-label={PICK_ASSET_CRITICALITY}
data-test-subj="asset-criticality-modal-select"
/>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={() => modal.toggle(false)}>
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>

<EuiButton
onClick={() =>
criticality.mutation.mutate({
criticalityLevel: value,
idField: `${entity.type}.name`,
idValue: entity.name,
})
}
fill
data-test-subj="asset-criticality-modal-save-btn"
>
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.saveButton"
defaultMessage="Save"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

const option = (level: CriticalityLevel): EuiSuperSelectOption<CriticalityLevel> => ({
value: level,
dropdownDisplay: (
<EuiHealth
color={CRITICALITY_LEVEL_COLOR[level]}
style={{ lineHeight: 'inherit' }}
data-test-subj="asset-criticality-modal-select-option"
>
<strong>{CRITICALITY_LEVEL_TITLE[level]}</strong>
<EuiText size="s" color="subdued">
<p>{CRITICALITY_LEVEL_DESCRIPTION[level]}</p>
</EuiText>
</EuiHealth>
),
inputDisplay: (
<EuiHealth color={CRITICALITY_LEVEL_COLOR[level]} style={{ lineHeight: 'inherit' }}>
{CRITICALITY_LEVEL_TITLE[level]}
</EuiHealth>
),
});
const options: Array<EuiSuperSelectOption<CriticalityLevel>> = [
option('normal'),
option('not_important'),
option('important'),
option('very_important'),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { euiLightVars } from '@kbn/ui-theme';
import type { AssetCriticalityRecord } from '../../../../common/api/entity_analytics/asset_criticality';

export type CriticalityLevel = AssetCriticalityRecord['criticality_level'];

export const CRITICALITY_LEVEL_COLOR: Record<CriticalityLevel, string> = {
very_important: '#E7664C',
important: '#D6BF57',
normal: '#54B399',
not_important: euiLightVars.euiColorMediumShade,
};

// SUGGESTION: @tiansivive Move this to some more general place within Entity Analytics
export const buildCriticalityQueryKeys = (id: string) => {
const ASSET_CRITICALITY = 'ASSET_CRITICALITY';
const PRIVILEGES = 'PRIVILEGES';
return {
doc: [ASSET_CRITICALITY, id],
privileges: [ASSET_CRITICALITY, PRIVILEGES, id],
};
};
Loading

0 comments on commit 121fb3a

Please sign in to comment.