Skip to content

Commit

Permalink
fixes to Image Vulnerabilities cluster status
Browse files Browse the repository at this point in the history
  • Loading branch information
alecmerdler committed May 7, 2020
1 parent b30a4ba commit 900dc9b
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 76 deletions.
Expand Up @@ -12,7 +12,7 @@ import {
ImageManifestVulnDetailsProps,
} from '../image-manifest-vuln';
import { fakeVulnFor } from '../../../integration-tests/bad-pods';
import { Priority, vulnPriority, totalFor } from '../../const';
import { Priority, vulnPriority, totalFor, priorityFor } from '../../const';

describe(ImageVulnerabilityRow.displayName, () => {
let wrapper: ShallowWrapper<ImageVulnerabilityRowProps>;
Expand Down Expand Up @@ -45,7 +45,7 @@ describe(ImageVulnerabilitiesTable.displayName, () => {
expect(wrapper.find(ImageVulnerabilityRow).length).toEqual(3);
const indexes = wrapper
.find(ImageVulnerabilityRow)
.map((r) => vulnPriority.find((p) => p.title === r.props().vulnerability.severity).index);
.map((r) => priorityFor(r.props().vulnerability.severity).index);
expect(indexes).toEqual(_.sortBy(indexes));
});
});
Expand Down
Expand Up @@ -26,10 +26,11 @@ import {
Firehose,
FirehoseResult,
Loading,
MsgBox,
} from '@console/internal/components/utils';
import { ChartDonut } from '@patternfly/react-charts';
import { DefaultList } from '@console/internal/components/default-resource';
import { vulnPriority, totalFor } from '../const';
import { vulnPriority, totalFor, priorityFor } from '../const';
import { ImageManifestVuln, Feature, Vulnerability } from '../types';
import { ImageManifestVulnModel } from '../models';
import { quayURLFor } from './summary';
Expand All @@ -38,6 +39,7 @@ import { ContainerLink } from '@console/internal/components/pod';

const shortenImage = (img: string) =>
img
.replace('@sha256', '')
.split('/')
.slice(1, 3)
.join('/');
Expand All @@ -50,9 +52,7 @@ export const ImageVulnerabilityRow: React.FC<ImageVulnerabilityRowProps> = (prop
<ExternalLink text={props.vulnerability.name} href={props.vulnerability.link} />
</div>
<div className="col-lg-2 col-md-2 col-sm-5 col-xs-6">
<SecurityIcon
color={vulnPriority.find((p) => p.title === props.vulnerability.severity).color.value}
/>
<SecurityIcon color={priorityFor(props.vulnerability.severity).color.value} />
&nbsp;{props.vulnerability.severity}
</div>
<div className="col-lg-2 col-md-2 col-sm-3 hidden-xs">{props.packageName}</div>
Expand All @@ -71,7 +71,7 @@ export const ImageVulnerabilitiesTable: React.FC<ImageVulnerabilitiesTableProps>
feature.vulnerabilities.map((vulnerability) => ({ feature, vulnerability })),
),
),
(v) => vulnPriority.find((p) => p.title === v.vulnerability.severity).index,
(v) => priorityFor(v.vulnerability.severity).index,
);

