mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Add new UI feature: Filter hosts by software version (#2541)
* Add new UI feature: filter hosts by software version * Move specific CVE information to appear alongside aggregate hosts filtered by software version rather than on individual host details page * Relocate users table to below software inventory on host details page
This commit is contained in:
parent
b783b5721f
commit
8c6ca606c3
4
changes/issue-2050-software-filter
Normal file
4
changes/issue-2050-software-filter
Normal file
@ -0,0 +1,4 @@
|
||||
* Add new UI feature: filter hosts by software version
|
||||
* Move specific CVE information to appear alongside aggregate hosts filtered by software version
|
||||
rather than on individual host details page
|
||||
* Relocate users table to below software inventory on host details page
|
@ -24,7 +24,7 @@ import ReactTooltip from "react-tooltip";
|
||||
import Spinner from "components/loaders/Spinner";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/modals/Modal"; // @ts-ignore
|
||||
import SoftwareVulnerabilities from "pages/hosts/HostDetailsPage/SoftwareVulnerabilities"; // @ts-ignore
|
||||
import SoftwareVulnerabilities from "pages/hosts/HostDetailsPage/SoftwareVulnCount"; // @ts-ignore
|
||||
import HostUsersListRow from "pages/hosts/HostDetailsPage/HostUsersListRow";
|
||||
import TableContainer from "components/TableContainer";
|
||||
|
||||
@ -522,7 +522,9 @@ const HostDetailsPage = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<SoftwareVulnerabilities softwareList={host?.software} />
|
||||
{host?.software && (
|
||||
<SoftwareVulnerabilities softwareList={host?.software} />
|
||||
)}
|
||||
{host?.software && (
|
||||
<TableContainer
|
||||
columns={tableHeaders}
|
||||
@ -811,8 +813,8 @@ const HostDetailsPage = ({
|
||||
</div>
|
||||
{renderLabels()}
|
||||
{renderPacks()}
|
||||
{renderUsers()}
|
||||
{host?.software && renderSoftware()}
|
||||
{renderUsers()}
|
||||
{showDeleteHostModal && renderDeleteHostModal()}
|
||||
{showQueryHostModal && (
|
||||
<SelectQueryModal
|
||||
|
@ -1,16 +1,16 @@
|
||||
import React from "react";
|
||||
// import { Link } from "react-router"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id
|
||||
import { Link } from "react-router"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { isEmpty } from "lodash";
|
||||
// import distanceInWordsToNow from "date-fns/distance_in_words_to_now"; // TODO: Enable after backend has been updated to provide last_opened_at
|
||||
|
||||
// import PATHS from "router/paths"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id
|
||||
import PATHS from "router/paths"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import IssueIcon from "../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
|
||||
import QuestionIcon from "../../../../../assets/images/icon-question-16x16@2x.png";
|
||||
// import Chevron from "../../../../../assets/images/icon-chevron-blue-16x16@2x.png"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id
|
||||
import Chevron from "../../../../../assets/images/icon-chevron-blue-16x16@2x.png"; // TODO: Enable after manage hosts page has been updated to filter hosts by software id
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
@ -195,20 +195,25 @@ const generateTableHeaders = (): IDataColumn[] => {
|
||||
// sortType: "dateStrings",
|
||||
// },
|
||||
// TODO: Enable after manage hosts page has been updated to filter hosts by software id
|
||||
// {
|
||||
// title: "",
|
||||
// Header: "",
|
||||
// disableSortBy: true,
|
||||
// accessor: "linkToFilteredHosts",
|
||||
// Cell: (cellProps) => {
|
||||
// return (
|
||||
// <Link to={cellProps.cell.value} className={`software-link`}>
|
||||
// <img alt="link to hosts filtered by software ID" src={Chevron} />
|
||||
// </Link>
|
||||
// );
|
||||
// },
|
||||
// disableHidden: true,
|
||||
// },
|
||||
{
|
||||
title: "",
|
||||
Header: "",
|
||||
disableSortBy: true,
|
||||
accessor: "linkToFilteredHosts",
|
||||
Cell: (cellProps) => {
|
||||
return (
|
||||
<Link
|
||||
to={`${
|
||||
PATHS.MANAGE_HOSTS
|
||||
}?software_id=${cellProps.row.original.id.toString()}`}
|
||||
className={`software-link`}
|
||||
>
|
||||
<img alt="link to hosts filtered by software ID" src={Chevron} />
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
disableHidden: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
// TODO: Replace SoftwareVulnerabilities.jsx with this file upon completion of changes to ManageHostsPage planned that will display vulnerability information along with filter-by-software-id functionality
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { ISoftware } from "interfaces/software";
|
@ -11,18 +11,14 @@
|
||||
|
||||
p {
|
||||
padding-left: $pad-large;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
text-decoration: none;
|
||||
margin-top: $pad-medium;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 16px;
|
||||
width: auto;
|
||||
padding-right: $pad-small;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&__count {
|
@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareVulnCount";
|
@ -1,86 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import softwareInterface from "interfaces/software";
|
||||
import FleetIcon from "../../../../../assets/images/open-new-tab-12x12@2x.png";
|
||||
|
||||
const baseClass = "software-vulnerabilities";
|
||||
|
||||
class SoftwareVulnerabilities extends Component {
|
||||
static propTypes = {
|
||||
softwareList: PropTypes.arrayOf(softwareInterface),
|
||||
};
|
||||
|
||||
render() {
|
||||
const { softwareList } = this.props;
|
||||
|
||||
const vulsList = [];
|
||||
|
||||
const vulnerabilitiesListMaker = () => {
|
||||
softwareList.forEach((software) => {
|
||||
if (software.vulnerabilities) {
|
||||
software.vulnerabilities.forEach((vulnerability) => {
|
||||
vulsList.push({
|
||||
name: software.name,
|
||||
cve: vulnerability.cve,
|
||||
details_link: vulnerability.details_link,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
vulnerabilitiesListMaker();
|
||||
|
||||
const renderVulsCount = (list) => {
|
||||
if (list.length === 1) {
|
||||
return "1 vulnerability detected";
|
||||
}
|
||||
return `${list.length} vulnerabilities detected`;
|
||||
};
|
||||
|
||||
const renderVul = (vul, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
Read more about <b>{vul.name}</b>{" "}
|
||||
<a href={vul.details_link} target="_blank" rel="noopener noreferrer">
|
||||
{vul.cve} vulnerability
|
||||
<img alt="External link" src={FleetIcon} />
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
// No software vulnerabilities
|
||||
if (vulsList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Software vulnerabilities
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<div className={`${baseClass}__count`}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="19"
|
||||
height="19"
|
||||
viewBox="0 -2.5 19 19"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M0 8C0 12.4183 3.5817 16 8 16C12.4183 16 16 12.4183 16 8C16 3.5817 12.4183 0 8 0C3.5817 0 0 3.5817 0 8ZM14 8C14 11.3137 11.3137 14 8 14C4.6863 14 2 11.3137 2 8C2 4.6863 4.6863 2 8 2C11.3137 2 14 4.6863 14 8ZM7 12V10H9V12H7ZM7 4V9H9V4H7Z"
|
||||
fill="#8B8FA2"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{renderVulsCount(vulsList)}
|
||||
</div>
|
||||
<div className={`${baseClass}__list`}>
|
||||
<ul>{vulsList.map((vul, index) => renderVul(vul, index))}</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SoftwareVulnerabilities;
|
@ -380,11 +380,10 @@
|
||||
width: 16px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
// TODO: Enable for link to filtered hosts page after manage hosts page has been updated to filter hosts by software id
|
||||
// &:last-child {
|
||||
// width: 90px;
|
||||
// padding: 0px;
|
||||
// }
|
||||
&:last-child {
|
||||
width: 90px;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useState, useContext, useEffect } from "react";
|
||||
import React, { useState, useContext } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
import { RouteProps } from "react-router/lib/Route";
|
||||
import { find, isEmpty, isEqual, memoize, omit } from "lodash";
|
||||
import { find, isEmpty, isEqual, omit } from "lodash";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
|
||||
import labelsAPI from "services/entities/labels";
|
||||
import statusLabelsAPI from "services/entities/statusLabels";
|
||||
@ -26,6 +27,7 @@ import { IStatusLabels } from "interfaces/status_labels";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IHost } from "interfaces/host";
|
||||
import { IPolicy } from "interfaces/policy";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import { useDeepEffect } from "utilities/hooks"; // @ts-ignore
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
import {
|
||||
@ -69,6 +71,7 @@ import PoliciesFilter from "./components/PoliciesFilter"; // @ts-ignore
|
||||
import EditColumnsModal from "./components/EditColumnsModal/EditColumnsModal";
|
||||
import TransferHostModal from "./components/TransferHostModal";
|
||||
import DeleteHostModal from "./components/DeleteHostModal";
|
||||
import SoftwareVulnerabilities from "./components/SoftwareVulnerabilities";
|
||||
import GenerateInstallerModal from "./components/GenerateInstallerModal";
|
||||
import EditColumnsIcon from "../../../../assets/images/icon-edit-columns-16x16@2x.png";
|
||||
import PencilIcon from "../../../../assets/images/icon-pencil-14x14@2x.png";
|
||||
@ -187,6 +190,9 @@ const ManageHostsPage = ({
|
||||
const [hasHostCountErrors, setHasHostCountErrors] = useState<boolean>(false);
|
||||
const [sortBy, setSortBy] = useState<ISortOption[]>(initialSortBy);
|
||||
const [policy, setPolicy] = useState<IPolicy>();
|
||||
const [softwareDetails, setSoftwareDetails] = useState<ISoftware | null>(
|
||||
null
|
||||
);
|
||||
const [tableQueryData, setTableQueryData] = useState<ITableQueryProps>();
|
||||
// ======== end states
|
||||
|
||||
@ -195,6 +201,7 @@ const ManageHostsPage = ({
|
||||
const routeTemplate = route && route.path ? route.path : "";
|
||||
const policyId = queryParams?.policy_id;
|
||||
const policyResponse: PolicyResponse = queryParams?.policy_response;
|
||||
const softwareId = parseInt(queryParams?.software_id, 10);
|
||||
const { active_label: activeLabel, label_id: labelID } = routeParams;
|
||||
|
||||
// ===== filter matching
|
||||
@ -311,8 +318,11 @@ const ManageHostsPage = ({
|
||||
};
|
||||
|
||||
try {
|
||||
const { hosts: returnedHosts } = await hostsAPI.loadAll(options);
|
||||
const { hosts: returnedHosts, software } = await hostsAPI.loadAll(
|
||||
options
|
||||
);
|
||||
setHosts(returnedHosts);
|
||||
software && setSoftwareDetails(software);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setHasHostErrors(true);
|
||||
@ -378,6 +388,7 @@ const ManageHostsPage = ({
|
||||
teamId: selectedTeam?.id,
|
||||
policyId,
|
||||
policyResponse,
|
||||
softwareId,
|
||||
};
|
||||
|
||||
if (tableQueryData) {
|
||||
@ -411,6 +422,7 @@ const ManageHostsPage = ({
|
||||
teamId: selectedTeam?.id,
|
||||
policyId,
|
||||
policyResponse,
|
||||
softwareId,
|
||||
};
|
||||
|
||||
retrieveHostCount(options);
|
||||
@ -420,6 +432,7 @@ const ManageHostsPage = ({
|
||||
policyId,
|
||||
policyResponse,
|
||||
selectedFilters,
|
||||
softwareId,
|
||||
]);
|
||||
|
||||
const handleLabelChange = ({ slug }: ILabel) => {
|
||||
@ -453,10 +466,15 @@ const ManageHostsPage = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Non-status labels are not compatible with policies so omit policy params from next location
|
||||
// Non-status labels are not compatible with policies or software filters
|
||||
// so omit policies and software params from next location
|
||||
let newQueryParams = queryParams;
|
||||
if (newFilters.find((f) => f.includes(LABEL_SLUG_PREFIX))) {
|
||||
newQueryParams = omit(newQueryParams, ["policy_id", "policy_response"]);
|
||||
newQueryParams = omit(newQueryParams, [
|
||||
"policy_id",
|
||||
"policy_response",
|
||||
"software_id",
|
||||
]);
|
||||
}
|
||||
|
||||
router.replace(
|
||||
@ -494,6 +512,21 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearSoftwareFilter = () => {
|
||||
// TODO: In current UX, clearing the software filter resets all URL params.
|
||||
// The code below can be reimplemented if other URL params are to be preserved.
|
||||
// router.replace(
|
||||
// getNextLocationPath({
|
||||
// pathPrefix: PATHS.MANAGE_HOSTS,
|
||||
// routeTemplate,
|
||||
// routeParams,
|
||||
// queryParams: omit(queryParams, ["software_id"]),
|
||||
// })
|
||||
// );
|
||||
router.replace(PATHS.MANAGE_HOSTS);
|
||||
setSoftwareDetails(null);
|
||||
};
|
||||
|
||||
// The handleChange method below is for the filter-by-team dropdown rather than the dropdown used in modals
|
||||
const handleChangeSelectedTeamFilter = (selectedTeam: number) => {
|
||||
const { MANAGE_HOSTS } = PATHS;
|
||||
@ -627,6 +660,10 @@ const ManageHostsPage = ({
|
||||
newQueryParams.policy_response = policyResponse;
|
||||
}
|
||||
|
||||
if (softwareId && !policyId) {
|
||||
newQueryParams.software_id = softwareId;
|
||||
}
|
||||
|
||||
// triggers useDeepEffect using queryParams
|
||||
router.replace(
|
||||
getNextLocationPath({
|
||||
@ -771,6 +808,7 @@ const ManageHostsPage = ({
|
||||
teamId: currentTeam?.id,
|
||||
policyId,
|
||||
policyResponse,
|
||||
softwareId,
|
||||
});
|
||||
|
||||
toggleTransferHostModal();
|
||||
@ -816,6 +854,7 @@ const ManageHostsPage = ({
|
||||
teamId: currentTeam?.id,
|
||||
policyId,
|
||||
policyResponse,
|
||||
softwareId,
|
||||
});
|
||||
|
||||
refetchLabels();
|
||||
@ -897,6 +936,49 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderSoftwareFilterBlock = () => {
|
||||
if (softwareDetails) {
|
||||
const { name, version } = softwareDetails;
|
||||
const buttonText = name && version ? `${name} ${version}` : "";
|
||||
return (
|
||||
<div className={`${baseClass}__software-filter-block`}>
|
||||
<Button
|
||||
className={`${baseClass}__clear-software-filter`}
|
||||
onClick={handleClearSoftwareFilter}
|
||||
variant={"small-text-icon"}
|
||||
title={name}
|
||||
>
|
||||
<span className="software-filter-button">
|
||||
<span
|
||||
className="software-filter-tooltip"
|
||||
data-tip
|
||||
data-for="software-filter-tooltip"
|
||||
data-tip-disable={!name || !version}
|
||||
>
|
||||
{buttonText}
|
||||
<img src={CloseIcon} alt="Remove software filter" />
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
backgroundColor="#3e4771"
|
||||
id="software-filter-tooltip"
|
||||
data-html
|
||||
>
|
||||
<span className={`tooltip__tooltip-text`}>
|
||||
{`Hosts with ${name}`},<br />
|
||||
{`${version} installed`}
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderEditColumnsModal = () => {
|
||||
if (!showEditColumnsModal || !config || !currentUser) {
|
||||
return null;
|
||||
@ -1012,33 +1094,39 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeaderLabelBlock = ({
|
||||
description,
|
||||
display_text: displayText,
|
||||
label_type: labelType,
|
||||
}: ILabel) => {
|
||||
displayText = PLATFORM_LABEL_DISPLAY_NAMES[displayText] || displayText;
|
||||
const renderHeaderLabelBlock = () => {
|
||||
if (selectedLabel) {
|
||||
const {
|
||||
description,
|
||||
display_text: displayText,
|
||||
label_type: labelType,
|
||||
} = selectedLabel;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__label-block`}>
|
||||
<div className="title">
|
||||
<span>{displayText}</span>
|
||||
{labelType !== "builtin" && !isOnlyObserver && (
|
||||
<>
|
||||
<Button onClick={onEditLabelClick} variant={"text-icon"}>
|
||||
<img src={PencilIcon} alt="Edit label" />
|
||||
</Button>
|
||||
<Button onClick={toggleDeleteLabelModal} variant={"text-icon"}>
|
||||
<img src={TrashIcon} alt="Delete label" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
return (
|
||||
<div className={`${baseClass}__label-block`}>
|
||||
<div className="title">
|
||||
<span>
|
||||
{PLATFORM_LABEL_DISPLAY_NAMES[displayText] || displayText}
|
||||
</span>
|
||||
{labelType !== "builtin" && !isOnlyObserver && (
|
||||
<>
|
||||
<Button onClick={onEditLabelClick} variant={"text-icon"}>
|
||||
<img src={PencilIcon} alt="Edit label" />
|
||||
</Button>
|
||||
<Button onClick={toggleDeleteLabelModal} variant={"text-icon"}>
|
||||
<img src={TrashIcon} alt="Delete label" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="description">
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="description">
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderHeader = () => {
|
||||
@ -1051,24 +1139,36 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderLabelOrPolicyBlock = () => {
|
||||
const type = selectedLabel?.type;
|
||||
|
||||
if (policyId || selectedLabel) {
|
||||
const renderActiveFilterBlock = () => {
|
||||
const showSelectedLabel =
|
||||
selectedLabel &&
|
||||
selectedLabel.type !== "all" &&
|
||||
selectedLabel.type !== "status";
|
||||
if (policyId || softwareId || showSelectedLabel) {
|
||||
return (
|
||||
<div className={`${baseClass}__labels-policies-wrap`}>
|
||||
{policyId && renderPoliciesFilterBlock()}
|
||||
{!policyId &&
|
||||
type !== "all" &&
|
||||
type !== "status" &&
|
||||
selectedLabel &&
|
||||
renderHeaderLabelBlock(selectedLabel)}
|
||||
<div className={`${baseClass}__labels-active-filter-wrap`}>
|
||||
{showSelectedLabel && renderHeaderLabelBlock()}
|
||||
{!!policyId &&
|
||||
!softwareId &&
|
||||
!showSelectedLabel &&
|
||||
renderPoliciesFilterBlock()}
|
||||
{!!softwareId &&
|
||||
!policyId &&
|
||||
!showSelectedLabel &&
|
||||
renderSoftwareFilterBlock()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderSoftwareVulnerabilities = () => {
|
||||
if (softwareDetails) {
|
||||
return <SoftwareVulnerabilities software={softwareDetails} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderForm = () => {
|
||||
if (isAddLabel) {
|
||||
return (
|
||||
@ -1261,7 +1361,8 @@ const ManageHostsPage = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderLabelOrPolicyBlock()}
|
||||
{renderActiveFilterBlock()}
|
||||
{renderSoftwareVulnerabilities()}
|
||||
{config && (!isPremiumTier || teams) && renderTable(selectedTeam)}
|
||||
</div>
|
||||
)}
|
||||
|
@ -241,7 +241,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__labels-policies-wrap {
|
||||
&__labels-active-filter-wrap {
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
@ -260,4 +260,19 @@
|
||||
padding: $pad-small;
|
||||
margin-right: $pad-small;
|
||||
}
|
||||
|
||||
&__software-filter-block {
|
||||
.button--small-text-icon {
|
||||
margin-left: 0;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.software-filter-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.software-filter-tooltip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,70 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { ISoftware } from "interfaces/software";
|
||||
|
||||
import CloseIcon from "../../../../../../assets/images/icon-close-fleet-black-16x16@2x.png";
|
||||
import ExternalLinkIcon from "../../../../../../assets/images/open-new-tab-12x12@2x.png";
|
||||
import IssueIcon from "../../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
|
||||
|
||||
interface ISoftwareVulnerabilitiesProps {
|
||||
software: ISoftware;
|
||||
}
|
||||
|
||||
const baseClass = "software-vulnerabilities";
|
||||
|
||||
const SoftwareVulnerabilities = ({
|
||||
software,
|
||||
}: ISoftwareVulnerabilitiesProps): JSX.Element | null => {
|
||||
const { name, version, vulnerabilities } = software;
|
||||
const count = vulnerabilities?.length;
|
||||
|
||||
const [showVulnerabilities, setShowVulnerablities] = useState<boolean>(true);
|
||||
|
||||
if (count && showVulnerabilities) {
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__count`}>
|
||||
<img alt="Software vulnerabilities found" src={IssueIcon} />
|
||||
{`${
|
||||
count === 1 ? "1 vulnerability" : `${count} vulnerabilities`
|
||||
} detected ${name && version ? `for ${name}, ${version}` : ""}`}
|
||||
</div>
|
||||
<div className={`${baseClass}__ex`}>
|
||||
<button
|
||||
className="button button--unstyled"
|
||||
onClick={() => setShowVulnerablities(!showVulnerabilities)}
|
||||
>
|
||||
<img
|
||||
alt="Dismiss software vulnerabilities banner"
|
||||
src={CloseIcon}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__list`}>
|
||||
<ul>
|
||||
{vulnerabilities?.map((v) => {
|
||||
return (
|
||||
<li key={v.cve}>
|
||||
Read more about <b>{v.cve} vulnerability</b>
|
||||
<a
|
||||
href={v.details_link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img alt="External link" src={ExternalLinkIcon} />
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
export default SoftwareVulnerabilities;
|
@ -0,0 +1,82 @@
|
||||
.software-vulnerabilities {
|
||||
font-size: $x-small;
|
||||
background-color: $ui-off-white;
|
||||
border: solid 1px $ui-fleet-black-50;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
overflow: auto;
|
||||
margin-bottom: $pad-large;
|
||||
padding: $pad-large;
|
||||
padding-bottom: $pad-small;
|
||||
|
||||
p {
|
||||
padding-left: $pad-large;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 16px;
|
||||
width: auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
img {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
&__ex {
|
||||
text-decoration: none;
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
b {
|
||||
color: $core-vibrant-blue;
|
||||
}
|
||||
img {
|
||||
height: 12px;
|
||||
width: auto;
|
||||
padding-left: $pad-small;
|
||||
}
|
||||
ul {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
li {
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
img {
|
||||
height: 12px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ export interface IHostCountLoadOptions {
|
||||
teamId?: number;
|
||||
policyId?: number;
|
||||
policyResponse?: string;
|
||||
softwareId?: number;
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -27,6 +28,7 @@ export default {
|
||||
const policyId = options?.policyId || null;
|
||||
const policyResponse = options?.policyResponse || null;
|
||||
const selectedLabels = options?.selectedLabels || [];
|
||||
const softwareId = options?.softwareId || null;
|
||||
|
||||
const labelPrefix = "labels/";
|
||||
|
||||
@ -65,6 +67,11 @@ export default {
|
||||
queryString += `&policy_response=${policyResponse || "passing"}`; // TODO confirm whether there should be a default if there is an id but no response specified
|
||||
}
|
||||
|
||||
// TODO: consider how to check for mutually exclusive scenarios with label, policy and software
|
||||
if (!label && !policyId && softwareId) {
|
||||
queryString += `&software_id=${softwareId}`;
|
||||
}
|
||||
|
||||
// Append query string to endpoint route after slicing off the leading ampersand
|
||||
const path = `${HOSTS_COUNT}${queryString && `?${queryString.slice(1)}`}`;
|
||||
|
||||
|
@ -17,6 +17,7 @@ export interface IHostLoadOptions {
|
||||
teamId?: number;
|
||||
policyId?: number;
|
||||
policyResponse?: string;
|
||||
softwareId?: number;
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -69,6 +70,7 @@ export default {
|
||||
const teamId = options?.teamId || null;
|
||||
const policyId = options?.policyId || null;
|
||||
const policyResponse = options?.policyResponse || null;
|
||||
const softwareId = options?.softwareId || null;
|
||||
|
||||
// TODO: add this query param logic to client class
|
||||
const pagination = `page=${page}&per_page=${perPage}`;
|
||||
@ -120,7 +122,11 @@ export default {
|
||||
|
||||
if (!label && policyId) {
|
||||
path += `&policy_id=${policyId}`;
|
||||
path += `&policy_response=${policyResponse || "passing"}`; // TODO confirm whether there should be a default if there is an id but no response sepcified
|
||||
path += `&policy_response=${policyResponse || "passing"}`; // TODO: confirm whether there should be a default if there is an id but no response sepcified
|
||||
}
|
||||
// TODO: consider how to check for mutually exclusive scenarios with label, policy and software
|
||||
if (!label && !policyId && softwareId) {
|
||||
path += `&software_id=${softwareId}`;
|
||||
}
|
||||
|
||||
return sendRequest("GET", path);
|
||||
|
Loading…
Reference in New Issue
Block a user