Host Details Page: Software vulnerability column (#4836)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Host details page software table now has search by vulnerabilities and a vulnerabilities column
|
||||
@@ -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(
|
||||
() => ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user