Host Details Page: Software vulnerability column (#4836)

This commit is contained in:
RachelElysia
2022-04-04 12:33:02 -04:00
committed by GitHub
parent b834e7d2f5
commit 5cce257e1e
10 changed files with 117 additions and 100 deletions
@@ -0,0 +1 @@
* Host details page software table now has search by vulnerabilities and a vulnerabilities column
+1 -1
View File
@@ -171,7 +171,7 @@ describe("Hosts flow", () => {
initialCount = parseInt(newCount[0], 10);
expect(initialCount).to.be.at.least(1);
});
cy.findByPlaceholderText(/filter software/i).type("lib");
cy.findByPlaceholderText(/search software/i).type("lib");
// Ensures search completes
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
cy.getAttached(".table-container__results-count")
@@ -147,7 +147,7 @@ const DataTable = ({
// Initializes as false, but changes briefly to true on successful notification
autoResetSelectedRows: resetSelectedRows,
// Expands the enumerated `filterTypes` for react-table
// (see https://github.com/tannerlinsley/react-table/blob/master/src/filterTypes.js)
// (see https://github.com/TanStack/react-table/blob/alpha/packages/react-table/src/filterTypes.ts)
// with custom `filterTypes` defined for this `useTable` instance
filterTypes: React.useMemo(
() => ({
+17
View File
@@ -18,6 +18,7 @@ import {
} from "interfaces/target";
import { ITeam, ITeamSummary } from "interfaces/team";
import { IUser } from "interfaces/user";
import { IVulnerability } from "interfaces/vulnerability";
import stringUtils from "utilities/strings";
import sortUtils from "utilities/sort";
@@ -760,6 +761,21 @@ export const wrapFleetHelper = (
return value === "---" ? value : helperFn(value);
};
export const condenseVulnColumn = (
vulnerabilities: IVulnerability[]
): string[] => {
const condensed =
(vulnerabilities?.length &&
vulnerabilities
.slice(-3)
.map((v) => v.cve)
.reverse()) ||
[];
return vulnerabilities.length > 3
? condensed.concat(`+${vulnerabilities.length - 3} more`)
: condensed;
};
export default {
addGravatarUrlToResource,
formatConfigDataForServer,
@@ -793,4 +809,5 @@ export default {
getValidatedTeamId,
normalizeEmptyValues,
wrapFleetHelper,
condenseVulnColumn,
};
@@ -26,20 +26,23 @@ const SoftwareTable = ({
software,
deviceUser,
}: ISoftwareTableProps): JSX.Element => {
const [filterName, setFilterName] = useState("");
const [searchString, setSearchString] = useState("");
const [filterVuln, setFilterVuln] = useState(false);
const [filters, setFilters] = useState({
name: filterName,
name: searchString,
vulnerabilities: filterVuln,
});
useEffect(() => {
setFilters({ name: filterName, vulnerabilities: filterVuln });
}, [filterName, filterVuln]);
setFilters({
name: searchString,
vulnerabilities: filterVuln,
});
}, [searchString, filterVuln]);
const onQueryChange = useDebouncedCallback(
({ searchQuery }: { searchQuery: string }) => {
setFilterName(searchQuery);
setSearchString(searchQuery);
},
300
);
@@ -82,7 +85,9 @@ const SoftwareTable = ({
isLoading={isLoading}
defaultSortHeader={"name"}
defaultSortDirection={"asc"}
inputPlaceHolder={"Filter software"}
inputPlaceHolder={
"Search software by name or vulnerabilities (CVEs)"
}
onQueryChange={onQueryChange}
resultsTitle={"software items"}
emptyComponent={EmptySoftware}
@@ -1,18 +1,18 @@
import React from "react";
import { Link } from "react-router";
import ReactTooltip from "react-tooltip";
import { isEmpty } from "lodash";
// TODO: Enable after backend has been updated to provide last_opened_at
// import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import { condenseVulnColumn } from "fleet/helpers";
import { ISoftware } from "interfaces/software";
import { IVulnerability } from "interfaces/vulnerability";
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 IssueIcon from "../../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
import Chevron from "../../../../../../assets/images/icon-chevron-right-9x6@2x.png";
interface IHeaderProps {
@@ -23,18 +23,32 @@ interface IHeaderProps {
}
interface ICellProps {
cell: {
value: string;
value: number | string | IVulnerability[];
};
row: {
original: ISoftware;
};
}
interface IStringCellProps extends ICellProps {
cell: {
value: string;
};
}
interface IVulnCellProps extends ICellProps {
cell: {
value: IVulnerability[];
};
}
interface IDataColumn {
title: string;
Header: ((props: IHeaderProps) => JSX.Element) | string;
accessor: string;
Cell: (props: ICellProps) => JSX.Element;
Cell:
| ((props: IStringCellProps) => JSX.Element)
| ((props: IVulnCellProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
sortType?: string;
@@ -74,46 +88,6 @@ const formatSoftwareType = (source: string) => {
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
const generateSoftwareTableHeaders = (deviceUser = false): IDataColumn[] => {
const tableHeaders: IDataColumn[] = [
{
title: "Vulnerabilities",
Header: "",
disableSortBy: true,
accessor: "vulnerabilities",
Filter: () => null, // input for this column filter outside of column header
filter: "hasLength", // filters out rows where vulnerabilities has no length if filter value is `true`
Cell: (cellProps) => {
const vulnerabilities = cellProps.cell.value;
if (isEmpty(vulnerabilities)) {
return <></>;
}
return (
<>
<span
className={`vulnerabilities tooltip__tooltip-icon`}
data-tip
data-for={`vulnerabilities__${cellProps.row.original.id.toString()}`}
data-tip-disable={false}
>
<img alt="software vulnerabilities" src={IssueIcon} />
</span>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id={`vulnerabilities__${cellProps.row.original.id.toString()}`}
data-html
>
<span className={`vulnerabilities tooltip__tooltip-text`}>
{vulnerabilities.length === 1
? "1 vulnerability detected"
: `${vulnerabilities.length} vulnerabilities detected`}
</span>
</ReactTooltip>
</>
);
},
},
{
title: "Name",
Header: (cellProps) => (
@@ -125,7 +99,7 @@ const generateSoftwareTableHeaders = (deviceUser = false): IDataColumn[] => {
accessor: "name",
Filter: () => null, // input for this column filter is rendered outside of column header
filter: "text", // filters name text based on the user's search query
Cell: (cellProps) => {
Cell: (cellProps: IStringCellProps) => {
const { name, bundle_identifier } = cellProps.row.original;
if (bundle_identifier) {
return (
@@ -148,6 +122,15 @@ const generateSoftwareTableHeaders = (deviceUser = false): IDataColumn[] => {
},
sortType: "caseInsensitive",
},
{
title: "Version",
Header: "Version",
disableSortBy: true,
accessor: "version",
Cell: (cellProps: IStringCellProps) => {
return <TextCell value={cellProps.cell.value} />;
},
},
{
title: "Type",
Header: (cellProps) => (
@@ -158,16 +141,60 @@ const generateSoftwareTableHeaders = (deviceUser = false): IDataColumn[] => {
),
disableSortBy: false,
accessor: "source",
Cell: (cellProps) => (
Cell: (cellProps: IStringCellProps) => (
<TextCell value={cellProps.cell.value} formatter={formatSoftwareType} />
),
},
{
title: "Installed version",
Header: "Installed version",
title: "Vulnerabilities",
Header: "Vulnerabilities",
disableSortBy: true,
accessor: "version",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
accessor: "vulnerabilities",
Cell: (cellProps: IVulnCellProps): JSX.Element => {
const vulnerabilities = cellProps.cell.value || [];
const tooltipText = condenseVulnColumn(vulnerabilities)?.map(
(value) => {
return (
<span key={`vuln_${value}`}>
{value}
<br />
</span>
);
}
);
if (!vulnerabilities?.length) {
return <span className="vulnerabilities text-muted">---</span>;
}
return (
<>
<span
className={`vulnerabilities ${
vulnerabilities.length > 1 ? "text-muted" : ""
}`}
data-tip
data-for={`vulnerabilities__${cellProps.row.original.id.toString()}`}
data-tip-disable={vulnerabilities.length <= 1}
>
{vulnerabilities.length === 1
? vulnerabilities[0].cve
: `${vulnerabilities.length} vulnerabilities`}
</span>
<ReactTooltip
place="top"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id={`vulnerabilities__${cellProps.row.original.id.toString()}`}
data-html
>
<span className={`vulnerabilities tooltip__tooltip-text`}>
{tooltipText}
</span>
</ReactTooltip>
</>
);
},
},
// TODO: Enable after backend has been updated to provide last_opened_at
// {
@@ -202,7 +229,7 @@ const generateSoftwareTableHeaders = (deviceUser = false): IDataColumn[] => {
Header: "",
disableSortBy: true,
accessor: "linkToFilteredHosts",
Cell: (cellProps) => {
Cell: (cellProps: IStringCellProps) => {
return (
<Link
to={`${
@@ -28,12 +28,6 @@ const SoftwareVulnCount = ({
? "1 vulnerability detected"
: `${vulnCount} vulnerabilities detected`}
</div>
{!deviceUser && (
<p>
Click a vulnerable item below to see the associated Common
Vulnerabilites and Exposures (CVEs).
</p>
)}
</div>
) : (
<></>
@@ -1,14 +1,12 @@
.section--software {
.table-container__search-input {
width: 411px;
}
.data-table__table {
table-layout: fixed;
th {
&:first-child {
border-right: none;
width: 16px;
padding-right: 0px;
}
&:nth-child(2) {
width: 25%;
padding-right: 0px;
}
@@ -20,20 +18,9 @@
&.source__header {
width: 20%;
}
&.version__header {
border-right: 0;
}
}
tr {
td {
position: relative;
&:first-child {
padding-right: 0px;
}
}
.software-link {
visibility: hidden;
img {
@@ -98,4 +85,7 @@
font-size: $small !important;
}
}
.text-muted {
color: $ui-fleet-black-50;
}
}
@@ -2,6 +2,7 @@ import React from "react";
import { Link } from "react-router";
import ReactTooltip from "react-tooltip";
import { condenseVulnColumn } from "fleet/helpers";
import PATHS from "router/paths";
import { ISoftware } from "interfaces/software";
import { IVulnerability } from "interfaces/vulnerability";
@@ -44,19 +45,6 @@ interface IHeaderProps {
};
}
const condense = (vulnerabilities: IVulnerability[]): string[] => {
const condensed =
(vulnerabilities?.length &&
vulnerabilities
.slice(-3)
.map((v) => v.cve)
.reverse()) ||
[];
return vulnerabilities.length > 3
? condensed.concat(`+${vulnerabilities.length - 3} more`)
: condensed;
};
const softwareTableHeaders = [
{
title: "Name",
@@ -83,7 +71,7 @@ const softwareTableHeaders = [
accessor: "vulnerabilities",
Cell: (cellProps: IVulnCellProps): JSX.Element => {
const vulnerabilities = cellProps.cell.value || [];
const tooltipText = condense(vulnerabilities)?.map((value) => {
const tooltipText = condenseVulnColumn(vulnerabilities)?.map((value) => {
return (
<span key={`vuln_${value}`}>
{value}
@@ -231,11 +231,6 @@
.id__cell {
padding: 0;
}
.vulnerabilities__cell {
img {
transform: scale(0.5);
}
}
}
tr {