Skip to content

Commit bc54229

Browse files
authored
[Contextual Security] Vulnerability Flyout Updates (#233919)
## Summary This PR does the following - Updated Description section of Vulnerability Flyout, it now has the option show more and show less when there are more than 3 line of description - Updated Cards on the Header, it now has 4 cards with one of them being CVE. CVE in the title is now moved into this card. In case there are no CVE, it will render "-" in the card https://github.com/user-attachments/assets/6b9cf6f4-5fba-4076-9745-e5f9ba8ca400
1 parent 67ef069 commit bc54229

File tree

5 files changed

+214
-89
lines changed

5 files changed

+214
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { useEffect, useRef, useState } from 'react';
9+
import { EuiButtonEmpty, useEuiTheme } from '@elastic/eui';
10+
import { FormattedMessage } from '@kbn/i18n-react';
11+
import { CspFlyoutMarkdown } from '../../configurations/findings_flyout/findings_flyout';
12+
13+
const EMPTY_VALUE = 'No description available';
14+
const MAX_LINES = 3;
15+
16+
interface ClampedMarkdownProps {
17+
description?: string;
18+
}
19+
20+
export const VulnerabilityDescriptionSection: React.FC<ClampedMarkdownProps> = ({
21+
description,
22+
}) => {
23+
const { euiTheme } = useEuiTheme();
24+
const [isExpanded, setIsExpanded] = useState(false);
25+
const [isOverflowing, setIsOverflowing] = useState(false);
26+
const contentRef = useRef<HTMLDivElement>(null);
27+
const containerRef = useRef<HTMLDivElement>(null);
28+
29+
const content = description || EMPTY_VALUE;
30+
31+
const checkOverflow = () => {
32+
const el = contentRef.current;
33+
if (el) {
34+
const style = getComputedStyle(el);
35+
const lineHeightRaw = style.lineHeight;
36+
const lineHeight = lineHeightRaw === 'normal' ? 16 : parseFloat(lineHeightRaw);
37+
const maxHeight = lineHeight * MAX_LINES;
38+
setIsOverflowing(el.scrollHeight > maxHeight + 1);
39+
}
40+
};
41+
42+
useEffect(() => {
43+
// Wait for layout to stabilize before measuring
44+
requestAnimationFrame(checkOverflow);
45+
}, [content, isExpanded]);
46+
47+
useEffect(() => {
48+
if (!containerRef.current) return;
49+
50+
const resizeObserver = new ResizeObserver(() => {
51+
if (!isExpanded) {
52+
checkOverflow();
53+
}
54+
});
55+
56+
resizeObserver.observe(containerRef.current);
57+
58+
return () => resizeObserver.disconnect();
59+
}, [isExpanded]);
60+
61+
const clampedStyle: React.CSSProperties = {
62+
display: '-webkit-box',
63+
WebkitLineClamp: MAX_LINES,
64+
WebkitBoxOrient: 'vertical',
65+
overflow: 'hidden',
66+
transition: 'all 0.3s ease',
67+
};
68+
69+
const expandedStyle: React.CSSProperties = {
70+
display: 'block',
71+
transition: 'all 0.3s ease',
72+
};
73+
74+
return (
75+
<div ref={containerRef} style={{ position: 'relative' }}>
76+
<div ref={contentRef} style={isExpanded ? expandedStyle : clampedStyle}>
77+
<CspFlyoutMarkdown>{content}</CspFlyoutMarkdown>
78+
{isExpanded && isOverflowing && (
79+
<EuiButtonEmpty
80+
color="primary"
81+
flush="left"
82+
onClick={() => setIsExpanded(false)}
83+
style={{
84+
display: 'inline-block',
85+
verticalAlign: 'baseline',
86+
height: 'auto',
87+
minHeight: 'auto',
88+
fontSize: 'inherit',
89+
}}
90+
>
91+
<FormattedMessage
92+
id="xpack.csp.vulnerabilities.vulnerabilityDescriptionSection.showLess"
93+
defaultMessage="Show less"
94+
/>
95+
</EuiButtonEmpty>
96+
)}
97+
</div>
98+
{isOverflowing && !isExpanded && (
99+
<div
100+
style={{
101+
position: 'absolute',
102+
bottom: '0',
103+
right: '0',
104+
backgroundColor: euiTheme.colors.emptyShade,
105+
paddingLeft: '4px',
106+
}}
107+
>
108+
<EuiButtonEmpty
109+
color="primary"
110+
onClick={() => setIsExpanded(true)}
111+
flush="left"
112+
style={{
113+
height: 'auto',
114+
minHeight: 'auto',
115+
fontSize: 'inherit',
116+
}}
117+
>
118+
<FormattedMessage
119+
id="xpack.csp.vulnerabilities.vulnerabilityDescriptionSection.showMore"
120+
defaultMessage="...Show more"
121+
/>
122+
</EuiButtonEmpty>
123+
</div>
124+
)}
125+
</div>
126+
);
127+
};

x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe('<VulnerabilityFindingFlyout/>', () => {
103103
expect.stringContaining(mockQualysVulnerabilityHit.vulnerability.reference[0])
104104
);
105105

106-
expect(getByTestId(VULNERABILITY_HEADER_CVE_BADGE).textContent).toEqual('1 More');
106+
expect(getByTestId(VULNERABILITY_HEADER_CVE_BADGE).textContent).toEqual('+1');
107107
});
108108