return (
Expand Down Expand Up @@ -231,15 +231,19 @@ export const ImageManifestVulnTableRow: RowFunction<ImageManifestVuln> = ({
<ResourceLink kind="Namespace" name={namespace} />
</TableData>
<TableData className={tableColumnClasses[2]}>
<SecurityIcon
color={vulnPriority.find(({ title }) => obj.status.highestSeverity === title).color.value}
/>
&nbsp;{obj.status.highestSeverity}
{_.get(obj.status, 'highestSeverity') ? (
<>
<SecurityIcon color={priorityFor(_.get(obj.status, 'highestSeverity')).color.value} />
&nbsp;{obj.status.highestSeverity}
</>
) : (
<Loading />
)}
</TableData>
<TableData className={tableColumnClasses[3]}>
{Object.keys(obj.status.affectedPods).length}
</TableData>
<TableData className={tableColumnClasses[4]}>{obj.status.fixableCount}</TableData>
<TableData className={tableColumnClasses[4]}>{obj.status.fixableCount || 0}</TableData>
<TableData className={tableColumnClasses[4]}>
<ExternalLink text={shortenHash(obj.spec.manifest)} href={quayURLFor(obj)} />
</TableData>
Expand Down Expand Up @@ -286,9 +290,10 @@ export const ImageManifestVulnList: React.FC<ImageManifestVulnListProps> = (prop
return (
<Table
{...props}
aria-label="Image Manifest Vulns"
aria-label="Image Manifest Vulnerabilities"
Header={ImageManifestVulnTableHeader}
Row={ImageManifestVulnTableRow}
EmptyMsg={() => <MsgBox title="No Image Vulnerabilities Found" detail="" />}
virtualize
/>
);
Expand Down Expand Up @@ -365,10 +370,7 @@ export const ContainerVulnerabilities: React.FC<ContainerVulnerabilitiesProps> =
(vuln) => (
<span style={{ display: 'flex', alignItems: 'center' }}>
<SecurityIcon
color={
vulnPriority.find(({ title }) => vuln.status.highestSeverity === title)
.color.value
}
color={priorityFor(_.get(vuln.status, 'highestSeverity')).color.value}
/>
&nbsp;
<ResourceLink
Expand All @@ -378,7 +380,7 @@ export const ContainerVulnerabilities: React.FC<ContainerVulnerabilitiesProps> =
title={vuln.metadata.uid}
displayName={`${totalFor(
vulnPriority.findKey(
({ title }) => vuln.status.highestSeverity === title,
({ title }) => _.get(vuln.status, 'highestSeverity') === title,
),
)(vuln)} ${vuln.status.highestSeverity}`}
hideIcon
Expand Down
140 changes: 84 additions & 56 deletions frontend/packages/container-security/src/components/summary.tsx
Expand Up @@ -2,15 +2,15 @@ import * as React from 'react';
import * as _ from 'lodash';
import { pluralize } from '@patternfly/react-core';
import { ChartDonut } from '@patternfly/react-charts';
import { SecurityIcon } from '@patternfly/react-icons';
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import { ResourceHealthHandler } from '@console/plugin-sdk';
import { WatchK8sResults } from '@console/internal/components/utils/k8s-watch-hook';
import { ExternalLink } from '@console/internal/components/utils/link';
import { HealthState } from '@console/shared/src/components/dashboard/status-card/states';
import { Link } from 'react-router-dom';
import { referenceForModel } from '@console/internal/module/k8s';
import { ImageManifestVuln, WatchImageVuln } from '../types';
import { vulnPriority } from '../const';
import { vulnPriority, priorityFor } from '../const';
import { ImageManifestVulnModel } from '../models';

export const securityHealthHandler: ResourceHealthHandler<WatchImageVuln> = ({
Expand All @@ -25,20 +25,24 @@ export const securityHealthHandler: ResourceHealthHandler<WatchImageVuln> = ({
return { state: HealthState.LOADING, message: 'Scanning in progress' };
}
if (!_.isEmpty(data)) {
return { state: HealthState.ERROR, message: `${data.length} vulnerabilities` };
return {
state: HealthState.ERROR,
message: `${_.uniqBy(data, 'metadata.name').length} vulnerabilities`,
};
}
return { state: HealthState.OK, message: '0 vulnerabilities' };
};

export const quayURLFor = (vuln: ImageManifestVuln) => {
const base = vuln.spec.image
.replace('@sha256', '')
.split('/')
.reduce((url, part, i) => [...url, part, ...(i === 0 ? ['repository'] : [])], [])
.join('/');
return `//${base}/manifest/${vuln.spec.manifest}?tab=vulnerabilities`;
};

export const SecurityBreakdownPopup: React.FC<WatchK8sResults<WatchImageVuln>> = ({
export const SecurityBreakdownPopup: React.FC<SecurityBreakdownPopupProps> = ({
imageManifestVuln,
}) => {
const resource = imageManifestVuln.data;
Expand All @@ -56,66 +60,73 @@ export const SecurityBreakdownPopup: React.FC<WatchK8sResults<WatchImageVuln>> =
registries are not scanned.
</div>
{!_.isEmpty(resource) ? (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ width: '66%', marginRight: '24px' }}>
<div className="co-status-popup__row">
<div className="co-status-popup__text--bold">Severity</div>
<div className="text-secondary">Fixable</div>
<>
<div className="co-status-popup__section">
<div className="co-status-popup__row">
<div className="co-status-popup__text--bold">
Vulnerable Container Images by Severity
</div>
{vulnPriority
.map((priority) =>
!_.isEmpty(vulnsFor(priority.value)) ? (
<div className="co-status-popup__row" key={priority.value}>
<div className="co-status-popup__text--bold">
{vulnsFor(priority.value).length} {priority.title}
</div>
<div className="text-secondary">
{
resource.filter(
(v) =>
_.get(v.status, 'highestSeverity') === priority.value &&
_.get(v.status, 'fixableCount', 0) > 0,
).length
}{' '}
<SecurityIcon color={priority.color.value} />
</div>
</div>
) : null,
)
.toArray()}
</div>
<div>
<ChartDonut
colorScale={vulnPriority.map((priority) => priority.color.value).toArray()}
data={vulnPriority
.map((priority) => ({
label: priority.title,
x: priority.value,
y: vulnsFor(priority.value).length,
}))
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ width: '66%', marginRight: '24px' }}>
{vulnPriority
.map((priority) =>
!_.isEmpty(vulnsFor(priority.value)) ? (
<div className="co-status-popup__row" key={priority.value}>
<div>
<ExclamationTriangleIcon
color={priority.color.value}
alt={priority.title}
/>
&nbsp;
{_.uniqBy(vulnsFor(priority.value), 'metadata.name').length}{' '}
{priority.title}
</div>
</div>
) : null,
)
.toArray()}
title={`${resource.length} total`}
/>
</div>
<div>
<Link
to={`/k8s/all-namespaces/${referenceForModel(ImageManifestVulnModel)}`}
aria-label="View all"
>
<ChartDonut
colorScale={vulnPriority.map((priority) => priority.color.value).toArray()}
data={vulnPriority
.map((priority) => ({
label: priority.title,
x: priority.value,
y: _.uniqBy(vulnsFor(priority.value), 'metadata.name').length,
}))
.toArray()}
title={`${_.uniqBy(resource, 'metadata.name').length} total`}
/>
</Link>
</div>
</div>
</div>
{!_.isEmpty(fixableVulns) && (
<>
<div className="co-status-popup__section">
<div className="co-status-popup__row">
<div className="co-status-popup__text--bold">Fixable Vulnerabilities</div>
<div>
<span className="co-status-popup__text--bold">Fixable Container Images</span>
<span className="text-secondary">&nbsp;({fixableVulns.size} total)</span>
</div>
</div>
{_.take([...fixableVulns.values()], 5).map((v) => (
<div className="co-status-popup__row">
<span className="co-status-popup__text--bold">Impact</span>
<span className="co-status-popup__text--bold">Vulnerabilities</span>
</div>
{_.sortBy(_.take([...fixableVulns.values()], 5), [
(v) => priorityFor(v.status.highestSeverity).index,
]).map((v) => (
<div className="co-status-popup__row" key={v.metadata.name}>
<span>
<SecurityIcon
color={
vulnPriority.find((p) => p.title === _.get(v.status, 'highestSeverity'))
.color.value
}
<ExclamationTriangleIcon
color={priorityFor(_.get(v.status, 'highestSeverity')).color.value}
/>{' '}
<ExternalLink href={quayURLFor(v)} text={v.spec.features[0].name} />
</span>
<div className="text-secondary">
<Link
to={`/k8s/all-namespaces/${referenceForModel(ImageManifestVulnModel)}?name=${
v.metadata.name
Expand All @@ -126,17 +137,34 @@ export const SecurityBreakdownPopup: React.FC<WatchK8sResults<WatchImageVuln>> =
'namespace',
)}
</Link>
</span>
<div className="text-secondary">
<ExternalLink href={quayURLFor(v)} text={`${v.status.fixableCount} fixable`} />
</div>
</div>
))}
</>
<div className="co-status-popup__row">
<Link
to={{
pathname: `/k8s/all-namespaces/${referenceForModel(ImageManifestVulnModel)}`,
search: '?orderBy=desc&sortBy=Fixable',
}}
>
View all
</Link>
</div>
</div>
)}
</div>
</>
) : (
<div>No vulnerabilities detected.</div>
<div className="co-status-popup__section">
<span className="text-secondary">No vulnerabilities detected.</span>
</div>
)}
</>
);
};

export type SecurityBreakdownPopupProps = WatchK8sResults<WatchImageVuln>;

SecurityBreakdownPopup.displayName = 'SecurityBreakdownPopup';
3 changes: 3 additions & 0 deletions frontend/packages/container-security/src/const.ts
Expand Up @@ -125,3 +125,6 @@ export const totalFor = (priority: Priority) => (obj: ImageManifestVuln) => {
return 0;
}
};

export const priorityFor = (severity: string) =>
vulnPriority.find(({ title }) => title === severity) || vulnPriority.get(Priority.Unknown);
4 changes: 2 additions & 2 deletions frontend/packages/container-security/src/plugin.ts
Expand Up @@ -96,7 +96,7 @@ const plugin: Plugin<ConsumedExtensions> = [
{
type: 'Dashboards/Overview/Health/Resource',
properties: {
title: 'Quay Image Security',
title: 'Image Vulnerabilities',
resources: {
imageManifestVuln: {
kind: referenceForModel(ImageManifestVulnModel),
Expand All @@ -105,7 +105,7 @@ const plugin: Plugin<ConsumedExtensions> = [
},
},
healthHandler: securityHealthHandler,
popupTitle: 'Quay Image Security breakdown',
popupTitle: 'Image Vulnerabilities breakdown',
popupComponent: () =>
import('./components/summary' /* webpackChunkName: "container-security" */).then(
(m) => m.SecurityBreakdownPopup,
Expand Down

0 comments on commit 900dc9b

Please sign in to comment.