Skip to content

Commit

Permalink
[Cloud Security] User Name Misconfiguration Table and Preview Context…
Browse files Browse the repository at this point in the history
…ual Flyout (#192946)

## Summary

This PR is the implementation of Misconfiguration Preview and Data table
on user.name flyout in Alerts Page.
<img width="1717" alt="Screenshot 2024-09-14 at 12 54 37 AM"
src="https://github.com/user-attachments/assets/ad405a4a-9820-4bb1-87f0-7e915eeb003b">
How to test:
Pre req: In order to test this, you need to generate some fake alerts.
This [repo](https://github.com/elastic/security-documents-generator)
will help you do that

1. Generate Some Alerts
2. Use the Reindex API to get some Findings data in (change the
host.name field to match the host.name from alerts generated if you want
to test Findings table in the left panel flyout)
3. Turn on Risky Entity Score if you want to test if both Risk
Contribution and Insights tabs shows up, follow this
[guide](https://www.elastic.co/guide/en/security/current/turn-on-risk-engine.html)
to turn on Risk Entity Score
  • Loading branch information
animehart authored Sep 18, 2024
1 parent b9d7de6 commit 4d4afa5
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,16 @@
*/

import React, { memo } from 'react';
import { EuiButtonGroup, EuiSpacer } from '@elastic/eui';
import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/button/button_group/button_group';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useExpandableFlyoutState } from '@kbn/expandable-flyout';
import { EuiSpacer } from '@elastic/eui';
import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table';

enum InsightsTabCspTab {
MISCONFIGURATION = 'misconfigurationTabId',
}

const insightsButtons: EuiButtonGroupOptionProps[] = [
{
id: InsightsTabCspTab.MISCONFIGURATION,
label: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.misconfigurationButtonLabel"
defaultMessage="Misconfiguration"
/>
),
'data-test-subj': 'misconfigurationTabDataTestId',
},
];

/**
* Insights view displayed in the document details expandable flyout left section
*/
export const InsightsTabCsp = memo(
({ name, fieldName }: { name: string; fieldName: 'host.name' | 'user.name' }) => {
const panels = useExpandableFlyoutState();
const activeInsightsId = panels.left?.path?.subTab ?? 'misconfigurationTabId';

return (
<>
<EuiButtonGroup
color="primary"
legend={i18n.translate(
'xpack.securitySolution.flyout.left.insights.optionsButtonGroups',
{
defaultMessage: 'Insights options',
}
)}
options={insightsButtons}
idSelected={activeInsightsId}
onChange={() => {}}
buttonSize="compressed"
isFullWidth
data-test-subj={'insightButtonGroupsTestId'}
/>
<EuiSpacer size="xl" />
<MisconfigurationFindingsDetailsTable fieldName={fieldName} queryName={name} />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ export const MisconfigurationFindingsDetailsTable = memo(

const navToFindings = useNavigateFindings();

const navToFindingsByHostName = (hostName: string) => {
navToFindings({ 'host.name': hostName }, ['rule.name']);
};

const navToFindingsByRuleAndResourceId = (ruleId: string, resourceId: string) => {
navToFindings({ 'rule.id': ruleId, 'resource.id': resourceId });
};

const navToFindingsByName = (name: string, queryField: 'host.name' | 'user.name') => {
navToFindings({ [queryField]: name }, ['rule.name']);
};

const columns: Array<EuiBasicTableColumn<MisconfigurationFindingDetailFields>> = [
{
field: 'rule',
Expand Down Expand Up @@ -154,13 +154,13 @@ export const MisconfigurationFindingsDetailsTable = memo(
<EuiPanel hasShadow={false}>
<EuiLink
onClick={() => {
navToFindingsByHostName(queryName);
navToFindingsByName(queryName, fieldName);
}}
>
{i18n.translate(
'xpack.securitySolution.flyout.left.insights.misconfigurations.tableTitle',
{
defaultMessage: 'Misconfigurations',
defaultMessage: 'Misconfigurations ',
}
)}
<EuiIcon type={'popout'} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useCspSetupStatusApi } from '@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api';
import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview';

export const EntityInsight = <T,>({ hostName }: { hostName: string }) => {
export const EntityInsight = <T,>({
name,
fieldName,
isPreviewMode,
}: {
name: string;
fieldName: 'host.name' | 'user.name';
isPreviewMode?: boolean;
}) => {
const { euiTheme } = useEuiTheme();
const getSetupStatus = useCspSetupStatusApi();
const hasMisconfigurationFindings = getSetupStatus.data?.hasMisconfigurationsFindings;
Expand All @@ -22,7 +30,6 @@ export const EntityInsight = <T,>({ hostName }: { hostName: string }) => {
<>
{hasMisconfigurationFindings && (
<>
<EuiHorizontalRule />
<EuiAccordion
initialIsOpen={true}
id="entityInsight-accordion"
Expand All @@ -45,9 +52,14 @@ export const EntityInsight = <T,>({ hostName }: { hostName: string }) => {
}
>
<EuiSpacer size="m" />
<MisconfigurationsPreview hostName={hostName} />
<MisconfigurationsPreview
name={name}
fieldName={fieldName}
isPreviewMode={isPreviewMode}
/>
<EuiSpacer size="m" />
</EuiAccordion>
<EuiHorizontalRule />
</>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { render } from '@testing-library/react';
import React from 'react';
import { MisconfigurationsPreview } from './misconfiguration_preview';

const mockProps = {
hostName: 'testContextID',
const mockProps: { name: string; fieldName: 'host.name' | 'user.name' } = {
name: 'testContextID',
fieldName: 'host.name',
};

describe('MisconfigurationsPreview', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import { i18n } from '@kbn/i18n';
import { ExpandablePanel } from '@kbn/security-solution-common';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { UserDetailsPanelKey } from '../../../flyout/entity_details/user_details_left';
import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left';
import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine';
import { buildHostNamesFilter } from '../../../../common/search_strategy';
import type { HostRiskScore, UserRiskScore } from '../../../../common/search_strategy';
import { buildHostNamesFilter, buildUserNamesFilter } from '../../../../common/search_strategy';

const FIRST_RECORD_PAGINATION = {
cursorStart: 0,
Expand Down Expand Up @@ -120,46 +122,63 @@ const MisconfigurationPreviewScore = ({
);
};

export const MisconfigurationsPreview = ({ hostName }: { hostName: string }) => {
export const MisconfigurationsPreview = ({
name,
fieldName,
isPreviewMode,
}: {
name: string;
fieldName: 'host.name' | 'user.name';
isPreviewMode?: boolean;
}) => {
const { data } = useMisconfigurationPreview({
query: buildEntityFlyoutPreviewQuery('host.name', hostName),
query: buildEntityFlyoutPreviewQuery(fieldName, name),
sort: [],
enabled: true,
pageSize: 1,
});

const isUsingHostName = fieldName === 'host.name';
const passedFindings = data?.count.passed || 0;
const failedFindings = data?.count.failed || 0;

const { euiTheme } = useEuiTheme();
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
const hostNameFilterQuery = useMemo(
() => (hostName ? buildHostNamesFilter([hostName]) : undefined),
[hostName]

const buildFilterQuery = useMemo(
() => (isUsingHostName ? buildHostNamesFilter([name]) : buildUserNamesFilter([name])),
[isUsingHostName, name]
);

const riskScoreState = useRiskScore({
riskEntity: RiskScoreEntity.host,
filterQuery: hostNameFilterQuery,
riskEntity: isUsingHostName ? RiskScoreEntity.host : RiskScoreEntity.user,
filterQuery: buildFilterQuery,
onlyLatest: false,
pagination: FIRST_RECORD_PAGINATION,
});
const { data: hostRisk } = riskScoreState;
const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined;
const isRiskScoreExist = !!hostRiskData?.host.risk;
const riskData = hostRisk?.[0];
const isRiskScoreExist = isUsingHostName
? !!(riskData as HostRiskScore)?.host.risk
: !!(riskData as UserRiskScore)?.user.risk;
const { openLeftPanel } = useExpandableFlyoutApi();
const isPreviewMode = false;
const goToEntityInsightTab = useCallback(() => {
openLeftPanel({
id: HostDetailsPanelKey,
params: {
name: hostName,
isRiskScoreExist,
hasMisconfigurationFindings,
path: { tab: 'csp_insights' },
},
id: isUsingHostName ? HostDetailsPanelKey : UserDetailsPanelKey,
params: isUsingHostName
? {
name,
isRiskScoreExist,
hasMisconfigurationFindings,
path: { tab: 'csp_insights' },
}
: {
user: { name },
isRiskScoreExist,
hasMisconfigurationFindings,
path: { tab: 'csp_insights' },
},
});
}, [hasMisconfigurationFindings, hostName, isRiskScoreExist, openLeftPanel]);
}, [hasMisconfigurationFindings, isRiskScoreExist, isUsingHostName, name, openLeftPanel]);
const link = useMemo(
() =>
!isPreviewMode
Expand All @@ -178,7 +197,7 @@ export const MisconfigurationsPreview = ({ hostName }: { hostName: string }) =>
return (
<ExpandablePanel
header={{
iconType: hasMisconfigurationFindings ? 'arrowStart' : '',
iconType: !isPreviewMode && hasMisconfigurationFindings ? 'arrowStart' : '',
title: (
<EuiText
size="xs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const HostPanelContent = ({
entity={{ name: hostName, type: 'host' }}
onChange={onAssetCriticalityChange}
/>
<EntityInsight name={hostName} fieldName={'host.name'} isPreviewMode={isPreviewMode} />
<ObservedEntity
observedData={observedHost}
contextID={contextID}
Expand All @@ -72,7 +73,6 @@ export const HostPanelContent = ({
observedFields={observedFields}
queryId={HOST_PANEL_OBSERVED_HOST_QUERY_ID}
/>
<EntityInsight hostName={hostName} />
</FlyoutBody>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,46 @@ describe('LeftPanel', () => {

expect(tabElement).not.toBeInTheDocument();
});

it("doesn't render insights panel when there no misconfiguration findings", () => {
const { queryByText } = render(
<UserDetailsPanel
path={{
tab: EntityDetailsLeftPanelTab.RISK_INPUTS,
}}
isRiskScoreExist
user={{ name: 'test user', email: [] }}
scopeId={'scopeId'}
hasMisconfigurationFindings={false}
/>,
{
wrapper: TestProviders,
}
);

const tabElement = queryByText('Insights');

expect(tabElement).not.toBeInTheDocument();
});

it('render insights panel when there are misconfiguration findings', () => {
const { queryByText } = render(
<UserDetailsPanel
path={{
tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
}}
isRiskScoreExist
user={{ name: 'test user', email: [] }}
scopeId={'scopeId'}
hasMisconfigurationFindings={true}
/>,
{
wrapper: TestProviders,
}
);

const tabElement = queryByText('Insights');

expect(tabElement).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface UserDetailsPanelProps extends Record<string, unknown> {
user: UserParam;
path?: PanelPath;
scopeId: string;
hasMisconfigurationFindings?: boolean;
}
export interface UserDetailsExpandableFlyoutProps extends FlyoutPanelProps {
key: 'user_details';
Expand All @@ -40,10 +41,24 @@ export const UserDetailsPanel = ({
user,
path,
scopeId,
hasMisconfigurationFindings,
}: UserDetailsPanelProps) => {
const managedUser = useManagedUser(user.name, user.email);
const tabs = useTabs(managedUser.data, user.name, isRiskScoreExist, scopeId);
const { selectedTabId, setSelectedTabId } = useSelectedTab(isRiskScoreExist, user, tabs, path);
const tabs = useTabs(
managedUser.data,
user.name,
isRiskScoreExist,
scopeId,
hasMisconfigurationFindings
);

const { selectedTabId, setSelectedTabId } = useSelectedTab(
isRiskScoreExist,
user,
tabs,
path,
hasMisconfigurationFindings
);

if (managedUser.isLoading) return <FlyoutLoading />;

Expand All @@ -67,7 +82,8 @@ const useSelectedTab = (
isRiskScoreExist: boolean,
user: UserParam,
tabs: LeftPanelTabsType,
path: PanelPath | undefined
path: PanelPath | undefined,
hasMisconfigurationFindings?: boolean
) => {
const { openLeftPanel } = useExpandableFlyoutApi();

Expand All @@ -81,12 +97,13 @@ const useSelectedTab = (
const setSelectedTabId = (tabId: EntityDetailsLeftPanelTab) => {
openLeftPanel({
id: UserDetailsPanelKey,
path: {
tab: tabId,
},
params: {
user,
isRiskScoreExist,
hasMisconfigurationFindings,
path: {
tab: tabId,
},
},
});
};
Expand Down
Loading

0 comments on commit 4d4afa5

Please sign in to comment.