diff --git a/frontend/components/TeamsHeader/TeamsHeader.tsx b/frontend/components/TeamsHeader/TeamsHeader.tsx new file mode 100644 index 0000000000..2e9ee9b674 --- /dev/null +++ b/frontend/components/TeamsHeader/TeamsHeader.tsx @@ -0,0 +1,40 @@ +import React from "react"; + +import { ITeamSummary } from "interfaces/team"; + +import TeamsDropdown from "components/TeamsDropdown"; + +interface ITeamsHeader { + isOnGlobalTeam?: boolean; + currentTeamId?: number; + userTeams?: ITeamSummary[]; + isSandboxMode?: boolean; + onTeamChange: (teamId: number) => void; +} + +const TeamsHeader = ({ + isOnGlobalTeam, + currentTeamId, + userTeams = [], + isSandboxMode = false, + onTeamChange, +}: ITeamsHeader) => { + if (userTeams) { + if (userTeams.length > 1 || isOnGlobalTeam) { + return ( + + ); + } + if (userTeams.length === 1 && !isOnGlobalTeam) { + return

{userTeams[0].name}

; + } + } + return <>; +}; + +export default TeamsHeader; diff --git a/frontend/components/TeamsHeader/index.ts b/frontend/components/TeamsHeader/index.ts new file mode 100644 index 0000000000..5d73c06b4b --- /dev/null +++ b/frontend/components/TeamsHeader/index.ts @@ -0,0 +1 @@ +export { default } from "./TeamsHeader"; diff --git a/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx b/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx index 33f977f8af..e94c84fd1a 100644 --- a/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx +++ b/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx @@ -17,6 +17,8 @@ interface IHostLinkProps { customText?: string; /** Table links shows on row hover only */ rowHover?: boolean; + // don't actually create a link, useful when click is handled by an ancestor + noLink?: boolean; } const baseClass = "view-all-hosts-link"; @@ -29,6 +31,7 @@ const ViewAllHostsLink = ({ responsive = false, customText, rowHover = false, + noLink = false, }: IHostLinkProps): JSX.Element => { const viewAllHostsLinkClass = classnames(baseClass, className, { "row-hover-link": rowHover, @@ -43,7 +46,11 @@ const ViewAllHostsLink = ({ : endpoint; return ( - + {!condensed && ( { return ( <> {isFreeTier &&

Software

} - {isPremiumTier && - userTeams && - (userTeams.length > 1 || isOnGlobalTeam) && ( - - )} - {isPremiumTier && - !isOnGlobalTeam && - userTeams && - userTeams.length === 1 &&

{userTeams[0].name}

} + {isPremiumTier && ( + + )} ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx index 599ab0f394..7cc2664216 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx @@ -18,16 +18,19 @@ interface ISoftwareVulnOSVersions { osVersions: IVulnerability["os_versions"]; isPremiumTier: boolean; router: InjectedRouter; + teamIdForApi?: number; } const SoftwareVulnOSVersions = ({ osVersions, isPremiumTier, router, + teamIdForApi, }: ISoftwareVulnOSVersions) => { - const columnConfigs = useMemo(() => generateColumnConfigs(isPremiumTier), [ - isPremiumTier, - ]); + const columnConfigs = useMemo( + () => generateColumnConfigs(isPremiumTier, router, teamIdForApi), + [isPremiumTier, router, teamIdForApi] + ); const onSelectSingleRow = useCallback( ({ original: { os_version_id } }) => { @@ -36,10 +39,13 @@ const SoftwareVulnOSVersions = ({ } router.push( - `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({ os_version_id })}` + `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({ + os_version_id, + team_id: teamIdForApi, + })}` ); }, - [router] + [teamIdForApi, router] ); const renderVulnerableOSTable = () => { diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx index 4764338b03..9dcbc20d9f 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx @@ -10,6 +10,7 @@ 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"; +import { InjectedRouter } from "react-router"; interface ICellProps { row: { @@ -23,7 +24,11 @@ interface INumberCellProps extends ICellProps { }; } -const generateColumnConfigs = (isPremiumTier: boolean): Column[] => { +const generateColumnConfigs = ( + isPremiumTier: boolean, + router: InjectedRouter, + teamIdForApi?: number +): Column[] => { const configs = [ { Header: "Name", @@ -31,9 +36,21 @@ const generateColumnConfigs = (isPremiumTier: boolean): Column[] => { accessor: "name_only", Cell: ({ row }: ICellProps) => { const { name, os_version_id, platform } = row.original; + const endpoint = PATHS.SOFTWARE_OS_DETAILS(os_version_id); + // since No Teams not supported on this page, falsiness of 0 is okay + const path = teamIdForApi + ? `${endpoint}?team_id=${teamIdForApi}` + : endpoint; + const onClickVulnOs = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + + router?.push(path); + }; return ( @@ -73,6 +90,7 @@ const generateColumnConfigs = (isPremiumTier: boolean): Column[] => { queryParams={{ os_version_id }} responsive rowHover + noLink /> ); diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SoftwareVulnSoftwareVersions.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SoftwareVulnSoftwareVersions.tsx index ffef1aeff3..c19ba2da64 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SoftwareVulnSoftwareVersions.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SoftwareVulnSoftwareVersions.tsx @@ -20,16 +20,19 @@ interface ISoftwareVulnSoftwareVersions { vulnSoftware: IVulnerability["software"]; isPremiumTier: boolean; router: InjectedRouter; + teamIdForApi?: number; } const SoftwareVulnSoftwareVersions = ({ vulnSoftware, isPremiumTier, router, + teamIdForApi, }: ISoftwareVulnSoftwareVersions) => { - const columnConfigs = useMemo(() => generateColumnConfigs(isPremiumTier), [ - isPremiumTier, - ]); + const columnConfigs = useMemo( + () => generateColumnConfigs(isPremiumTier, router, teamIdForApi), + [isPremiumTier, router, teamIdForApi] + ); const onSelectSingleRow = useCallback( ({ original: { id: software_title_id } }) => { @@ -40,10 +43,11 @@ const SoftwareVulnSoftwareVersions = ({ router.push( `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({ software_title_id, + team_id: teamIdForApi, })}` ); }, - [router] + [teamIdForApi, router] ); const renderVulnerableSoftwareTable = () => { return ( diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx index 86116b523f..d9c3dcbded 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx @@ -9,6 +9,7 @@ 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"; +import { InjectedRouter } from "react-router"; interface ICellProps { cell: { @@ -25,7 +26,11 @@ interface IStringCellProps extends ICellProps { }; } -const generateColumnConfigs = (isPremiumTier: boolean): Column[] => { +const generateColumnConfigs = ( + isPremiumTier: boolean, + router: InjectedRouter, + teamIdForApi?: number +): Column[] => { const configs = [ { Header: "Name", @@ -33,9 +38,21 @@ const generateColumnConfigs = (isPremiumTier: boolean): Column[] => { accessor: "name", Cell: ({ row }: ICellProps) => { const { name, id } = row.original; + const endpoint = PATHS.SOFTWARE_VERSION_DETAILS(id.toString()); + // since No Teams not supported on this page, falsiness of 0 is okay + const path = teamIdForApi + ? `${endpoint}?team_id=${teamIdForApi}` + : endpoint; + const onClickVulnSwVersion = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + + router?.push(path); + }; return ( @@ -75,6 +92,7 @@ const generateColumnConfigs = (isPremiumTier: boolean): Column[] => { queryParams={{ software_title_id: id }} responsive rowHover + noLink /> ); diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx index cb7d2fe9db..853901d5aa 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx @@ -17,11 +17,13 @@ const baseClass = "software-vuln-summary"; interface ISoftwareVulnSummaryProps { vuln: IVulnerability; isPremiumTier: boolean; + teamIdForApi?: number; } const SoftwareVulnSummary = ({ vuln, isPremiumTier, + teamIdForApi, }: ISoftwareVulnSummaryProps) => { const { cve, @@ -43,7 +45,7 @@ const SoftwareVulnSummary = ({
diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnerabilityDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnerabilityDetailsPage.tsx index f54576f42b..b747b6972b 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnerabilityDetailsPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnerabilityDetailsPage.tsx @@ -1,16 +1,17 @@ -/** software/vulnerabilities/:cve */ +/* software/vulnerabilities/:cve */ +import React, { useCallback, useContext } from "react"; -import React, { useContext } from "react"; import { useQuery } from "react-query"; import { RouteComponentProps } from "react-router"; - import { AxiosError } from "axios"; +import useTeamIdParam from "hooks/useTeamIdParam"; + import { AppContext } from "context/app"; import { IVulnerability } from "interfaces/vulnerability"; - import softwareVulnAPI, { + IGetVulnerabilityQueryKey, IVulnerabilityResponse, } from "services/entities/vulnerabilities"; @@ -18,6 +19,8 @@ import Spinner from "components/Spinner"; import DataError from "components/DataError"; import Fleet404 from "pages/errors/Fleet404"; import MainContent from "components/MainContent"; +import TeamsHeader from "components/TeamsHeader"; +import Card from "components/Card"; import SoftwareVulnSummary from "./SoftwareVulnSummary/SoftwareVulnSummary"; import SoftwareVulnOSVersions from "./SoftwareVulnOSVersions"; @@ -27,6 +30,7 @@ const baseClass = "software-vulnerability-details-page"; interface ISoftwareVulnerabilityDetailsRouteParams { cve: string; + team_id?: string; } type ISoftwareVulnerabilityDetailsPageProps = RouteComponentProps< @@ -37,24 +41,92 @@ type ISoftwareVulnerabilityDetailsPageProps = RouteComponentProps< const SoftwareVulnerabilityDetailsPage = ({ router, routeParams, + location, }: ISoftwareVulnerabilityDetailsPageProps) => { - const { isPremiumTier } = useContext(AppContext); + const { isPremiumTier, isOnGlobalTeam } = useContext(AppContext); - const cve = routeParams.cve; + const { + currentTeamId, + teamIdForApi, + userTeams, + handleTeamChange, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); const { data: vuln, isLoading: isVulnLoading, isError: isVulnError, error: vulnError, - } = useQuery( - ["softwareVulnByCVE", cve], - () => softwareVulnAPI.getVulnerability(cve), + } = useQuery< + IVulnerabilityResponse, + AxiosError, + IVulnerability, + IGetVulnerabilityQueryKey[] + >( + [ + { + scope: "softwareVulnByCVE", + cve: routeParams.cve, + teamId: teamIdForApi, + }, + ], + ({ queryKey }) => { + return softwareVulnAPI.getVulnerability(queryKey[0]); + }, { select: (data) => data.vulnerability, } ); + const onTeamChange = useCallback( + (teamId: number) => { + handleTeamChange(teamId); + }, + [handleTeamChange] + ); + + const renderVulnTables = () => { + // always the case, just for typing + if (vuln) { + if (vuln.hosts_count === 0) { + return ( + +

Vulnerability not detected

+

+ No host {!!teamIdForApi && "on this team "}is affected by{" "} + {vuln.cve}. +

+
+ ); + } + return ( + <> + {!!vuln.os_versions && vuln.os_versions.length > 0 && ( + + )} + {!!vuln.software && vuln.software.length > 0 && ( + + )} + + ); + } + }; + const renderContent = () => { if (isVulnLoading || !vuln) { return ; @@ -68,24 +140,20 @@ const SoftwareVulnerabilityDetailsPage = ({ } return ( <> + {isPremiumTier && ( + + )} - {!!vuln.os_versions && vuln.os_versions.length > 0 && ( - - )} - {!!vuln.software && vuln.software.length > 0 && ( - - )} + {renderVulnTables()} ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss index bbe910cd13..a1b4ba7e0c 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss @@ -9,7 +9,7 @@ padding: $pad-xxlarge; } - h1, + * > h1, h2 { margin: 0; font-weight: $bold; @@ -22,6 +22,16 @@ font-size: 1rem; } + .not-detected { + gap: 0; + text-align: center; + * { + font-size: $x-small; + line-height: 21px; + margin: 0; + } + } + .resolved-suffix { display: inline; @media (max-width: $break-md) { diff --git a/frontend/services/entities/vulnerabilities.ts b/frontend/services/entities/vulnerabilities.ts index 236284a17c..74df86c683 100644 --- a/frontend/services/entities/vulnerabilities.ts +++ b/frontend/services/entities/vulnerabilities.ts @@ -23,6 +23,15 @@ export interface IGetVulnerabilitiesQueryKey scope: string; } +interface IGetVulnerabilityOptions { + cve: string; + teamId?: number; +} + +export interface IGetVulnerabilityQueryKey extends IGetVulnerabilityOptions { + scope: "softwareVulnByCVE"; +} + export interface IVulnerabilitiesResponse { count: number; counts_updated_at: string; @@ -67,10 +76,14 @@ export const getVulnerabilities = ({ }); }; -const getVulnerability = (cve: string): Promise => { - const { VULNERABILITY } = endpoints; +const getVulnerability = ({ + cve, + teamId, +}: IGetVulnerabilityOptions): Promise => { + const endpoint = endpoints.VULNERABILITY(cve); + const path = teamId ? `${endpoint}?team_id=${teamId}` : endpoint; - // return sendRequest("GET", VULNERABILITY(cve)); // TODO: API INTEGRATION: uncomment when API is ready + // return sendRequest("GET", path); // TODO: API INTEGRATION: uncomment when API is ready return new Promise((resolve, reject) => { resolve(createMockVulnerabilityResponse()); });