109109
it('displays id as a text and a badge when there are multiple ids - qualys integration', () => {
@@ -135,7 +135,7 @@ describe('<VulnerabilityFindingFlyout/>', () => {
135135
expect(getByTestId(VULNERABILITY_HEADER_ID).textContent).toEqual(
136136
mockQualysVulnerabilityHit.vulnerability.id[0]
137137
);
138-
expect(getByTestId(VULNERABILITY_HEADER_CVE_BADGE).textContent).toEqual('1 More');
138+
expect(getByTestId(VULNERABILITY_HEADER_CVE_BADGE).textContent).toEqual('+1');
139139
});
140140

141141
it('Qualys vulnerability renders id (CVE) item correctly', () => {

x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/header.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('FindingsVulnerabilityFlyoutHeader', () => {
2929

3030
expect(cveslinkElement).toHaveTextContent(mockQualysVulnerabilityHit.vulnerability.id[0]);
3131
expect(cvesBadge).toHaveTextContent(
32-
`${mockQualysVulnerabilityHit.vulnerability.id.length - 1} More`
32+
`+${mockQualysVulnerabilityHit.vulnerability.id.length - 1}`
3333
);
3434
});
3535

@@ -49,7 +49,7 @@ describe('FindingsVulnerabilityFlyoutHeader', () => {
4949

5050
expect(cvesTextElement).toHaveTextContent(vulnerabilityHitWithNoReference.vulnerability.id[0]);
5151
expect(cvesBadge).toHaveTextContent(
52-
`${vulnerabilityHitWithNoReference.vulnerability.id.length - 1} More`
52+
`+${vulnerabilityHitWithNoReference.vulnerability.id.length - 1}`
5353
);
5454
});
5555

x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_right/header.tsx

Lines changed: 79 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55
* 2.0.
66
*/
77

8-
/*
9-
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
10-
* or more contributor license agreements. Licensed under the Elastic License
11-
* 2.0; you may not use this file except in compliance with the Elastic License
12-
* 2.0.
13-
*/
14-
158
import React from 'react';
169
import { css } from '@emotion/react';
1710
import {
@@ -24,6 +17,7 @@ import {
2417
EuiText,
2518
EuiBadge,
2619
EuiTitle,
20+
EuiFlexGrid,
2721
} from '@elastic/eui';
2822
import type { FindingsVulnerabilityFlyoutHeaderProps } from '@kbn/cloud-security-posture';
2923
import { CVSScoreBadge, findReferenceLink } from '@kbn/cloud-security-posture';
@@ -51,14 +45,14 @@ export const FindingsVulnerabilityFlyoutHeader = ({
5145
<EuiFlexItem grow={false}>
5246
<EuiBadge
5347
data-test-subj={VULNERABILITY_HEADER_CVE_BADGE}
54-
color="accent"
48+
color="subdued"
5549
css={css`
5650
border-radius: ${euiTheme.border.radius.small};
5751
`}
5852
>
5953
<FormattedMessage
6054
id="xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.cveTooltip"
61-
defaultMessage="{value} More"
55+
defaultMessage="+{value}"
6256
values={{ value: vulnerability?.id.length - 1 }}
6357
/>
6458
</EuiBadge>
@@ -71,7 +65,7 @@ export const FindingsVulnerabilityFlyoutHeader = ({
7165

7266
const renderCves = () => {
7367
if (!vulnerability?.id || vulnerability?.id.length === 0) {
74-
return <></>;
68+
return <>{EMPTY_VALUE}</>;
7569
}
7670

7771
const renderVulnerabilityId = () => {
@@ -116,7 +110,7 @@ export const FindingsVulnerabilityFlyoutHeader = ({
116110
>
117111
<EuiFlexItem grow={false}>
118112
<EuiTitle
119-
size="s"
113+
size="xxs"
120114
css={css`
121115
font-weight: ${euiTheme.font.weight.semiBold};
122116
line-height: ${euiTheme.size.xl};
@@ -132,85 +126,89 @@ export const FindingsVulnerabilityFlyoutHeader = ({
132126

133127
return (
134128
<>
135-
<EuiFlexGroup
136-
direction="column"
137-
css={css`
138-
gap: ${euiTheme.size.s};
139-
`}
140-
>
141-
{renderCves()}
142-
</EuiFlexGroup>
143129
<EuiSpacer size="m" />
144-
<EuiFlexGroup gutterSize="s">
130+
<EuiFlexGrid columns={2} gutterSize="m">
131+
<EuiFlexItem data-test-subj={VULNERABILITY_SCORES_FLYOUT} style={{ display: 'flex' }}>
132+
<EuiPanel
133+
style={{ height: '100%' }}
134+
borderRadius="m"
135+
paddingSize="m"
136+
hasShadow={false}
137+
hasBorder={true}
138+
>
139+
<EuiFlexGroup direction="column" gutterSize="m">
140+
<EuiFlexItem>
141+
<b>CVSS</b>
142+
</EuiFlexItem>
143+
{vulnerability?.score ? (
144+
<CVSScoreBadge
145+
version={vulnerability?.score?.version}
146+
score={vulnerability?.score?.base}
147+
/>
148+
) : (
149+
<EuiText>{EMPTY_VALUE}</EuiText>
150+
)}
151+
</EuiFlexGroup>
152+
</EuiPanel>
153+
</EuiFlexItem>
154+
145155
<EuiFlexItem>
146-
<EuiPanel hasBorder={true}>
147-
<EuiFlexGroup gutterSize="none">
148-
<EuiFlexItem data-test-subj={VULNERABILITY_SCORES_FLYOUT}>
149-
<EuiPanel
150-
borderRadius="none"
151-
paddingSize="xl"
152-
css={{ borderRight: 'solid 1px #D3DAE6', padding: '12px' }}
153-
hasBorder={false}
154-
hasShadow={false}
155-
>
156-
<EuiFlexGroup direction="column" gutterSize="s">
157-
<EuiFlexItem>
158-
<b>CVSS</b>
159-
</EuiFlexItem>
160-
{vulnerability?.score ? (
161-
<CVSScoreBadge
162-
version={vulnerability?.score?.version}
163-
score={vulnerability?.score?.base}
164-
/>
165-
) : (
166-
<EuiText>{EMPTY_VALUE}</EuiText>
167-
)}
168-
</EuiFlexGroup>
169-
</EuiPanel>
156+
<EuiPanel
157+
style={{ height: '100%' }}
158+
borderRadius="m"
159+
paddingSize="m"
160+
hasShadow={false}
161+
hasBorder={true}
162+
>
163+
<EuiFlexGroup direction="column" gutterSize="s">
164+
<EuiFlexItem>
165+
<b>CVE</b>
170166
</EuiFlexItem>
171-
<EuiFlexItem data-test-subj={DATA_SOURCE_VULNERABILITY_FLYOUT}>
172-
<EuiPanel
173-
borderRadius="none"
174-
paddingSize="xl"
175-
css={{ borderRight: 'solid 1px #D3DAE6', padding: '12px' }}
176-
hasBorder={false}
177-
hasShadow={false}
178-
>
179-
<EuiFlexGroup direction="column" gutterSize="s">
180-
<EuiFlexItem>
181-
<b>Data source</b>
182-
<EuiSpacer size="s" />
183-
{vulnerability?.data_source?.URL ? (
184-
<EuiLink href={vulnerability.data_source?.URL} target="_blank">
185-
{vulnerability?.data_source?.ID}
186-
</EuiLink>
187-
) : (
188-
<EuiText>{EMPTY_VALUE}</EuiText>
189-
)}
190-
</EuiFlexItem>
191-
</EuiFlexGroup>
192-
</EuiPanel>
167+
<EuiFlexItem>{renderCves()}</EuiFlexItem>
168+
</EuiFlexGroup>
169+
</EuiPanel>
170+
</EuiFlexItem>
171+
172+
<EuiFlexItem data-test-subj={DATA_SOURCE_VULNERABILITY_FLYOUT}>
173+
<EuiPanel
174+
style={{ height: '100%' }}
175+
borderRadius="m"
176+
paddingSize="m"
177+
hasShadow={false}
178+
hasBorder={true}
179+
>
180+
<EuiFlexGroup direction="column" gutterSize="m">
181+
<EuiFlexItem>
182+
<b>Data source</b>
193183
</EuiFlexItem>
184+
{vulnerability?.data_source?.URL ? (
185+
<EuiLink href={vulnerability.data_source?.URL} target="_blank">
186+
{vulnerability?.data_source?.ID}
187+
</EuiLink>
188+
) : (
189+
<EuiText>{EMPTY_VALUE}</EuiText>
190+
)}
191+
</EuiFlexGroup>
192+
</EuiPanel>
193+
</EuiFlexItem>
194+
195+
<EuiFlexItem>
196+
<EuiPanel
197+
style={{ height: '100%' }}
198+
borderRadius="m"
199+
paddingSize="m"
200+
hasShadow={false}
201+
hasBorder={true}
202+
>
203+
<EuiFlexGroup direction="column" gutterSize="m">
194204
<EuiFlexItem>
195-
<EuiPanel
196-
borderRadius="none"
197-
paddingSize="xl"
198-
css={{ padding: '12px' }}
199-
hasBorder={false}
200-
hasShadow={false}
201-
>
202-
<EuiFlexGroup direction="column" gutterSize="s">
203-
<EuiFlexItem>
204-
<b>Vendor</b>
205-
</EuiFlexItem>
206-
<EuiFlexItem> {vendor} </EuiFlexItem>
207-
</EuiFlexGroup>
208-
</EuiPanel>
205+
<b>Vendor</b>
209206
</EuiFlexItem>
207+
<EuiFlexItem>{vendor}</EuiFlexItem>
210208
</EuiFlexGroup>
211209
</EuiPanel>
212210
</EuiFlexItem>
213-
</EuiFlexGroup>
211+
</EuiFlexGrid>
214212
<div
215213
css={css`
216214
margin: ${euiTheme.size.s};

0 commit comments

Comments
 (0)