Frontend : Vulnerability changes to existing pages (#16601)

This commit is contained in:
RachelElysia
2024-02-15 10:35:05 -07:00
committed by mostlikelee
parent 4f15b24b07
commit 0ec010976a
26 changed files with 360 additions and 192 deletions
@@ -0,0 +1,60 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import { formatFloatAsPercentage } from "utilities/helpers";
import Icon from "components/Icon";
import { COLORS } from "styles/var/colors";
import TextCell from "../TextCell";
interface IProbabilityOfExploitCellProps {
probabilityOfExploit?: number | null;
cisaKnownExploit?: boolean | null;
rowId: string;
}
const ProbabilityOfExploitCell = ({
probabilityOfExploit,
cisaKnownExploit,
rowId,
}: IProbabilityOfExploitCellProps): JSX.Element => {
// Includes edge case of probability being 0
if (typeof probabilityOfExploit === "number") {
return (
<>
{formatFloatAsPercentage(probabilityOfExploit)}{" "}
{cisaKnownExploit && (
<>
<span
className={`probability-of-exploit tooltip tooltip__tooltip-icon`}
data-tip
data-for={`probability-of-exploit__${rowId.toString()}`}
>
<Icon name="error" size="small" color={"status-error"} />
</span>
<ReactTooltip
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`probability-of-exploit__${rowId.toString()}`}
data-html
>
<span className={`tooltip__tooltip-text`}>
<>
The vulnerability has been actively exploited in the wild.
This data is reported by the Cybersecurity and Infrastructure
Security Agency (CISA).
</>
</span>
</ReactTooltip>
</>
)}
</>
);
}
// Probability can return null or undefined
return <TextCell greyed />;
};
export default ProbabilityOfExploitCell;
@@ -0,0 +1 @@
export { default } from "./ProbabilityOfExploitCell";
@@ -211,6 +211,10 @@ $shadow-transition-width: 10px;
max-width: 500px;
word-wrap: break-word;
&.linkToFilteredHosts__cell {
text-align: right;
}
&.selection__cell {
width: 0px;
padding: 0 $pad-medium;
@@ -159,4 +159,21 @@
vertical-align: sub;
}
}
.linkToFilteredHosts__header {
width: 120px;
}
// This hides View all host link unless the row is hovered
tr {
.row-hover-link {
opacity: 0;
transition: 250ms;
text-overflow: none;
}
&:hover {
.row-hover-link {
opacity: 1;
}
}
}
}
@@ -13,6 +13,8 @@ interface IHostLinkProps {
platformLabelId?: number;
/** Shows right chevron without text */
condensed?: boolean;
/** Table links shows on row hover only */
rowHover?: boolean;
}
const baseClass = "view-all-hosts-link";
@@ -22,8 +24,11 @@ const ViewAllHostsLink = ({
className,
platformLabelId,
condensed = false,
rowHover = false,
}: IHostLinkProps): JSX.Element => {
const viewAllHostsLinkClass = classnames(baseClass, className);
const viewAllHostsLinkClass = classnames(baseClass, className, {
"row-hover-link": rowHover,
});
const endpoint = platformLabelId
? PATHS.MANAGE_HOSTS_LABEL(platformLabelId)
+1
View File
@@ -67,6 +67,7 @@ export interface ISoftwareVulnerability {
cve_published?: string | null;
cve_description?: string | null;
resolved_in_version?: string | null;
created_at?: string | null;
}
export interface ISoftwareVersion {
@@ -108,21 +108,32 @@ const defaultTableHeaders: IDataColumn[] = [
),
disableSortBy: true,
accessor: "windowsHosts",
Cell: ({
cell: { value: aggregateCount },
row: { original },
}: ICellProps) => {
Cell: ({ cell: { value: aggregateCount } }: ICellProps) => {
return (
<div className="disk-encryption-table__aggregate-table-data">
<TextCell value={aggregateCount} formatter={(val) => <>{val}</>} />
<ViewAllHostsLink
className="view-hosts-link"
queryParams={{
[HOSTS_QUERY_PARAMS.DISK_ENCRYPTION]: original.status.value,
team_id: original.teamId,
}}
/>
</div>
<TextCell value={aggregateCount} formatter={(val) => <>{val}</>} />
);
},
},
{
title: "",
Header: "",
accessor: "linkToFilteredHosts",
disableSortBy: true,
Cell: (cellProps: ICellProps) => {
return (
<>
{cellProps.row.original && (
<ViewAllHostsLink
className="view-hosts-link"
queryParams={{
[HOSTS_QUERY_PARAMS.DISK_ENCRYPTION]:
cellProps.row.original.status.value,
team_id: cellProps.row.original.teamId,
}}
rowHover
/>
)}
</>
);
},
},
@@ -5,25 +5,6 @@
min-width: auto;
}
&__aggregate-table-data {
display: flex;
align-items: center;
justify-content: space-between;
}
tr {
.view-hosts-link {
opacity: 0;
transition: 250ms;
}
&:hover {
.view-hosts-link {
opacity: 1;
}
}
}
@media (max-width: $break-md) {
.view-hosts-link {
span {
@@ -61,23 +61,30 @@ export const generateTableHeaders = (teamId: number) => {
),
Cell: ({ row }: IHostCellProps): JSX.Element => {
const { hosts_count, name_only, version } = row.original;
return <TextCell value={hosts_count} />;
},
},
{
title: "",
Header: "",
accessor: "linkToFilteredHosts",
disableSortBy: true,
Cell: (cellProps: IOSTypeCellProps) => {
return (
<span className="hosts-cell__wrapper">
<span className="hosts-cell__count">
<TextCell value={hosts_count} />
</span>
<span className="hosts-cell__link">
<>
{cellProps.row.original && (
<ViewAllHostsLink
queryParams={{
os_name: name_only,
os_version: version,
os_name: cellProps.row.original.name_only,
os_version: cellProps.row.original.version,
team_id: teamId,
}}
condensed
className="os-hosts-link"
rowHover
/>
</span>
</span>
)}
</>
);
},
},
@@ -82,21 +82,31 @@ export const COLUMN_CONFIGS: IColumnConfig[] = [
/>
),
accessor: "hosts",
Cell: ({
cell: { value: aggregateCount },
row: { original },
}: ICellProps) => {
Cell: ({ cell: { value: aggregateCount } }: ICellProps) => {
return (
<div className="bootstrap-package-table__aggregate-table-data">
<TextCell value={aggregateCount} formatter={(val) => <>{val}</>} />
<ViewAllHostsLink
className="view-hosts-link"
queryParams={{
bootstrap_package: original.status.value,
team_id: original.teamId,
}}
/>
</div>
<TextCell value={aggregateCount} formatter={(val) => <>{val}</>} />
);
},
},
{
title: "",
Header: "",
accessor: "linkToFilteredHosts",
disableSortBy: true,
Cell: (cellProps: ICellProps) => {
return (
<>
{cellProps.row.original && (
<ViewAllHostsLink
className="view-hosts-link"
queryParams={{
bootstrap_package: cellProps.row.original.status.value,
team_id: cellProps.row.original.teamId,
}}
rowHover
/>
)}
</>
);
},
},
@@ -10,23 +10,8 @@
min-width: auto;
}
&__aggregate-table-data {
display: flex;
align-items: center;
justify-content: space-between;
}
tr {
.view-hosts-link {
opacity: 0;
transition: 250ms;
}
&:hover {
.view-hosts-link {
opacity: 1;
}
}
th:nth-last-child(2) {
border-right: 0;
}
@media (max-width: $break-lg) {
@@ -88,21 +88,30 @@ const generateSoftwareTitleDetailsTableConfig = (router: InjectedRouter) => {
disableSortBy: true,
accessor: "hosts_count",
Cell: (cellProps: INumberCellProps): JSX.Element => (
<span className="hosts-cell__wrapper">
<span className="hosts-cell__count">
<TextCell value={cellProps.cell.value} />
</span>
<span className="hosts-cell__link">
<ViewAllHostsLink
queryParams={{
software_version_id: cellProps.row.original.id,
}}
className="software-link"
/>
</span>
</span>
<TextCell value={cellProps.cell.value} />
),
},
{
title: "",
Header: "",
accessor: "linkToFilteredHosts",
disableSortBy: true,
Cell: (cellProps: ICellProps) => {
return (
<>
{cellProps.row.original && (
<ViewAllHostsLink
queryParams={{
software_version_id: cellProps.row.original.id,
}}
className="software-link"
rowHover
/>
)}
</>
);
},
},
];
return tableHeaders;
@@ -1,13 +1,7 @@
.software-title-details-table {
.hosts-cell__wrapper {
display: flex;
align-items: center;
justify-content: space-between;
.hosts-cell__link {
display: flex;
white-space: nowrap;
.data-table {
.hosts_count__header {
border-right: 0;
}
}
}
@@ -16,18 +16,4 @@
font-size: $medium;
}
}
// for showing and hiding software link on hover
tr {
.software-link {
opacity: 0;
transition: opacity 250ms;
}
&:hover {
.software-link {
opacity: 1;
}
}
}
}
@@ -161,22 +161,27 @@ const generateTableHeaders = (
disableSortBy: false,
accessor: "hosts_count",
Cell: (cellProps: INumberCellProps): JSX.Element => (
<span className="hosts-cell__wrapper">
<span className="hosts-cell__count">
<TextCell value={cellProps.cell.value} />
</span>
<span className="hosts-cell__link">
<ViewAllHostsLink
queryParams={{
software_title_id: cellProps.row.original.id,
team_id: teamId, // TODO: do we need team id here?
}}
className="software-link"
/>
</span>
</span>
<TextCell value={cellProps.cell.value} />
),
},
{
title: "",
Header: "",
accessor: "linkToFilteredHosts",
disableSortBy: true,
Cell: (cellProps: ICellProps) => {
return (
<ViewAllHostsLink
queryParams={{
software_title_id: cellProps.row.original.id,
team_id: teamId, // TODO: do we need team id here?
}}
className="software-link"
rowHover
/>
);
},
},
];
return softwareTableHeaders;
@@ -134,22 +134,31 @@ const generateTableHeaders = (
disableSortBy: false,
accessor: "hosts_count",
Cell: (cellProps: INumberCellProps): JSX.Element => (
<span className="hosts-cell__wrapper">
<span className="hosts-cell__count">
<TextCell value={cellProps.cell.value} />
</span>
<span className="hosts-cell__link">
<ViewAllHostsLink
queryParams={{
software_version_id: cellProps.row.original.id,
team_id: teamId, // TODO: do we need team id here?
}}
className="software-link"
/>
</span>
</span>
<TextCell value={cellProps.cell.value} />
),
},
{
title: "",
Header: "",
accessor: "linkToFilteredHosts",
disableSortBy: true,
Cell: (cellProps: ICellProps) => {
return (
<>
{cellProps.row.original && (
<ViewAllHostsLink
queryParams={{
software_version_id: cellProps.row.original.id,
team_id: teamId, // TODO: do we need team id here?
}}
className="software-link"
rowHover
/>
)}
</>
);
},
},
];
return softwareTableHeaders;
@@ -32,6 +32,9 @@
&__data-set {
font-size: $x-small;
display: flex;
flex-direction: column;
gap: $pad-xsmall;
dt {
font-weight: $bold;
@@ -62,7 +62,7 @@ const SoftwareVulnerabilitiesTable = ({
<TableContainer
columnConfigs={tableHeaders}
data={data}
defaultSortHeader={isPremiumTier ? "epss_probability" : "cve"}
defaultSortHeader={isPremiumTier ? "updated_at" : "cve"} // TODO: Change premium to created_at when added to API
defaultSortDirection={"desc"}
emptyComponent={() => <NoVulnsDetected itemName={itemName} />}
isAllPagesSelected={false}
@@ -1,15 +1,17 @@
import React from "react";
import { formatFloatAsPercentage } from "utilities/helpers";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import { ISoftwareVulnerability } from "interfaces/software";
import paths from "router/paths";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
import CustomLink from "components/CustomLink";
import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import ProbabilityOfExploitCell from "components/TableContainer/DataTable/ProbabilityOfExploitCell.tsx/ProbabilityOfExploitCell";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import LinkCell from "components/TableContainer/DataTable/LinkCell";
interface IHeaderProps {
column: {
@@ -72,12 +74,12 @@ const generateTableConfig = (
accessor: "cve",
disableSortBy: true,
Header: "Vulnerability",
Cell: ({ cell: { value }, row }: ITextCellProps) => {
Cell: ({ cell: { value } }: ITextCellProps) => {
const cveName = value.toString();
return (
<CustomLink
url={row.original.details_link}
text={value.toString()}
newTab
<LinkCell
value={cveName}
path={paths.SOFTWARE_VULNERABILITY_DETAILS(cveName)}
/>
);
},
@@ -85,39 +87,6 @@ const generateTableConfig = (
];
const premiumHeaders: IDataColumn[] = [
{
title: "Probability of exploit",
accessor: "epss_probability",
disableSortBy: false,
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={
<>
The probability that this vulnerability will be exploited in the
next 30 days (EPSS probability).
<br />
This data is reported by FIRST.org.
</>
}
>
Probability of exploit
</TooltipWrapper>
);
return (
<>
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
{isSandboxMode && <PremiumFeatureIconWithTooltip />}
</>
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => (
<TextCell formatter={formatFloatAsPercentage} value={value} />
),
},
{
title: "Severity",
accessor: "cvss_score",
@@ -129,9 +98,6 @@ const generateTableConfig = (
<>
The worst case impact across different environments (CVSS base
score).
<br />
This data is reported by the National Vulnerability Database
(NVD).
</>
}
>
@@ -153,22 +119,22 @@ const generateTableConfig = (
),
},
{
title: "Known exploit",
accessor: "cisa_known_exploit",
title: "Probability of exploit",
accessor: "epss_probability",
disableSortBy: false,
sortType: "boolean",
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
className="epss_probability"
tipContent={
<>
The vulnerability has been actively exploited in the wild. This
data is reported by the Cybersecurity and Infrustructure
Security Agency (CISA).
The probability that this vulnerability will be exploited in the
next 30 days (EPSS probability). <br />
This data is reported by FIRST.org.
</>
}
>
Known exploit
Probability of exploit
</TooltipWrapper>
);
return (
@@ -181,8 +147,12 @@ const generateTableConfig = (
</>
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => (
<TextCell value={value ? "Yes" : "No"} />
Cell: (cellProps: ICellProps): JSX.Element => (
<ProbabilityOfExploitCell
probabilityOfExploit={cellProps.row.original.epss_probability}
cisaKnownExploit={cellProps.row.original.cisa_known_exploit}
rowId={cellProps.row.original.cve}
/>
),
},
{
@@ -222,6 +192,53 @@ const generateTableConfig = (
);
},
},
{
title: "Detected",
accessor: "created_at",
disableSortBy: false,
Header: (headerProps: IHeaderProps): JSX.Element => {
return (
<>
<HeaderCell
value="Detected"
isSortedDesc={headerProps.column.isSortedDesc}
/>
{isSandboxMode && <PremiumFeatureIconWithTooltip />}
</>
);
},
Cell: (cellProps: ICellProps): JSX.Element => {
const createdAt = cellProps.row.original.created_at || "";
return (
<TextCell
value={{ timeString: createdAt }}
formatter={HumanTimeDiffWithDateTip}
/>
);
},
},
{
title: "",
Header: "",
accessor: "linkToFilteredHosts",
disableSortBy: true,
Cell: (cellProps: ICellProps) => {
return (
<>
{cellProps.row.original && (
<ViewAllHostsLink
queryParams={{
vulnerability: cellProps.row.original.cve,
}}
className="vulnerabilities-link"
rowHover
/>
)}
</>
);
},
},
];
return isPremiumTier ? tableHeaders.concat(premiumHeaders) : tableHeaders;
@@ -1,10 +1,13 @@
.software-vulnerabilities-table {
// keeps table data within the table container at smaller screen sizes
.data-table {
&__wrapper {
overflow-x: auto;
}
th:nth-last-child(2) {
border-right: 0;
}
}
// used to position header text with premium icon correctly
@@ -12,4 +15,21 @@
display: flex;
gap: $pad-small;
}
@media (max-width: $break-md) {
.linkToFilteredHosts__header {
width: 40px;
}
.view-all-hosts-link {
span {
display: none;
}
}
.epss_probability__cell,
.epss_probability__header,
.cve_published__cell,
.cve_published__header {
display: none;
}
}
}
@@ -241,6 +241,7 @@ const ManageHostsPage = ({
os_name: osName,
os_version: osVersion,
} = queryParams;
const vulnerability = queryParams?.vulnerability;
const munkiIssueId =
queryParams?.munki_issue_id !== undefined
? parseInt(queryParams.munki_issue_id, 10)
@@ -383,6 +384,7 @@ const ManageHostsPage = ({
osVersionId,
osName,
osVersion,
vulnerability,
page: tableQueryData ? tableQueryData.pageIndex : 0,
perPage: tableQueryData ? tableQueryData.pageSize : 50,
device_mapping: true,
@@ -425,6 +427,7 @@ const ManageHostsPage = ({
osVersionId,
osName,
osVersion,
vulnerability,
osSettings: osSettingsStatus,
diskEncryptionStatus,
bootstrapPackageStatus,
@@ -824,6 +827,8 @@ const ManageHostsPage = ({
newQueryParams.os_version_id = osVersionId;
newQueryParams.os_name = osName;
newQueryParams.os_version = osVersion;
} else if (vulnerability) {
newQueryParams.vulnerability = vulnerability;
} else if (osSettingsStatus) {
newQueryParams[PARAMS.OS_SETTINGS] = osSettingsStatus;
} else if (diskEncryptionStatus && isPremiumTier) {
@@ -1281,6 +1286,7 @@ const ManageHostsPage = ({
os_version_id: osVersionId,
os_name: osName,
os_version: osVersion,
vulnerability,
visibleColumns,
};
@@ -61,6 +61,7 @@ interface IHostsFilterBlockProps {
osVersionId?: string;
osName?: string;
osVersion?: string;
vulnerability?: string;
munkiIssueId?: number;
osVersions?: IOperatingSystemVersion[];
softwareDetails: { name: string; version?: string } | null;
@@ -104,6 +105,7 @@ const HostsFilterBlock = ({
osVersionId,
osName,
osVersion,
vulnerability,
munkiIssueId,
munkiIssueDetails,
policyResponse,
@@ -203,6 +205,24 @@ const HostsFilterBlock = ({
);
};
const renderVulnerabilityFilterBlock = () => {
if (!vulnerability) return null;
// TODO: Move formatOperatingSystemDisplayName into utils file
const label = formatOperatingSystemDisplayName(vulnerability);
const TooltipDescription = (
<span>Hosts affected by the specified CVE.</span>
);
return (
<FilterPill
label={label}
tooltipDescription={TooltipDescription}
onClear={() => handleClearFilter(["vulnerability"])}
/>
);
};
// NOTE: good example of filter dropdown with pill
const renderPoliciesFilterBlock = () => (
<>
@@ -498,6 +518,8 @@ const HostsFilterBlock = ({
return renderMDMEnrollmentFilterBlock();
case !!osVersionId || (!!osName && !!osVersion):
return renderOSFilterBlock();
case !!vulnerability:
return renderVulnerabilityFilterBlock();
case !!munkiIssueId:
return renderMunkiIssueFilterBlock();
case !!lowDiskSpaceHosts:
+4
View File
@@ -58,6 +58,10 @@ export default {
SOFTWARE_OS_DETAILS: (id: number): string => {
return `${URL_PREFIX}/software/os/${id}`;
},
SOFTWARE_VULNERABILITIES: `${URL_PREFIX}/software/vulnerabilities`,
SOFTWARE_VULNERABILITY_DETAILS: (cve: string): string => {
return `${URL_PREFIX}/software/vulnerabilities/${cve}`;
},
EDIT_PACK: (packId: number): string => {
return `${URL_PREFIX}/packs/${packId}/edit`;
+3
View File
@@ -50,6 +50,7 @@ export interface IHostCountLoadOptions {
osName?: string;
osVersion?: string;
osSettings?: MdmProfileStatus;
vulnerability?: string;
diskEncryptionStatus?: DiskEncryptionStatus;
bootstrapPackageStatus?: BootstrapPackageStatus;
}
@@ -77,6 +78,7 @@ export default {
const osName = options?.osName;
const osVersion = options?.osVersion;
const osSettings = options?.osSettings;
const vulnerability = options?.vulnerability;
const diskEncryptionStatus = options?.diskEncryptionStatus;
const bootstrapPackageStatus = options?.bootstrapPackageStatus;
@@ -102,6 +104,7 @@ export default {
osVersionId,
osVersion,
osSettings,
vulnerability,
diskEncryptionStatus,
bootstrapPackageStatus,
}),
+4
View File
@@ -72,6 +72,7 @@ export interface ILoadHostsOptions {
osVersionId?: number;
osName?: string;
osVersion?: string;
vulnerability?: string;
munkiIssueId?: number;
device_mapping?: boolean;
columns?: string;
@@ -102,6 +103,7 @@ export interface IExportHostsOptions {
osId?: number;
osName?: string;
osVersion?: string;
vulnerability?: string;
device_mapping?: boolean;
columns?: string;
visibleColumns?: string;
@@ -261,6 +263,7 @@ export default {
osVersionId,
osName,
osVersion,
vulnerability,
device_mapping,
selectedLabels,
sortBy,
@@ -299,6 +302,7 @@ export default {
osVersionId,
osName,
osVersion,
vulnerability,
diskEncryptionStatus,
osSettings,
bootstrapPackageStatus,
+4
View File
@@ -9,6 +9,7 @@ import {
HOSTS_QUERY_PARAMS,
MacSettingsStatusQueryParam,
} from "services/entities/hosts";
import vulnerability from "interfaces/vulnerability";
type QueryValues = string | number | boolean | undefined | null;
export type QueryParams = Record<string, QueryValues>;
@@ -36,6 +37,7 @@ interface IMutuallyExclusiveHostParams {
osVersionId?: number;
osName?: string;
osVersion?: string;
vulnerability?: string;
osSettings?: MdmProfileStatus;
diskEncryptionStatus?: DiskEncryptionStatus;
bootstrapPackageStatus?: BootstrapPackageStatus;
@@ -155,6 +157,8 @@ export const reconcileMutuallyExclusiveHostParams = ({
return { os_version_id: osVersionId };
case !!osName && !!osVersion:
return { os_name: osName, os_version: osVersion };
case !!vulnerability:
return { vulnerability };
case !!lowDiskSpaceHosts:
return { low_disk_space: lowDiskSpaceHosts };
case !!osSettings: