mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
UI – Vulnerability details page (#16665)
### _Draft until available to address feedback and merge on Monday – ready for review_ ## Addresses #16472 - Full accounting of features in linked ticket <img width="1676" alt="Screenshot 2024-02-07 at 10 24 03 PM" src="https://github.com/fleetdm/fleet/assets/61553566/aa0cd3b5-9191-4078-b57c-ee451dc5c632"> <img width="909" alt="Screenshot 2024-02-07 at 10 38 52 PM" src="https://github.com/fleetdm/fleet/assets/61553566/ea1b0067-bb91-4502-bde0-0e36914a0829"> ## Checklist for submitter - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
parent
0ec010976a
commit
b9fc6968a5
@ -12,7 +12,7 @@ const DEFAULT_VULNERABILITY: IVulnerability = {
|
||||
details_link: "https://nvd.nist.gov/vuln/detail/CVE-2022-30190",
|
||||
cvss_score: 7.8, // Available in Fleet Premium
|
||||
epss_probability: 0.9729, // Available in Fleet Premium
|
||||
cisa_known_exploit: false, // Available in Fleet Premium
|
||||
cisa_known_exploit: true, // Available in Fleet Premium
|
||||
cve_published: "2022-06-01T00:15:00Z", // Available in Fleet Premium
|
||||
cve_description:
|
||||
"Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability.", // Available in Fleet Premium
|
||||
@ -29,7 +29,20 @@ const DEFAULT_VULNERABILITY: IVulnerability = {
|
||||
generated_cpes: [],
|
||||
},
|
||||
],
|
||||
software: [],
|
||||
software: [
|
||||
{
|
||||
id: 1,
|
||||
name: "bad software",
|
||||
version: "1.1.1",
|
||||
bundle_identifier: "com.bad.software",
|
||||
source: "apps",
|
||||
generated_cpe: "cpe:/a:bad:software:1.1.1",
|
||||
hosts_count: 5,
|
||||
last_opened_at: "2021-08-18T15:11:35Z",
|
||||
installed_paths: ["/Applications/BadSoftware.app"],
|
||||
resolved_in_version: "2",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const createMockVulnerability = (
|
||||
|
@ -1,6 +1,7 @@
|
||||
.card {
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
// TODO - update this to be 40px per style guide
|
||||
padding: $pad-large;
|
||||
|
||||
// radius styles
|
||||
|
18
frontend/components/DataSet/DataSet.stories.tsx
Normal file
18
frontend/components/DataSet/DataSet.stories.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import DataSet from "./DataSet";
|
||||
|
||||
const meta: Meta<typeof DataSet> = {
|
||||
title: "Components/DataSet",
|
||||
component: DataSet,
|
||||
args: {
|
||||
title: "Data set",
|
||||
value: "This is the value",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DataSet>;
|
||||
|
||||
export const Basic: Story = {};
|
19
frontend/components/DataSet/DataSet.tsx
Normal file
19
frontend/components/DataSet/DataSet.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
const baseClass = "data-set";
|
||||
|
||||
interface IDataSetProps {
|
||||
title: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
}
|
||||
|
||||
const DataSet = ({ title, value }: IDataSetProps) => {
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<dt>{title}</dt>
|
||||
<dd>{value}</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataSet;
|
7
frontend/components/DataSet/_styles.scss
Normal file
7
frontend/components/DataSet/_styles.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.data-set {
|
||||
font-size: $x-small;
|
||||
|
||||
dt {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
1
frontend/components/DataSet/index.ts
Normal file
1
frontend/components/DataSet/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./DataSet";
|
@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
|
||||
import { uniqueId } from "lodash";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
|
||||
import { formatFloatAsPercentage } from "utilities/helpers";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
const baseClass = "probability-of-exploit";
|
||||
|
||||
interface IProbabilityOfExploit {
|
||||
probabilityOfExploit: number;
|
||||
cisaKnownExploit?: boolean | null;
|
||||
tooltipPosition?: "top" | "bottom" | "left" | "right";
|
||||
}
|
||||
|
||||
const ProbabilityOfExploit = ({
|
||||
probabilityOfExploit,
|
||||
cisaKnownExploit,
|
||||
tooltipPosition = "top",
|
||||
}: IProbabilityOfExploit) => {
|
||||
const renderExploitedIcon = () => {
|
||||
const tooltipId = uniqueId();
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={`${baseClass} tooltip tooltip__tooltip-icon`}
|
||||
data-tip
|
||||
data-for={tooltipId}
|
||||
>
|
||||
<Icon name="error" size="small" color="status-error" />
|
||||
</span>
|
||||
|
||||
<ReactTooltip
|
||||
place={tooltipPosition}
|
||||
effect="solid"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
id={tooltipId}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={baseClass}>
|
||||
{formatFloatAsPercentage(probabilityOfExploit)}
|
||||
{cisaKnownExploit && renderExploitedIcon()}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProbabilityOfExploit;
|
5
frontend/components/ProbabilityOfExploit/_styles.scss
Normal file
5
frontend/components/ProbabilityOfExploit/_styles.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.probability-of-exploit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
1
frontend/components/ProbabilityOfExploit/index.ts
Normal file
1
frontend/components/ProbabilityOfExploit/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./ProbabilityOfExploit";
|
@ -13,6 +13,8 @@ interface IHostLinkProps {
|
||||
platformLabelId?: number;
|
||||
/** Shows right chevron without text */
|
||||
condensed?: boolean;
|
||||
responsive?: boolean;
|
||||
customText?: string;
|
||||
/** Table links shows on row hover only */
|
||||
rowHover?: boolean;
|
||||
}
|
||||
@ -24,6 +26,8 @@ const ViewAllHostsLink = ({
|
||||
className,
|
||||
platformLabelId,
|
||||
condensed = false,
|
||||
responsive = false,
|
||||
customText,
|
||||
rowHover = false,
|
||||
}: IHostLinkProps): JSX.Element => {
|
||||
const viewAllHostsLinkClass = classnames(baseClass, className, {
|
||||
@ -40,7 +44,13 @@ const ViewAllHostsLink = ({
|
||||
|
||||
return (
|
||||
<Link className={viewAllHostsLinkClass} to={path} title="host-link">
|
||||
{!condensed && <span>View all hosts</span>}
|
||||
{!condensed && (
|
||||
<span
|
||||
className={`${baseClass}__text${responsive ? "--responsive" : ""}`}
|
||||
>
|
||||
{customText ?? "View all hosts"}
|
||||
</span>
|
||||
)}
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
className={`${baseClass}__icon`}
|
||||
|
@ -1,3 +1,10 @@
|
||||
.view-all-hosts-link {
|
||||
@include table-link;
|
||||
&__text {
|
||||
&--responsive {
|
||||
@media (max-width: $break-md) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,9 @@ export interface ISoftware {
|
||||
installed_paths?: string[];
|
||||
}
|
||||
|
||||
export type IVulnerabilitySoftware = Omit<ISoftware, "vulnerabilities">;
|
||||
export type IVulnerabilitySoftware = Omit<ISoftware, "vulnerabilities"> & {
|
||||
resolved_in_version: string;
|
||||
};
|
||||
|
||||
export interface ISoftwareTitleVersion {
|
||||
id: number;
|
||||
|
@ -0,0 +1,47 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import { IVulnerability } from "interfaces/vulnerability";
|
||||
|
||||
import Card from "components/Card";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import generateColumnConfigs from "./SwVulnOSTableConfig";
|
||||
|
||||
const baseClass = "software-vuln-os-versions";
|
||||
|
||||
interface ISoftwareVulnOSVersions {
|
||||
osVersions: IVulnerability["os_versions"];
|
||||
isPremiumTier: boolean;
|
||||
}
|
||||
|
||||
const SoftwareVulnOSVersions = ({
|
||||
osVersions,
|
||||
isPremiumTier,
|
||||
}: ISoftwareVulnOSVersions) => {
|
||||
const columnConfigs = useMemo(() => generateColumnConfigs(isPremiumTier), []);
|
||||
|
||||
const renderVulnerableOSTable = () => {
|
||||
return (
|
||||
<TableContainer
|
||||
columnConfigs={columnConfigs}
|
||||
data={osVersions}
|
||||
defaultSortHeader="hosts"
|
||||
defaultSortDirection="desc"
|
||||
isClientSidePagination
|
||||
resultsTitle={osVersions.length > 1 ? "items" : "item"}
|
||||
isLoading={false} // not rendered otherwise
|
||||
emptyComponent={() => <></>}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card borderRadiusSize="large" includeShadow className={baseClass}>
|
||||
<h2>Vulnerable OS</h2>
|
||||
{renderVulnerableOSTable()}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareVulnOSVersions;
|
@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
|
||||
import { Column } from "react-table";
|
||||
|
||||
import { IVulnerabilityOSVersion } from "interfaces/operating_system";
|
||||
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
|
||||
interface ICellProps {
|
||||
row: {
|
||||
original: IVulnerabilityOSVersion;
|
||||
};
|
||||
}
|
||||
|
||||
interface INumberCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
const generateColumnConfigs = (isPremiumTier: boolean): Column[] => {
|
||||
const configs = [
|
||||
{
|
||||
Header: "Name",
|
||||
disableSortBy: true,
|
||||
accessor: "name_only",
|
||||
Cell: ({ row }: ICellProps) => {
|
||||
const { name, os_version_id, platform } = row.original;
|
||||
return (
|
||||
<LinkCell
|
||||
path={PATHS.SOFTWARE_OS_DETAILS(os_version_id)}
|
||||
value={
|
||||
<>
|
||||
<SoftwareIcon name={platform} />
|
||||
<span className="os-version-name">{name}</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Version",
|
||||
disableSortBy: true,
|
||||
accessor: "version",
|
||||
Cell: ({ cell }: INumberCellProps) => <TextCell value={cell.value} />,
|
||||
},
|
||||
{
|
||||
Header: () => (
|
||||
<>
|
||||
Resolved in <div className="resolved-suffix">version</div>
|
||||
</>
|
||||
),
|
||||
disableSortBy: true,
|
||||
accessor: "resolved_in_version",
|
||||
Cell: ({ cell }: INumberCellProps) => <TextCell value={cell.value} />,
|
||||
},
|
||||
{
|
||||
Header: "Hosts",
|
||||
disableSortBy: true,
|
||||
accessor: "hosts_count",
|
||||
Cell: ({ row }: ICellProps) => {
|
||||
const { hosts_count, os_version_id } = row.original;
|
||||
return (
|
||||
<>
|
||||
<TextCell value={hosts_count} />
|
||||
<ViewAllHostsLink queryParams={{ os_version_id }} responsive />
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!isPremiumTier) {
|
||||
return configs.filter(
|
||||
(header) => header.accessor !== "resolved_in_version"
|
||||
);
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
||||
|
||||
export default generateColumnConfigs;
|
@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareVulnOSVersions";
|
@ -0,0 +1,47 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import { IVulnerability } from "interfaces/vulnerability";
|
||||
|
||||
import Card from "components/Card";
|
||||
import TableContainer from "components/TableContainer";
|
||||
|
||||
import generateColumnConfigs from "./SwVulnSwTableConfig";
|
||||
|
||||
const baseClass = "software-vuln-software-versions";
|
||||
|
||||
interface ISoftwareVulnSoftwareVersions {
|
||||
vulnSoftware: IVulnerability["software"];
|
||||
isPremiumTier: boolean;
|
||||
}
|
||||
|
||||
const SoftwareVulnSoftwareVersions = ({
|
||||
vulnSoftware,
|
||||
isPremiumTier,
|
||||
}: ISoftwareVulnSoftwareVersions) => {
|
||||
const columnConfigs = useMemo(() => generateColumnConfigs(isPremiumTier), []);
|
||||
|
||||
const renderVulnerableSoftwareTable = () => {
|
||||
return (
|
||||
<TableContainer
|
||||
columnConfigs={columnConfigs}
|
||||
data={vulnSoftware}
|
||||
defaultSortHeader="hosts"
|
||||
defaultSortDirection="desc"
|
||||
isClientSidePagination
|
||||
resultsTitle={vulnSoftware.length > 1 ? "items" : "item"}
|
||||
isLoading={false} // not rendered otherwise
|
||||
emptyComponent={() => <></>}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Card borderRadiusSize="large" includeShadow className={`${baseClass}`}>
|
||||
<h2>Vulnerable software</h2>
|
||||
{renderVulnerableSoftwareTable()}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareVulnSoftwareVersions;
|
@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
import { Column } from "react-table";
|
||||
|
||||
import { IVulnerabilitySoftware } from "interfaces/software";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";
|
||||
|
||||
interface ICellProps {
|
||||
cell: {
|
||||
value: number | string | IVulnerabilitySoftware;
|
||||
};
|
||||
row: {
|
||||
original: IVulnerabilitySoftware;
|
||||
};
|
||||
}
|
||||
|
||||
interface IStringCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface INumberCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface IVulnSoftwareCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: IVulnerabilitySoftware;
|
||||
};
|
||||
}
|
||||
|
||||
const generateColumnConfigs = (isPremiumTier: boolean): Column[] => {
|
||||
const configs = [
|
||||
{
|
||||
Header: "Name",
|
||||
disableSortBy: true,
|
||||
accessor: "name",
|
||||
Cell: ({ row }: ICellProps) => {
|
||||
const { name, id } = row.original;
|
||||
return (
|
||||
<LinkCell
|
||||
path={PATHS.SOFTWARE_VERSION_DETAILS(id.toString())}
|
||||
value={
|
||||
<>
|
||||
<SoftwareIcon name={name} />
|
||||
<span className="vuln-sw-name">{name}</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Version",
|
||||
disableSortBy: true,
|
||||
accessor: "version",
|
||||
Cell: ({ cell }: IStringCellProps) => <TextCell value={cell.value} />,
|
||||
},
|
||||
{
|
||||
Header: () => (
|
||||
<>
|
||||
Resolved in <div className="resolved-suffix">version</div>
|
||||
</>
|
||||
),
|
||||
disableSortBy: true,
|
||||
accessor: "resolved_in_version",
|
||||
Cell: ({ cell }: IStringCellProps) => <TextCell value={cell.value} />,
|
||||
},
|
||||
{
|
||||
Header: "Hosts",
|
||||
disableSortBy: true,
|
||||
accessor: "hosts_count",
|
||||
Cell: ({ row }: ICellProps) => {
|
||||
const { hosts_count, id } = row.original;
|
||||
return (
|
||||
<>
|
||||
<TextCell value={hosts_count} />
|
||||
<ViewAllHostsLink
|
||||
queryParams={{ software_title_id: id }}
|
||||
responsive
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!isPremiumTier) {
|
||||
return configs.filter(
|
||||
(header) => header.accessor !== "resolved_in_version"
|
||||
);
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
||||
|
||||
export default generateColumnConfigs;
|
@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareVulnSoftwareVersions";
|
@ -0,0 +1,101 @@
|
||||
import React from "react";
|
||||
|
||||
import { IVulnerability } from "interfaces/vulnerability";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Card from "components/Card";
|
||||
import DataSet from "components/DataSet";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import ProbabilityOfExploit from "components/ProbabilityOfExploit";
|
||||
import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip";
|
||||
|
||||
const baseClass = "software-vuln-summary";
|
||||
|
||||
interface ISoftwareVulnSummaryProps {
|
||||
vuln: IVulnerability;
|
||||
isPremiumTier: boolean;
|
||||
}
|
||||
|
||||
const SoftwareVulnSummary = ({
|
||||
vuln,
|
||||
isPremiumTier,
|
||||
}: ISoftwareVulnSummaryProps) => {
|
||||
const {
|
||||
cve,
|
||||
details_link,
|
||||
cve_description,
|
||||
cvss_score,
|
||||
epss_probability,
|
||||
cisa_known_exploit,
|
||||
cve_published,
|
||||
created_at,
|
||||
hosts_count,
|
||||
} = vuln;
|
||||
|
||||
return (
|
||||
<Card borderRadiusSize="large" includeShadow className={baseClass}>
|
||||
<span className={`${baseClass}__header`}>
|
||||
<h1>{cve}</h1>
|
||||
<span className={`${baseClass}__header__links`}>
|
||||
<CustomLink url={details_link} text="Visit NVD page" newTab />
|
||||
<ViewAllHostsLink
|
||||
customText="View affected hosts"
|
||||
queryParams={{ cve }}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
{isPremiumTier && cve_description && (
|
||||
<div className={`${baseClass}__description`}>{cve_description}</div>
|
||||
)}
|
||||
<dl className={`${baseClass}__description-list`}>
|
||||
{isPremiumTier && (
|
||||
<>
|
||||
{cvss_score && (
|
||||
<DataSet
|
||||
title={
|
||||
<TooltipWrapper tipContent="The worst case impact across different environments (CVSS base score). This data is reported by the National Vulnerability Database (NVD).">
|
||||
Severity
|
||||
</TooltipWrapper>
|
||||
}
|
||||
value={cvss_score}
|
||||
/>
|
||||
)}
|
||||
{epss_probability && (
|
||||
<DataSet
|
||||
title={
|
||||
<TooltipWrapper
|
||||
tipContent="The probability that this vulnerability will be exploited in the next 30 days (EPSS probability). This data is reported by FIRST.org."
|
||||
position="top-end"
|
||||
>
|
||||
Probability of exploit
|
||||
</TooltipWrapper>
|
||||
}
|
||||
value={
|
||||
<ProbabilityOfExploit
|
||||
probabilityOfExploit={epss_probability}
|
||||
cisaKnownExploit={cisa_known_exploit}
|
||||
tooltipPosition="bottom"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{cve_published && (
|
||||
<DataSet
|
||||
title="Published"
|
||||
value={<HumanTimeDiffWithDateTip timeString={cve_published} />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DataSet
|
||||
title="Detected"
|
||||
value={<HumanTimeDiffWithDateTip timeString={created_at} />}
|
||||
/>
|
||||
<DataSet title="Affected hosts" value={hosts_count} />
|
||||
</dl>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareVulnSummary;
|
@ -0,0 +1,46 @@
|
||||
.software-vuln-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $large;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__links {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
|
||||
.component__tooltip-wrapper__underline {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
&__description-list {
|
||||
display: flex;
|
||||
gap: 24px 40px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.data-set {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.component__tooltip-wrapper__underline {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import React, { useContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { IVulnerability } from "interfaces/vulnerability";
|
||||
|
||||
import softwareVulnAPI, {
|
||||
IVulnerabilityResponse,
|
||||
} from "services/entities/vulnerabilities";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import DataError from "components/DataError";
|
||||
import Fleet404 from "pages/errors/Fleet404";
|
||||
import MainContent from "components/MainContent";
|
||||
|
||||
import SoftwareVulnSummary from "./SoftwareVulnSummary/SoftwareVulnSummary";
|
||||
import SoftwareVulnOSVersions from "./SoftwareVulnOSVersions";
|
||||
import SoftwareVulnSoftwareVersions from "./SoftwareVulnSoftwareVersions";
|
||||
|
||||
const baseClass = "software-vulnerability-details-page";
|
||||
|
||||
interface ISoftwareVulnerabilityDetailsRouteParams {
|
||||
cve: string;
|
||||
}
|
||||
|
||||
type ISoftwareVulnerabilityDetailsPageProps = RouteComponentProps<
|
||||
undefined,
|
||||
ISoftwareVulnerabilityDetailsRouteParams
|
||||
>;
|
||||
|
||||
const SoftwareVulnerabilityDetailsPage = ({
|
||||
router,
|
||||
routeParams,
|
||||
}: ISoftwareVulnerabilityDetailsPageProps) => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
|
||||
const cve = routeParams.cve;
|
||||
|
||||
const {
|
||||
data: vuln,
|
||||
isLoading: isVulnLoading,
|
||||
isError: isVulnError,
|
||||
error: vulnError,
|
||||
} = useQuery<IVulnerabilityResponse, AxiosError, IVulnerability>(
|
||||
["softwareVulnByCVE", cve],
|
||||
() => softwareVulnAPI.getVulnerability(cve),
|
||||
{
|
||||
select: (data) => data.vulnerability,
|
||||
}
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isVulnLoading || !vuln) {
|
||||
return <Spinner />;
|
||||
}
|
||||
if (isVulnError) {
|
||||
// confirm okay to cast to AxiosError like this
|
||||
if (vulnError.status === 404) {
|
||||
return <Fleet404 />;
|
||||
}
|
||||
return <DataError />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SoftwareVulnSummary
|
||||
vuln={vuln}
|
||||
isPremiumTier={isPremiumTier ?? false}
|
||||
/>
|
||||
{!!vuln.os_versions && vuln.os_versions.length > 0 && (
|
||||
<SoftwareVulnOSVersions
|
||||
osVersions={vuln.os_versions}
|
||||
isPremiumTier={isPremiumTier ?? false}
|
||||
/>
|
||||
)}
|
||||
{!!vuln.software && vuln.software.length > 0 && (
|
||||
<SoftwareVulnSoftwareVersions
|
||||
vulnSoftware={vuln.software}
|
||||
isPremiumTier={isPremiumTier ?? false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MainContent className={baseClass}>
|
||||
<>{renderContent()}</>
|
||||
</MainContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareVulnerabilityDetailsPage;
|
@ -0,0 +1,73 @@
|
||||
.software-vulnerability-details-page {
|
||||
background-color: $ui-off-white;
|
||||
@include page;
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
padding: $pad-xxlarge;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.resolved-suffix {
|
||||
display: inline;
|
||||
@media (max-width: $break-md) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
&__header {
|
||||
margin-top: 0;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
&__results-count,
|
||||
.results-count {
|
||||
height: initial;
|
||||
}
|
||||
|
||||
.data-table-block .data-table tbody {
|
||||
tr {
|
||||
.hosts_count__cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
// for showing and hiding "view all hosts" link on hover
|
||||
.view-all-hosts-link {
|
||||
opacity: 0;
|
||||
transition: opacity 250ms;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.w250 {
|
||||
min-width: initial;
|
||||
height: min-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
.link-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareVulnerabilityDetailsPage";
|
@ -3,26 +3,12 @@ import React from "react";
|
||||
import { QueryParams } from "utilities/url";
|
||||
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import DataSet from "components/DataSet";
|
||||
|
||||
import SoftwareIcon from "../icons/SoftwareIcon";
|
||||
|
||||
const baseClass = "software-details-summary";
|
||||
|
||||
interface IDescriptionSetProps {
|
||||
title: string;
|
||||
value: React.ReactNode;
|
||||
}
|
||||
|
||||
// TODO: move to frontend/components
|
||||
const DataSet = ({ title, value }: IDescriptionSetProps) => {
|
||||
return (
|
||||
<div className={`${baseClass}__data-set`}>
|
||||
<dt>{title}</dt>
|
||||
<dd>{value}</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISoftwareDetailsSummaryProps {
|
||||
title: string;
|
||||
type?: string;
|
||||
|
@ -29,15 +29,4 @@
|
||||
display: flex;
|
||||
gap: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__data-set {
|
||||
font-size: $x-small;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-xsmall;
|
||||
|
||||
dt {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@ import SoftwareTitleDetailsPage from "pages/SoftwarePage/SoftwareTitleDetailsPag
|
||||
import SoftwareVersionDetailsPage from "pages/SoftwarePage/SoftwareVersionDetailsPage";
|
||||
import TeamSettings from "pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings";
|
||||
import SoftwareOSDetailsPage from "pages/SoftwarePage/SoftwareOSDetailsPage";
|
||||
import SoftwareVulnerabilityDetailsPage from "pages/SoftwarePage/SoftwareVulnerabilityDetailsPage";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
|
||||
@ -226,6 +227,10 @@ const routes = (
|
||||
{/* This redirect keeps the old software/:id working */}
|
||||
<Redirect from=":id" to="versions/:id" />
|
||||
</Route>
|
||||
<Route
|
||||
path="vulnerabilities/:cve"
|
||||
component={SoftwareVulnerabilityDetailsPage}
|
||||
/>
|
||||
<Route path="titles/:id" component={SoftwareTitleDetailsPage} />
|
||||
<Route path="versions/:id" component={SoftwareVersionDetailsPage} />
|
||||
<Route path="os/:id" component={SoftwareOSDetailsPage} />
|
||||
|
@ -61,10 +61,10 @@ export const getVulnerabilities = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getVulnerability = (id: number): Promise<IVulnerabilityResponse> => {
|
||||
const getVulnerability = (cve: string): Promise<IVulnerabilityResponse> => {
|
||||
const { VULNERABILITY } = endpoints;
|
||||
|
||||
// return sendRequest("GET", VULNERABILITY(id)); // TODO: API INTEGRATION: uncomment when API is ready
|
||||
// return sendRequest("GET", VULNERABILITY(cve)); // TODO: API INTEGRATION: uncomment when API is ready
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(createMockVulnerabilityResponse());
|
||||
});
|
||||
|
@ -155,7 +155,8 @@ export default {
|
||||
|
||||
// Vulnerabilities endpoints
|
||||
VULNERABILITIES: `/${API_VERSION}/fleet/vulnerabilities`,
|
||||
VULNERABILITY: (id: number) => `/${API_VERSION}/fleet/vulnerabilities/${id}`,
|
||||
VULNERABILITY: (cve: string) =>
|
||||
`/${API_VERSION}/fleet/vulnerabilities/${cve}`,
|
||||
|
||||
// Script endpoints
|
||||
HOST_SCRIPTS: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/scripts`,
|
||||
|
Loading…
Reference in New Issue
Block a user