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());
});