Frontend: UI Vulnerabilities feature (#16322)
Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
This commit is contained in:
co-authored by
Gabriel Hernandez
parent
5b0360b517
commit
6e7174c22f
@@ -0,0 +1,36 @@
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
import { IOSVersionsResponse } from "services/entities/operating_systems";
|
||||
import { createMockSoftwareVulnerability } from "./softwareMock";
|
||||
|
||||
const DEFAULT_OS_VERSION: IOperatingSystemVersion = {
|
||||
name: "Mac OS X",
|
||||
name_only: "Mac OS X",
|
||||
version: "10.15.7",
|
||||
platform: "darwin",
|
||||
hosts_count: 1,
|
||||
generated_cpe: "cpe:/o:apple:mac_os_x:10.15.7",
|
||||
vulnerabilities: [createMockSoftwareVulnerability()],
|
||||
};
|
||||
|
||||
export const createMockOSVersion = (
|
||||
overrides?: Partial<IOperatingSystemVersion>
|
||||
): IOperatingSystemVersion => {
|
||||
return { ...DEFAULT_OS_VERSION, ...overrides };
|
||||
};
|
||||
|
||||
const DEFAULT_OS_VERSIONS_RESPONSE: IOSVersionsResponse = {
|
||||
count: 1,
|
||||
counts_updated_at: "2021-01-01T00:00:00Z",
|
||||
os_versions: [createMockOSVersion()],
|
||||
meta: {
|
||||
has_next_results: false,
|
||||
has_previous_results: false,
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const createMockOSVersionsResponse = (
|
||||
overrides?: Partial<IOSVersionsResponse>
|
||||
): IOSVersionsResponse => {
|
||||
return { ...DEFAULT_OS_VERSIONS_RESPONSE, ...overrides };
|
||||
};
|
||||
@@ -29,6 +29,8 @@ export interface ITableQueryData {
|
||||
interface IRowProps extends Row {
|
||||
original: {
|
||||
id?: number;
|
||||
name_only?: string; // Required for onSelectSingleRow of SoftwareOSTable.tsx
|
||||
version?: string; // Required for onSelectSingleRow of SoftwareOSTable.tsx
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface INavItem {
|
||||
};
|
||||
exclude?: boolean;
|
||||
/** If `true`, this nav item will always navigate to the given `location.pathname`. This
|
||||
* is useful when you always want to always naviate to a specific path no matter
|
||||
* is useful when you want to always naviate to a specific path no matter
|
||||
* which child page you are on (e.g. always navigate to /sofware/titles/ when
|
||||
* clicking on the software nav item even if on /software/versions,
|
||||
* software/titles/:id, or /software/versions/:id). Defaults to `undefined`.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ISoftwareVulnerability } from "./software";
|
||||
|
||||
export interface IOperatingSystemVersion {
|
||||
os_id: number;
|
||||
name: string;
|
||||
name_only: string;
|
||||
version: string;
|
||||
platform: string;
|
||||
hosts_count: number;
|
||||
generated_cpe: string;
|
||||
vulnerabilities: ISoftwareVulnerability[];
|
||||
}
|
||||
|
||||
export const OS_VENDOR_BY_PLATFORM: Record<string, string> = {
|
||||
|
||||
@@ -149,7 +149,7 @@ const OperatingSystems = ({
|
||||
}, [isFetching, osInfo, setTitleDescription, setTitleDetail]);
|
||||
|
||||
const tableHeaders = useMemo(
|
||||
() => generateTableHeaders(includeNameColumn, currentTeamId),
|
||||
() => generateTableHeaders(currentTeamId, undefined, { includeName: true }),
|
||||
[includeNameColumn, currentTeamId]
|
||||
);
|
||||
|
||||
|
||||
+105
-29
@@ -1,23 +1,49 @@
|
||||
import React from "react";
|
||||
import { Column } from "react-table";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import {
|
||||
formatOperatingSystemDisplayName,
|
||||
IOperatingSystemVersion,
|
||||
} from "interfaces/operating_system";
|
||||
import { ISoftwareVulnerability } from "interfaces/software";
|
||||
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell";
|
||||
|
||||
import VulnerabilitiesCell from "pages/SoftwarePage/components/VulnerabilitiesCell";
|
||||
import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";
|
||||
|
||||
interface ICellProps {
|
||||
cell: {
|
||||
value: string;
|
||||
value: number | string | ISoftwareVulnerability[];
|
||||
};
|
||||
row: {
|
||||
original: IOperatingSystemVersion;
|
||||
};
|
||||
}
|
||||
|
||||
interface IStringCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface INumberCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface IVulnCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: ISoftwareVulnerability[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
title: string;
|
||||
@@ -25,46 +51,86 @@ interface IHeaderProps {
|
||||
};
|
||||
}
|
||||
|
||||
interface IDataColumn {
|
||||
title: string;
|
||||
Header: ((props: IHeaderProps) => JSX.Element) | string;
|
||||
accessor: string;
|
||||
Cell: (props: ICellProps) => JSX.Element;
|
||||
disableHidden?: boolean;
|
||||
disableSortBy?: boolean;
|
||||
interface IOSTableConfigOptions {
|
||||
includeName?: boolean;
|
||||
includeVulnerabilities?: boolean;
|
||||
includeIcon?: boolean;
|
||||
}
|
||||
|
||||
const generateDefaultTableHeaders = (teamId?: number): IDataColumn[] => [
|
||||
const generateDefaultTableHeaders = (
|
||||
teamId?: number,
|
||||
router?: InjectedRouter,
|
||||
configOptions?: IOSTableConfigOptions
|
||||
): Column[] => [
|
||||
{
|
||||
title: "Name",
|
||||
Header: "Name",
|
||||
disableSortBy: true,
|
||||
accessor: "name_only",
|
||||
Cell: ({ cell: { value } }: ICellProps) => (
|
||||
<TextCell
|
||||
value={value}
|
||||
formatter={(name) => formatOperatingSystemDisplayName(name)}
|
||||
/>
|
||||
),
|
||||
Cell: (cellProps: IStringCellProps) => {
|
||||
if (!configOptions?.includeIcon) {
|
||||
return (
|
||||
<TextCell
|
||||
value={cellProps.cell.value}
|
||||
formatter={(name) => formatOperatingSystemDisplayName(name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { name, name_only, version } = cellProps.row.original;
|
||||
console.log("cellProps.row.original", cellProps.row.original);
|
||||
console.log("name_only, version", name_only, version);
|
||||
const onClickSoftware = (e: React.MouseEvent) => {
|
||||
// Allows for button to be clickable in a clickable row
|
||||
e.stopPropagation();
|
||||
|
||||
router?.push(PATHS.SOFTWARE_OS_DETAILS(name_only, version));
|
||||
};
|
||||
|
||||
return (
|
||||
<LinkCell
|
||||
path={PATHS.SOFTWARE_OS_DETAILS(name_only, version)}
|
||||
customOnClick={onClickSoftware}
|
||||
value={
|
||||
<>
|
||||
<SoftwareIcon name={name} />
|
||||
<span className="software-name">{name}</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Version",
|
||||
Header: "Version",
|
||||
disableSortBy: true,
|
||||
accessor: "version",
|
||||
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
|
||||
Cell: (cellProps: IStringCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Hosts",
|
||||
Header: (cellProps: IHeaderProps) => (
|
||||
Header: "Vulnerabilities",
|
||||
disableSortBy: true,
|
||||
accessor: "vulnerabilities",
|
||||
Cell: (cellProps: IVulnCellProps): JSX.Element => {
|
||||
const platform = cellProps.row.original.platform;
|
||||
if (platform !== "darwin" && platform !== "windows") {
|
||||
return <TextCell value="Not supported" greyed />;
|
||||
}
|
||||
return <VulnerabilitiesCell vulnerabilities={cellProps.cell.value} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: (cellProps: IHeaderProps): JSX.Element => (
|
||||
<HeaderCell
|
||||
value={cellProps.column.title}
|
||||
value="Hosts"
|
||||
disableSortBy={false}
|
||||
isSortedDesc={cellProps.column.isSortedDesc}
|
||||
/>
|
||||
),
|
||||
disableSortBy: false,
|
||||
accessor: "hosts_count",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
Cell: (cellProps: INumberCellProps): JSX.Element => {
|
||||
const { hosts_count, name_only, version } = cellProps.row.original;
|
||||
return (
|
||||
<span className="hosts-cell__wrapper">
|
||||
@@ -88,15 +154,25 @@ const generateDefaultTableHeaders = (teamId?: number): IDataColumn[] => [
|
||||
];
|
||||
|
||||
const generateTableHeaders = (
|
||||
includeName: boolean,
|
||||
teamId?: number
|
||||
): IDataColumn[] => {
|
||||
if (!includeName) {
|
||||
return generateDefaultTableHeaders(teamId).filter(
|
||||
(column) => column.title !== "Name"
|
||||
teamId?: number,
|
||||
router?: InjectedRouter,
|
||||
configOptions?: IOSTableConfigOptions
|
||||
): Column[] => {
|
||||
let tableConfig = generateDefaultTableHeaders(teamId, router, configOptions);
|
||||
|
||||
if (!configOptions?.includeName) {
|
||||
tableConfig = tableConfig.filter(
|
||||
(column) => column.accessor !== "name_only"
|
||||
);
|
||||
}
|
||||
return generateDefaultTableHeaders(teamId);
|
||||
|
||||
if (!configOptions?.includeVulnerabilities) {
|
||||
tableConfig = tableConfig.filter(
|
||||
(column) => column.accessor !== "vulnerabilities"
|
||||
);
|
||||
}
|
||||
|
||||
return tableConfig;
|
||||
};
|
||||
|
||||
export default generateTableHeaders;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import {
|
||||
IGetOSVersionsQueryKey,
|
||||
IOSVersionsResponse,
|
||||
getOSVersions,
|
||||
} from "services/entities/operating_systems";
|
||||
|
||||
import TableDataError from "components/DataError";
|
||||
|
||||
import SoftwareOSTable from "./SoftwareOSTable";
|
||||
|
||||
const baseClass = "software-os";
|
||||
|
||||
interface ISoftwareOSProps {
|
||||
router: InjectedRouter;
|
||||
isSoftwareEnabled: boolean;
|
||||
perPage: number;
|
||||
orderDirection: "asc" | "desc";
|
||||
orderKey: string;
|
||||
currentPage: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
const SoftwareOS = ({
|
||||
router,
|
||||
isSoftwareEnabled,
|
||||
perPage,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
currentPage,
|
||||
teamId,
|
||||
}: ISoftwareOSProps) => {
|
||||
const queryParams = {
|
||||
page: currentPage,
|
||||
per_page: perPage,
|
||||
order_direction: orderDirection,
|
||||
order_key: orderKey,
|
||||
teamId,
|
||||
};
|
||||
|
||||
const { data, isFetching, isError } = useQuery<
|
||||
IOSVersionsResponse,
|
||||
Error,
|
||||
IOSVersionsResponse,
|
||||
IGetOSVersionsQueryKey[]
|
||||
>(
|
||||
[
|
||||
{
|
||||
scope: "software-os",
|
||||
...queryParams,
|
||||
},
|
||||
],
|
||||
() => getOSVersions(queryParams),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 30000,
|
||||
}
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
return <TableDataError className={`${baseClass}__table-error`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SoftwareOSTable
|
||||
router={router}
|
||||
data={data}
|
||||
isSoftwareEnabled={isSoftwareEnabled}
|
||||
perPage={perPage}
|
||||
orderDirection={orderDirection}
|
||||
orderKey={orderKey}
|
||||
currentPage={currentPage}
|
||||
teamId={teamId}
|
||||
isLoading={isFetching}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareOS;
|
||||
@@ -0,0 +1,209 @@
|
||||
import React, { useCallback, useContext, useMemo } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { Row } from "react-table";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
||||
|
||||
import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable";
|
||||
import { IOSVersionsResponse } from "services/entities/operating_systems";
|
||||
|
||||
import generateTableConfig from "pages/DashboardPage/cards/OperatingSystems/OperatingSystemsTableConfig";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
import { getNextLocationPath } from "utilities/helpers";
|
||||
|
||||
const baseClass = "software-os-table";
|
||||
|
||||
interface IRowProps extends Row {
|
||||
original: {
|
||||
version?: string;
|
||||
name_only?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ISoftwareOSTableProps {
|
||||
router: InjectedRouter;
|
||||
isSoftwareEnabled: boolean;
|
||||
data?: IOSVersionsResponse;
|
||||
perPage: number;
|
||||
orderDirection: "asc" | "desc";
|
||||
orderKey: string;
|
||||
currentPage: number;
|
||||
teamId?: number;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const SoftwareOSTable = ({
|
||||
router,
|
||||
isSoftwareEnabled,
|
||||
data,
|
||||
perPage,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
currentPage,
|
||||
teamId,
|
||||
isLoading,
|
||||
}: ISoftwareOSTableProps) => {
|
||||
const { isSandboxMode, noSandboxHosts } = useContext(AppContext);
|
||||
|
||||
const determineQueryParamChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
|
||||
switch (key) {
|
||||
case "sortDirection":
|
||||
return val !== orderDirection;
|
||||
case "sortHeader":
|
||||
return val !== orderKey;
|
||||
case "pageIndex":
|
||||
return val !== currentPage;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return changedEntry?.[0] ?? "";
|
||||
},
|
||||
[currentPage, orderDirection, orderKey]
|
||||
);
|
||||
|
||||
const generateNewQueryParams = useCallback(
|
||||
(newTableQuery: ITableQueryData, changedParam: string) => {
|
||||
return {
|
||||
team_id: teamId,
|
||||
order_direction: newTableQuery.sortDirection,
|
||||
order_key: newTableQuery.sortHeader,
|
||||
page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
|
||||
};
|
||||
},
|
||||
[teamId]
|
||||
);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
// we want to determine which query param has changed in order to
|
||||
// reset the page index to 0 if any other param has changed.
|
||||
const changedParam = determineQueryParamChange(newTableQuery);
|
||||
|
||||
// if nothing has changed, don't update the route. this can happen when
|
||||
// this handler is called on the inital render.
|
||||
if (changedParam === "") return;
|
||||
|
||||
const newRoute = getNextLocationPath({
|
||||
pathPrefix: PATHS.SOFTWARE_OS,
|
||||
routeTemplate: "",
|
||||
queryParams: generateNewQueryParams(newTableQuery, changedParam),
|
||||
});
|
||||
|
||||
router.replace(newRoute);
|
||||
},
|
||||
[determineQueryParamChange, generateNewQueryParams, router]
|
||||
);
|
||||
|
||||
const softwareTableHeaders = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return generateTableConfig(teamId, router, {
|
||||
includeName: true,
|
||||
includeVulnerabilities: true,
|
||||
includeIcon: true,
|
||||
});
|
||||
}, [data, router, teamId]);
|
||||
|
||||
const handleRowSelect = (row: IRowProps) => {
|
||||
const hostsBySoftwareParams = {
|
||||
os_version: row.original.version,
|
||||
os_name: row.original.name_only,
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
const path = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(
|
||||
hostsBySoftwareParams
|
||||
)}`;
|
||||
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const getItemsCountText = () => {
|
||||
const count = data?.count;
|
||||
if (!data?.os_versions || !count) return "";
|
||||
|
||||
return count === 1 ? `${count} item` : `${count} items`;
|
||||
};
|
||||
|
||||
const getLastUpdatedText = () => {
|
||||
if (!data?.os_versions || !data?.counts_updated_at) return "";
|
||||
return (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={data.counts_updated_at}
|
||||
whatToRetrieve={"software"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSoftwareCount = () => {
|
||||
const itemText = getItemsCountText();
|
||||
const lastUpdatedText = getLastUpdatedText();
|
||||
|
||||
if (!itemText) return null;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__count`}>
|
||||
<span>{itemText}</span>
|
||||
{lastUpdatedText}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTableFooter = () => {
|
||||
return (
|
||||
<div>
|
||||
Seeing unexpected software or vulnerabilities?{" "}
|
||||
<CustomLink
|
||||
url={GITHUB_NEW_ISSUE_LINK}
|
||||
text="File an issue on GitHub"
|
||||
newTab
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
columnConfigs={softwareTableHeaders}
|
||||
data={data?.os_versions ?? []}
|
||||
isLoading={isLoading}
|
||||
resultsTitle={"items"}
|
||||
emptyComponent={() => (
|
||||
<EmptySoftwareTable
|
||||
isSoftwareDisabled={!isSoftwareEnabled}
|
||||
isSandboxMode={isSandboxMode}
|
||||
noSandboxHosts={noSandboxHosts}
|
||||
/>
|
||||
)}
|
||||
defaultSortHeader={orderKey}
|
||||
defaultSortDirection={orderDirection}
|
||||
defaultPageIndex={currentPage}
|
||||
manualSortBy
|
||||
pageSize={perPage}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disableNextPage={!data?.meta.has_next_results}
|
||||
searchable={false}
|
||||
onQueryChange={onQueryChange}
|
||||
stackControls
|
||||
renderCount={renderSoftwareCount}
|
||||
renderFooter={renderTableFooter}
|
||||
disableMultiRowSelect
|
||||
onSelectSingleRow={handleRowSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareOSTable;
|
||||
@@ -0,0 +1,40 @@
|
||||
.software-os-table {
|
||||
&__count {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hosts-cell__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.os-hosts-link {
|
||||
opacity: 0;
|
||||
transition: 250ms;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
.os-hosts-link {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
&__data-table-block {
|
||||
.data-table-block {
|
||||
.data-table__table {
|
||||
tbody {
|
||||
.link-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareOSTable";
|
||||
@@ -0,0 +1,3 @@
|
||||
.software-os {
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareOS";
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import osVersionsAPI, {
|
||||
IOSVersionsResponse,
|
||||
} from "services/entities/operating_systems";
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
import { SUPPORT_LINK } from "utilities/constants";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import TableDataError from "components/DataError";
|
||||
import MainContent from "components/MainContent";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import SoftwareDetailsSummary from "../components/SoftwareDetailsSummary";
|
||||
import SoftwareVulnerabilitiesTable from "../components/SoftwareVulnerabilitiesTable";
|
||||
|
||||
const baseClass = "software-os-details-page";
|
||||
|
||||
interface INotSupportedVulnProps {
|
||||
platform: string;
|
||||
}
|
||||
|
||||
const NotSupportedVuln = ({ platform }: INotSupportedVulnProps) => {
|
||||
return (
|
||||
<EmptyTable
|
||||
header="Vulnerabilities are not supported for this type of host"
|
||||
info={
|
||||
<>
|
||||
Interested in vulnerability management for{" "}
|
||||
{platform === "chrome" ? "Chromebooks" : "Linux hosts"}?{" "}
|
||||
<CustomLink url={SUPPORT_LINK} text="Let us know" newTab />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISoftwareOSDetailsPageProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
location: { query: { name: string; version: string } }; // no type in react-router v3
|
||||
}
|
||||
|
||||
const SoftwareOSDetailsPage = ({ location }: ISoftwareOSDetailsPageProps) => {
|
||||
const name = location.query.name;
|
||||
const osVersion = location.query.version;
|
||||
const { data: osVersionDetails, isLoading, isError } = useQuery<
|
||||
IOSVersionsResponse,
|
||||
Error,
|
||||
IOperatingSystemVersion
|
||||
>(
|
||||
["osVersionDetails", name, osVersion],
|
||||
() => osVersionsAPI.getOSVersion({ os_name: name, os_version: osVersion }),
|
||||
{
|
||||
select: (res) => res.os_versions[0],
|
||||
}
|
||||
);
|
||||
|
||||
const renderTable = () => {
|
||||
if (!osVersionDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
osVersionDetails.platform !== "darwin" &&
|
||||
osVersionDetails.platform !== "windows"
|
||||
) {
|
||||
return <NotSupportedVuln platform={osVersionDetails.platform} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SoftwareVulnerabilitiesTable
|
||||
data={osVersionDetails.vulnerabilities}
|
||||
itemName="version"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <TableDataError className={`${baseClass}__table-error`} />;
|
||||
}
|
||||
|
||||
if (!osVersionDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SoftwareDetailsSummary
|
||||
title={`${osVersionDetails.name} ${osVersionDetails.version}`}
|
||||
hosts={osVersionDetails.hosts_count}
|
||||
queryParams={{
|
||||
os_name: osVersionDetails.name_only,
|
||||
os_version: osVersionDetails.version,
|
||||
}}
|
||||
name={osVersionDetails.name}
|
||||
/>
|
||||
{/* TODO: can we use Card here for card styles */}
|
||||
<div className={`${baseClass}__vulnerabilities-section`}>
|
||||
<h2>Vulnerabilities</h2>
|
||||
{renderTable()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MainContent className={baseClass}>
|
||||
<>{renderContent()}</>
|
||||
</MainContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareOSDetailsPage;
|
||||
@@ -0,0 +1,19 @@
|
||||
.software-os-details-page {
|
||||
background-color: $ui-off-white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-medium;
|
||||
|
||||
&__vulnerabilities-section {
|
||||
background-color: $core-white;
|
||||
padding: $pad-xxlarge;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
border-radius: $border-radius-xxlarge;
|
||||
box-shadow: $box-shadow;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: $medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareOSDetailsPage";
|
||||
@@ -39,13 +39,18 @@ const softwareSubNav: ISoftwareSubNavItem[] = [
|
||||
pathname: PATHS.SOFTWARE_TITLES,
|
||||
},
|
||||
{
|
||||
name: "Versions",
|
||||
pathname: PATHS.SOFTWARE_VERSIONS,
|
||||
name: "OS",
|
||||
pathname: PATHS.SOFTWARE_OS,
|
||||
},
|
||||
];
|
||||
|
||||
const getTabIndex = (path: string): number => {
|
||||
return softwareSubNav.findIndex((navItem) => {
|
||||
// This check ensures that for software versions path we still
|
||||
// highlight the software tab.
|
||||
if (navItem.name === "Software" && PATHS.SOFTWARE_VERSIONS === path) {
|
||||
return true;
|
||||
}
|
||||
// tab stays highlighted for paths that start with same pathname
|
||||
return path.startsWith(navItem.pathname);
|
||||
});
|
||||
@@ -107,7 +112,6 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
|
||||
const queryParams = location.query;
|
||||
|
||||
// initial values for query params used on this page
|
||||
const query = queryParams && queryParams.query ? queryParams.query : "";
|
||||
const sortHeader =
|
||||
queryParams && queryParams.order_key
|
||||
? queryParams.order_key
|
||||
@@ -120,6 +124,8 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
|
||||
queryParams && queryParams.page
|
||||
? parseInt(queryParams.page, 10)
|
||||
: DEFAULT_PAGE;
|
||||
// TODO: move these down into the Software Titles component.
|
||||
const query = queryParams && queryParams.query ? queryParams.query : "";
|
||||
const showVulnerableSoftware =
|
||||
queryParams !== undefined && queryParams.vulnerable === "true";
|
||||
|
||||
@@ -322,14 +328,14 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
|
||||
isSoftwareEnabled: Boolean(
|
||||
softwareConfig?.features?.enable_software_inventory
|
||||
),
|
||||
query,
|
||||
// NOTE: may move this lower in tree if we need different values for different pages
|
||||
perPage: DEFAULT_PAGE_SIZE,
|
||||
orderDirection: sortDirection,
|
||||
orderKey: sortHeader,
|
||||
showVulnerableSoftware,
|
||||
currentPage: page,
|
||||
teamId: teamIdForApi,
|
||||
// TODO: move down into the Software Titles component
|
||||
query,
|
||||
showVulnerableSoftware,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -60,12 +60,11 @@ const SoftwareTitleDetailsPage = ({
|
||||
return (
|
||||
<>
|
||||
<SoftwareDetailsSummary
|
||||
id={softwareId}
|
||||
title={softwareTitle.name}
|
||||
type={formatSoftwareType(softwareTitle)}
|
||||
versions={softwareTitle.versions.length}
|
||||
hosts={softwareTitle.hosts_count}
|
||||
queryParam="software_title_id"
|
||||
queryParams={{ software_title_id: softwareId }}
|
||||
name={softwareTitle.name}
|
||||
source={softwareTitle.source}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import React, { useCallback, useContext, useMemo } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { Row } from "react-table";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import { getNextLocationPath } from "utilities/helpers";
|
||||
import {
|
||||
GITHUB_NEW_ISSUE_LINK,
|
||||
VULNERABLE_DROPDOWN_OPTIONS,
|
||||
} from "utilities/constants";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
import {
|
||||
ISoftwareTitlesResponse,
|
||||
ISoftwareVersionsResponse,
|
||||
} from "services/entities/software";
|
||||
import { ISoftwareTitle, ISoftwareVersion } from "interfaces/software";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import Slider from "components/forms/fields/Slider";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
||||
|
||||
import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable";
|
||||
|
||||
import generateTitlesTableConfig from "./SoftwareTitlesTableConfig";
|
||||
import generateVersionsTableConfig from "./SoftwareVersionsTableConfig";
|
||||
|
||||
interface IRowProps extends Row {
|
||||
original: {
|
||||
id?: number;
|
||||
};
|
||||
}
|
||||
|
||||
type ITableConfigGenerator = (router: InjectedRouter, teamId?: number) => void;
|
||||
|
||||
const isSoftwareTitles = (
|
||||
data?: ISoftwareTitlesResponse | ISoftwareVersionsResponse
|
||||
): data is ISoftwareTitlesResponse => {
|
||||
if (!data) return false;
|
||||
return (data as ISoftwareTitlesResponse).software_titles !== undefined;
|
||||
};
|
||||
|
||||
interface ISoftwareTableProps {
|
||||
router: InjectedRouter;
|
||||
data?: ISoftwareTitlesResponse | ISoftwareVersionsResponse;
|
||||
showVersions: boolean;
|
||||
isSoftwareEnabled: boolean;
|
||||
query: string;
|
||||
perPage: number;
|
||||
orderDirection: "asc" | "desc";
|
||||
orderKey: string;
|
||||
showVulnerableSoftware: boolean;
|
||||
currentPage: number;
|
||||
teamId?: number;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "software-table";
|
||||
|
||||
const SoftwareTable = ({
|
||||
router,
|
||||
data,
|
||||
showVersions,
|
||||
isSoftwareEnabled,
|
||||
query,
|
||||
perPage,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
showVulnerableSoftware,
|
||||
currentPage,
|
||||
teamId,
|
||||
isLoading,
|
||||
}: ISoftwareTableProps) => {
|
||||
const { isSandboxMode, noSandboxHosts } = useContext(AppContext);
|
||||
|
||||
const currentPath = showVersions
|
||||
? PATHS.SOFTWARE_VERSIONS
|
||||
: PATHS.SOFTWARE_TITLES;
|
||||
|
||||
const determineQueryParamChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
|
||||
switch (key) {
|
||||
case "searchQuery":
|
||||
return val !== query;
|
||||
case "sortDirection":
|
||||
return val !== orderDirection;
|
||||
case "sortHeader":
|
||||
return val !== orderKey;
|
||||
case "vulnerable":
|
||||
return val !== showVulnerableSoftware.toString();
|
||||
case "pageIndex":
|
||||
return val !== currentPage;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return changedEntry?.[0] ?? "";
|
||||
},
|
||||
[currentPage, orderDirection, orderKey, query, showVulnerableSoftware]
|
||||
);
|
||||
|
||||
const generateNewQueryParams = useCallback(
|
||||
(newTableQuery: ITableQueryData, changedParam: string) => {
|
||||
return {
|
||||
query: newTableQuery.searchQuery,
|
||||
team_id: teamId,
|
||||
order_direction: newTableQuery.sortDirection,
|
||||
order_key: newTableQuery.sortHeader,
|
||||
vulnerable: showVulnerableSoftware.toString(),
|
||||
page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
|
||||
};
|
||||
},
|
||||
[showVulnerableSoftware, teamId]
|
||||
);
|
||||
|
||||
// NOTE: this is called once on initial render and every time the query changes
|
||||
const onQueryChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
// we want to determine which query param has changed in order to
|
||||
// reset the page index to 0 if any other param has changed.
|
||||
const changedParam = determineQueryParamChange(newTableQuery);
|
||||
|
||||
// if nothing has changed, don't update the route. this can happen when
|
||||
// this handler is called on the inital render.
|
||||
if (changedParam === "") return;
|
||||
|
||||
const newRoute = getNextLocationPath({
|
||||
pathPrefix: currentPath,
|
||||
routeTemplate: "",
|
||||
queryParams: generateNewQueryParams(newTableQuery, changedParam),
|
||||
});
|
||||
|
||||
router.replace(newRoute);
|
||||
},
|
||||
[determineQueryParamChange, generateNewQueryParams, router, currentPath]
|
||||
);
|
||||
|
||||
let tableData: ISoftwareTitle[] | ISoftwareVersion[] | undefined;
|
||||
let generateTableConfig: ITableConfigGenerator;
|
||||
|
||||
if (data === undefined) {
|
||||
tableData;
|
||||
generateTableConfig = () => [];
|
||||
} else if (isSoftwareTitles(data)) {
|
||||
tableData = data.software_titles;
|
||||
generateTableConfig = generateTitlesTableConfig;
|
||||
} else {
|
||||
tableData = data.software;
|
||||
generateTableConfig = generateVersionsTableConfig;
|
||||
}
|
||||
|
||||
const softwareTableHeaders = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return generateTableConfig(router, teamId);
|
||||
}, [generateTableConfig, data, router, teamId]);
|
||||
|
||||
// determines if a user be able to search in the table
|
||||
const searchable =
|
||||
isSoftwareEnabled &&
|
||||
(!!tableData || query !== "" || showVulnerableSoftware);
|
||||
|
||||
const getItemsCountText = () => {
|
||||
const count = data?.count;
|
||||
if (!tableData || !count) return "";
|
||||
|
||||
return count === 1 ? `${count} item` : `${count} items`;
|
||||
};
|
||||
|
||||
const getLastUpdatedText = () => {
|
||||
if (!tableData || !data?.counts_updated_at) return "";
|
||||
return (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={data.counts_updated_at}
|
||||
whatToRetrieve={"software"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleShowVersionsToggle = () => {
|
||||
router.replace(
|
||||
getNextLocationPath({
|
||||
pathPrefix: showVersions
|
||||
? PATHS.SOFTWARE_TITLES
|
||||
: PATHS.SOFTWARE_VERSIONS,
|
||||
routeTemplate: "",
|
||||
queryParams: {
|
||||
query,
|
||||
teamId,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
vulnerable: showVulnerableSoftware.toString(),
|
||||
page: 0, // resets page index
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleVulnFilterDropdownChange = (isFilterVulnerable: string) => {
|
||||
router.replace(
|
||||
getNextLocationPath({
|
||||
pathPrefix: currentPath,
|
||||
routeTemplate: "",
|
||||
queryParams: {
|
||||
query,
|
||||
teamId,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
vulnerable: isFilterVulnerable,
|
||||
page: 0, // resets page index
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowSelect = (row: IRowProps) => {
|
||||
const hostsBySoftwareParams = showVersions
|
||||
? {
|
||||
software_version_id: row.original.id,
|
||||
team_id: teamId,
|
||||
}
|
||||
: {
|
||||
software_title_id: row.original.id,
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
const path = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(
|
||||
hostsBySoftwareParams
|
||||
)}`;
|
||||
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const renderSoftwareCount = () => {
|
||||
const itemText = getItemsCountText();
|
||||
const lastUpdatedText = getLastUpdatedText();
|
||||
|
||||
if (!itemText) return null;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__count`}>
|
||||
<span>{itemText}</span>
|
||||
{lastUpdatedText}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCustomFilters = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__filter-controls`}>
|
||||
<div className={`${baseClass}__version-slider`}>
|
||||
{/* div required dropdown form field width bug */}
|
||||
<Slider
|
||||
value={showVersions}
|
||||
onChange={handleShowVersionsToggle}
|
||||
inactiveText="Show versions"
|
||||
activeText="Show versions"
|
||||
/>
|
||||
</div>
|
||||
<Dropdown
|
||||
value={showVulnerableSoftware}
|
||||
className={`${baseClass}__vuln_dropdown`}
|
||||
options={VULNERABLE_DROPDOWN_OPTIONS}
|
||||
searchable={false}
|
||||
onChange={handleVulnFilterDropdownChange}
|
||||
tableFilterDropdown
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTableFooter = () => {
|
||||
return (
|
||||
<div>
|
||||
Seeing unexpected software or vulnerabilities?{" "}
|
||||
<CustomLink
|
||||
url={GITHUB_NEW_ISSUE_LINK}
|
||||
text="File an issue on GitHub"
|
||||
newTab
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
columnConfigs={softwareTableHeaders}
|
||||
data={tableData ?? []}
|
||||
isLoading={isLoading}
|
||||
resultsTitle={"items"}
|
||||
emptyComponent={() => (
|
||||
<EmptySoftwareTable
|
||||
isSoftwareDisabled={!isSoftwareEnabled}
|
||||
isFilterVulnerable={showVulnerableSoftware}
|
||||
isSandboxMode={isSandboxMode}
|
||||
isCollectingSoftware={false} // TODO: update with new API
|
||||
isSearching={query !== ""}
|
||||
noSandboxHosts={noSandboxHosts}
|
||||
/>
|
||||
)}
|
||||
defaultSortHeader={orderKey}
|
||||
defaultSortDirection={orderDirection}
|
||||
defaultPageIndex={currentPage}
|
||||
defaultSearchQuery={query}
|
||||
manualSortBy
|
||||
pageSize={perPage}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disableNextPage={!data?.meta.has_next_results}
|
||||
searchable={searchable}
|
||||
inputPlaceHolder="Search by name or vulnerabilities (CVEs)"
|
||||
onQueryChange={onQueryChange}
|
||||
// additionalQueries serves as a trigger for the useDeepEffect hook
|
||||
// to fire onQueryChange for events happeing outside of
|
||||
// the TableContainer.
|
||||
additionalQueries={showVulnerableSoftware ? "vulnerable" : ""}
|
||||
customControl={searchable ? renderCustomFilters : undefined}
|
||||
stackControls
|
||||
renderCount={renderSoftwareCount}
|
||||
renderFooter={renderTableFooter}
|
||||
disableMultiRowSelect
|
||||
onSelectSingleRow={handleRowSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareTable;
|
||||
+6
-6
@@ -13,9 +13,9 @@ import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import VersionCell from "../components/VersionCell";
|
||||
import VulnerabilitiesCell from "../components/VulnerabilitiesCell";
|
||||
import SoftwareIcon from "../components/icons/SoftwareIcon";
|
||||
import VersionCell from "../../components/VersionCell";
|
||||
import VulnerabilitiesCell from "../../components/VulnerabilitiesCell";
|
||||
import SoftwareIcon from "../../components/icons/SoftwareIcon";
|
||||
|
||||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
@@ -37,9 +37,6 @@ interface IVersionCellProps extends ICellProps {
|
||||
cell: {
|
||||
value: ISoftwareTitleVersion[];
|
||||
};
|
||||
row: {
|
||||
original: ISoftwareTitle;
|
||||
};
|
||||
}
|
||||
|
||||
interface INumberCellProps extends ICellProps {
|
||||
@@ -61,6 +58,9 @@ interface IHeaderProps {
|
||||
}
|
||||
|
||||
const getVulnerabilities = (versions: ISoftwareTitleVersion[]) => {
|
||||
if (!versions) {
|
||||
return [];
|
||||
}
|
||||
const vulnerabilities = versions.reduce((acc: string[], currentVersion) => {
|
||||
if (
|
||||
currentVersion.vulnerabilities &&
|
||||
+2
-4
@@ -13,8 +13,8 @@ import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import VulnerabilitiesCell from "../components/VulnerabilitiesCell";
|
||||
import SoftwareIcon from "../components/icons/SoftwareIcon";
|
||||
import VulnerabilitiesCell from "../../components/VulnerabilitiesCell";
|
||||
import SoftwareIcon from "../../components/icons/SoftwareIcon";
|
||||
|
||||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
@@ -58,8 +58,6 @@ interface IHeaderProps {
|
||||
|
||||
const generateTableHeaders = (
|
||||
router: InjectedRouter,
|
||||
isPremiumTier?: boolean,
|
||||
isSandboxMode?: boolean,
|
||||
teamId?: number
|
||||
): Column[] => {
|
||||
const softwareTableHeaders = [
|
||||
+14
-11
@@ -1,6 +1,4 @@
|
||||
.software-versions {
|
||||
margin-top: $pad-xxlarge;
|
||||
|
||||
.software-table {
|
||||
&__count {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
@@ -26,6 +24,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
// Fix dropdown form bug
|
||||
&__version-slider {
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
&__header {
|
||||
flex-direction: column-reverse; // Search bar on top
|
||||
@@ -66,7 +75,6 @@
|
||||
&__data-table-block {
|
||||
.data-table-block {
|
||||
.data-table__table {
|
||||
|
||||
// for showing and hiding "view all hosts" link on hover
|
||||
tr {
|
||||
.software-link {
|
||||
@@ -93,7 +101,7 @@
|
||||
|
||||
@media (min-width: $break-lg) {
|
||||
// expand the width of version header at larger screen sizes
|
||||
.version__header {
|
||||
.versions__header {
|
||||
width: $col-md;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +130,6 @@
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
|
||||
.hosts_count__cell {
|
||||
.hosts-cell__wrapper {
|
||||
display: flex;
|
||||
@@ -143,7 +150,7 @@
|
||||
}
|
||||
|
||||
@media (min-width: $break-lg) {
|
||||
.version__cell {
|
||||
.versions__cell {
|
||||
width: $col-md;
|
||||
}
|
||||
}
|
||||
@@ -159,8 +166,4 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__table-error {
|
||||
margin-top: $pad-xxxlarge;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareTable";
|
||||
@@ -1,45 +1,34 @@
|
||||
import React, { useCallback, useContext, useMemo } from "react";
|
||||
import React from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { useQuery } from "react-query";
|
||||
import { Row } from "react-table";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import softwareAPI, {
|
||||
ISoftwareApiParams,
|
||||
ISoftwareTitlesResponse,
|
||||
ISoftwareVersionsResponse,
|
||||
} from "services/entities/software";
|
||||
import { AppContext } from "context/app";
|
||||
import {
|
||||
GITHUB_NEW_ISSUE_LINK,
|
||||
VULNERABLE_DROPDOWN_OPTIONS,
|
||||
} from "utilities/constants";
|
||||
import { getNextLocationPath } from "utilities/helpers";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import Spinner from "components/Spinner";
|
||||
import TableDataError from "components/DataError";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
||||
|
||||
import EmptySoftwareTable from "../components/EmptySoftwareTable";
|
||||
|
||||
import generateSoftwareTitlesTableHeaders from "./SoftwareTitlesTableConfig";
|
||||
import SoftwareTable from "./SoftwareTable";
|
||||
|
||||
const baseClass = "software-titles";
|
||||
|
||||
interface IRowProps extends Row {
|
||||
original: {
|
||||
id?: number;
|
||||
};
|
||||
}
|
||||
const DATA_STALE_TIME = 30000;
|
||||
const QUERY_OPTIONS = {
|
||||
keepPreviousData: true,
|
||||
staleTime: DATA_STALE_TIME,
|
||||
};
|
||||
|
||||
interface ISoftwareTitlesQueryKey extends ISoftwareApiParams {
|
||||
scope: "software-titles";
|
||||
}
|
||||
|
||||
interface ISoftwareVersionsQueryKey extends ISoftwareApiParams {
|
||||
scope: "software-versions";
|
||||
}
|
||||
|
||||
interface ISoftwareTitlesProps {
|
||||
router: InjectedRouter;
|
||||
isSoftwareEnabled: boolean;
|
||||
@@ -63,13 +52,13 @@ const SoftwareTitles = ({
|
||||
currentPage,
|
||||
teamId,
|
||||
}: ISoftwareTitlesProps) => {
|
||||
const { isSandboxMode, noSandboxHosts } = useContext(AppContext);
|
||||
const showVersions = location.pathname === PATHS.SOFTWARE_VERSIONS;
|
||||
|
||||
// request to get software data
|
||||
const {
|
||||
data: softwareData,
|
||||
isLoading: isSoftwareLoading,
|
||||
isError: isSoftwareError,
|
||||
data: titlesData,
|
||||
isFetching: isTitlesFetching,
|
||||
isError: isTitlesError,
|
||||
} = useQuery<
|
||||
ISoftwareTitlesResponse,
|
||||
Error,
|
||||
@@ -90,211 +79,64 @@ const SoftwareTitles = ({
|
||||
],
|
||||
({ queryKey }) => softwareAPI.getSoftwareTitles(queryKey[0]),
|
||||
{
|
||||
// stale time can be adjusted if fresher data is desired based on
|
||||
// software inventory interval
|
||||
staleTime: 30000,
|
||||
...QUERY_OPTIONS,
|
||||
enabled: location.pathname === PATHS.SOFTWARE_TITLES,
|
||||
}
|
||||
);
|
||||
|
||||
// determines if a user be able to search in the table
|
||||
const searchable =
|
||||
isSoftwareEnabled &&
|
||||
(!!softwareData?.software_titles || query !== "" || showVulnerableSoftware);
|
||||
|
||||
const softwareTableHeaders = useMemo(
|
||||
() => generateSoftwareTitlesTableHeaders(router, teamId),
|
||||
[router, teamId]
|
||||
// request to get software versions data
|
||||
const {
|
||||
data: versionsData,
|
||||
isFetching: isVersionsFetching,
|
||||
isError: isVersionsError,
|
||||
} = useQuery<
|
||||
ISoftwareVersionsResponse,
|
||||
Error,
|
||||
ISoftwareVersionsResponse,
|
||||
ISoftwareVersionsQueryKey[]
|
||||
>(
|
||||
[
|
||||
{
|
||||
scope: "software-versions",
|
||||
page: currentPage,
|
||||
perPage,
|
||||
query,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
teamId,
|
||||
vulnerable: showVulnerableSoftware,
|
||||
},
|
||||
],
|
||||
({ queryKey }) => softwareAPI.getSoftwareVersions(queryKey[0]),
|
||||
{
|
||||
...QUERY_OPTIONS,
|
||||
enabled: location.pathname === PATHS.SOFTWARE_VERSIONS,
|
||||
}
|
||||
);
|
||||
|
||||
const handleVulnFilterDropdownChange = (isFilterVulnerable: string) => {
|
||||
router.replace(
|
||||
getNextLocationPath({
|
||||
pathPrefix: PATHS.SOFTWARE_TITLES,
|
||||
routeTemplate: "",
|
||||
queryParams: {
|
||||
query,
|
||||
teamId,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
vulnerable: isFilterVulnerable,
|
||||
page: 0, // resets page index
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
if (isTitlesFetching) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const handleRowSelect = (row: IRowProps) => {
|
||||
const hostsBySoftwareParams = {
|
||||
software_title_id: row.original.id,
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
const path = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(
|
||||
hostsBySoftwareParams
|
||||
)}`;
|
||||
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const determineQueryParamChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
|
||||
switch (key) {
|
||||
case "searchQuery":
|
||||
return val !== query;
|
||||
case "sortDirection":
|
||||
return val !== orderDirection;
|
||||
case "sortHeader":
|
||||
return val !== orderKey;
|
||||
case "vulnerable":
|
||||
return val !== showVulnerableSoftware.toString();
|
||||
case "pageIndex":
|
||||
return val !== currentPage;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return changedEntry?.[0] ?? "";
|
||||
},
|
||||
[currentPage, orderDirection, orderKey, query, showVulnerableSoftware]
|
||||
);
|
||||
|
||||
const generateNewQueryParams = useCallback(
|
||||
(newTableQuery: ITableQueryData, changedParam: string) => {
|
||||
return {
|
||||
query: newTableQuery.searchQuery,
|
||||
team_id: teamId,
|
||||
order_direction: newTableQuery.sortDirection,
|
||||
order_key: newTableQuery.sortHeader,
|
||||
vulnerable: showVulnerableSoftware.toString(),
|
||||
page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
|
||||
};
|
||||
},
|
||||
[showVulnerableSoftware, teamId]
|
||||
);
|
||||
|
||||
// NOTE: this is called once on initial render and every time the query changes
|
||||
const onQueryChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
// we want to determine which query param has changed in order to
|
||||
// reset the page index to 0 if any other param has changed.
|
||||
const changedParam = determineQueryParamChange(newTableQuery);
|
||||
|
||||
// if nothing has changed, don't update the route. this can happen when
|
||||
// this handler is called on the inital render.
|
||||
if (changedParam === "") return;
|
||||
|
||||
const newRoute = getNextLocationPath({
|
||||
pathPrefix: PATHS.SOFTWARE_TITLES,
|
||||
routeTemplate: "",
|
||||
queryParams: generateNewQueryParams(newTableQuery, changedParam),
|
||||
});
|
||||
|
||||
router.replace(newRoute);
|
||||
},
|
||||
[determineQueryParamChange, generateNewQueryParams, router]
|
||||
);
|
||||
|
||||
const getItemsCountText = () => {
|
||||
const count = softwareData?.count;
|
||||
if (!softwareData || !count) return "";
|
||||
|
||||
return count === 1 ? `${count} item` : `${count} items`;
|
||||
};
|
||||
|
||||
const getLastUpdatedText = () => {
|
||||
if (!softwareData || !softwareData.counts_updated_at) return "";
|
||||
return (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={softwareData.counts_updated_at}
|
||||
whatToRetrieve={"software"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSoftwareCount = () => {
|
||||
const itemText = getItemsCountText();
|
||||
const lastUpdatedText = getLastUpdatedText();
|
||||
|
||||
if (!itemText) return null;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__count`}>
|
||||
<span>{itemText}</span>
|
||||
{lastUpdatedText}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderVulnFilterDropdown = () => {
|
||||
return (
|
||||
<Dropdown
|
||||
value={showVulnerableSoftware}
|
||||
className={`${baseClass}__vuln_dropdown`}
|
||||
options={VULNERABLE_DROPDOWN_OPTIONS}
|
||||
searchable={false}
|
||||
onChange={handleVulnFilterDropdownChange}
|
||||
tableFilterDropdown
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTableFooter = () => {
|
||||
return (
|
||||
<div>
|
||||
Seeing unexpected software or vulnerabilities?{" "}
|
||||
<CustomLink
|
||||
url={GITHUB_NEW_ISSUE_LINK}
|
||||
text="File an issue on GitHub"
|
||||
newTab
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (isSoftwareError) {
|
||||
if (isTitlesError || isVersionsError) {
|
||||
return <TableDataError className={`${baseClass}__table-error`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
columnConfigs={softwareTableHeaders}
|
||||
data={softwareData?.software_titles || []}
|
||||
isLoading={isSoftwareLoading}
|
||||
resultsTitle={"items"}
|
||||
emptyComponent={() => (
|
||||
<EmptySoftwareTable
|
||||
isSoftwareDisabled={!isSoftwareEnabled}
|
||||
isFilterVulnerable={showVulnerableSoftware}
|
||||
isSandboxMode={isSandboxMode}
|
||||
isCollectingSoftware={false} // TODO: update with new API
|
||||
isSearching={query !== ""}
|
||||
noSandboxHosts={noSandboxHosts}
|
||||
/>
|
||||
)}
|
||||
defaultSortHeader={orderKey}
|
||||
defaultSortDirection={orderDirection}
|
||||
defaultPageIndex={currentPage}
|
||||
defaultSearchQuery={query}
|
||||
manualSortBy
|
||||
pageSize={perPage}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disableNextPage={!softwareData?.meta.has_next_results}
|
||||
searchable={searchable}
|
||||
inputPlaceHolder="Search by name or vulnerabilities (CVEs)"
|
||||
onQueryChange={onQueryChange}
|
||||
// additionalQueries serves as a trigger for the useDeepEffect hook
|
||||
// to fire onQueryChange for events happeing outside of
|
||||
// the TableContainer.
|
||||
additionalQueries={showVulnerableSoftware ? "vulnerable" : ""}
|
||||
customControl={searchable ? renderVulnFilterDropdown : undefined}
|
||||
stackControls
|
||||
renderCount={renderSoftwareCount}
|
||||
renderFooter={renderTableFooter}
|
||||
disableMultiRowSelect
|
||||
onSelectSingleRow={handleRowSelect}
|
||||
<SoftwareTable
|
||||
router={router}
|
||||
data={showVersions ? versionsData : titlesData}
|
||||
showVersions={showVersions}
|
||||
isSoftwareEnabled={isSoftwareEnabled}
|
||||
query={query}
|
||||
perPage={perPage}
|
||||
orderDirection={orderDirection}
|
||||
orderKey={orderKey}
|
||||
showVulnerableSoftware={showVulnerableSoftware}
|
||||
currentPage={currentPage}
|
||||
teamId={teamId}
|
||||
isLoading={isTitlesFetching || isVersionsFetching}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,164 +1,6 @@
|
||||
.software-titles {
|
||||
margin-top: $pad-xxlarge;
|
||||
|
||||
&__count {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__vuln_dropdown {
|
||||
.Select-menu-outer {
|
||||
width: 250px;
|
||||
max-height: 310px;
|
||||
|
||||
.Select-menu {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
.Select-value {
|
||||
padding-left: $pad-medium;
|
||||
padding-right: $pad-medium;
|
||||
}
|
||||
|
||||
.dropdown__custom-value-label {
|
||||
width: 155px; // Override 105px for longer text options
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
&__header {
|
||||
flex-direction: column-reverse; // Search bar on top
|
||||
margin-bottom: $pad-medium;
|
||||
|
||||
@media (min-width: $break-md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
&__header-left {
|
||||
flex-direction: row; // Filter dropdown aligned with count
|
||||
|
||||
.controls {
|
||||
.form-field--dropdown {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__search-input,
|
||||
&__search {
|
||||
width: 100%; // Search bar across entire table
|
||||
|
||||
.input-icon-field__input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: $break-md) {
|
||||
width: auto;
|
||||
|
||||
.input-icon-field__input {
|
||||
width: 375px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__data-table-block {
|
||||
.data-table-block {
|
||||
.data-table__table {
|
||||
|
||||
// for showing and hiding "view all hosts" link on hover
|
||||
tr {
|
||||
.software-link {
|
||||
opacity: 0;
|
||||
transition: opacity 250ms;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.software-link {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
.name__header {
|
||||
width: $col-md;
|
||||
}
|
||||
|
||||
.hosts_count__header {
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $break-lg) {
|
||||
// expand the width of version header at larger screen sizes
|
||||
.versions__header {
|
||||
width: $col-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
.name__cell {
|
||||
max-width: $col-md;
|
||||
|
||||
// Tooltip does not get cut off
|
||||
.children-wrapper {
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
// ellipsis for software name
|
||||
.software-name {
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.link-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
.hosts_count__cell {
|
||||
.hosts-cell__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.hosts-cell__link {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $break-sm) {
|
||||
.name__cell {
|
||||
max-width: $col-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $break-lg) {
|
||||
.versions__cell {
|
||||
width: $col-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// needed to handle overflow of the table data on small screens
|
||||
.data-table {
|
||||
&__wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__table-error {
|
||||
margin-top: $pad-xxxlarge;
|
||||
}
|
||||
|
||||
+14
-62
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
|
||||
@@ -10,18 +10,13 @@ import hostsCountAPI, {
|
||||
IHostsCountResponse,
|
||||
} from "services/entities/host_count";
|
||||
import { ISoftwareVersion, formatSoftwareType } from "interfaces/software";
|
||||
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import MainContent from "components/MainContent";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import TableDataError from "components/DataError";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
import generateSoftwareVersionDetailsTableConfig from "./SoftwareVersionDetailsTableConfig";
|
||||
import SoftwareDetailsSummary from "../components/SoftwareDetailsSummary";
|
||||
import SoftwareVulnerabilitiesTable from "../components/SoftwareVulnerabilitiesTable";
|
||||
|
||||
const baseClass = "software-version-details-page";
|
||||
|
||||
@@ -34,29 +29,10 @@ type ISoftwareTitleDetailsPageProps = RouteComponentProps<
|
||||
ISoftwareVersionDetailsRouteParams
|
||||
>;
|
||||
|
||||
const NoVulnsDetected = (): JSX.Element => {
|
||||
return (
|
||||
<EmptyTable
|
||||
header="No vulnerabilities detected for this software item."
|
||||
info={
|
||||
<>
|
||||
Expecting to see vulnerabilities?{" "}
|
||||
<CustomLink
|
||||
url={GITHUB_NEW_ISSUE_LINK}
|
||||
text="File an issue on GitHub"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SoftwareVersionDetailsPage = ({
|
||||
routeParams,
|
||||
}: ISoftwareTitleDetailsPageProps) => {
|
||||
const versionId = parseInt(routeParams.id, 10);
|
||||
const { isPremiumTier, isSandboxMode } = useContext(AppContext);
|
||||
|
||||
const {
|
||||
data: softwareVersion,
|
||||
@@ -70,12 +46,12 @@ const SoftwareVersionDetailsPage = ({
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: Confirm desired UX for error and loading states
|
||||
const {
|
||||
data: hostsCount,
|
||||
// isError: isHostsCountError,
|
||||
// isLoading: isHostsCountLoading,
|
||||
} = useQuery<IHostsCountResponse, Error, number, IHostsCountQueryKey[]>(
|
||||
const { data: hostsCount } = useQuery<
|
||||
IHostsCountResponse,
|
||||
Error,
|
||||
number,
|
||||
IHostsCountQueryKey[]
|
||||
>(
|
||||
[{ scope: "hosts_count", softwareVersionId: versionId }],
|
||||
({ queryKey }) => hostsCountAPI.load(queryKey[0]),
|
||||
{
|
||||
@@ -85,15 +61,6 @@ const SoftwareVersionDetailsPage = ({
|
||||
}
|
||||
);
|
||||
|
||||
const tableHeaders = useMemo(
|
||||
() =>
|
||||
generateSoftwareVersionDetailsTableConfig(
|
||||
Boolean(isPremiumTier),
|
||||
Boolean(isSandboxMode)
|
||||
),
|
||||
[isPremiumTier, isSandboxMode]
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isSoftwareVersionLoading) {
|
||||
return <Spinner />;
|
||||
@@ -110,35 +77,20 @@ const SoftwareVersionDetailsPage = ({
|
||||
return (
|
||||
<>
|
||||
<SoftwareDetailsSummary
|
||||
id={softwareVersion.id}
|
||||
title={`${softwareVersion.name}, ${softwareVersion.version}`}
|
||||
type={formatSoftwareType(softwareVersion)}
|
||||
hosts={hostsCount ?? 0}
|
||||
queryParam="software_version_id"
|
||||
queryParams={{ software_version_id: softwareVersion.id }}
|
||||
name={softwareVersion.name}
|
||||
source={softwareVersion.source}
|
||||
/>
|
||||
<div className={`${baseClass}__vulnerabilities-section`}>
|
||||
<h2 className="section__header">Vulnerabilities</h2>
|
||||
{softwareVersion?.vulnerabilities?.length ? (
|
||||
<div className="vuln-table">
|
||||
<TableContainer
|
||||
columnConfigs={tableHeaders}
|
||||
data={softwareVersion.vulnerabilities}
|
||||
defaultSortHeader={isPremiumTier ? "epss_probability" : "cve"}
|
||||
defaultSortDirection={"desc"}
|
||||
emptyComponent={NoVulnsDetected}
|
||||
isAllPagesSelected={false}
|
||||
isLoading={isSoftwareVersionLoading}
|
||||
isClientSidePagination
|
||||
pageSize={20}
|
||||
resultsTitle={"vulnerabilities"}
|
||||
showMarkAllPages={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<NoVulnsDetected />
|
||||
)}
|
||||
<SoftwareVulnerabilitiesTable
|
||||
data={softwareVersion.vulnerabilities ?? []}
|
||||
itemName="software item"
|
||||
isLoading={isSoftwareVersionLoading}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,10 +16,4 @@
|
||||
font-size: $medium;
|
||||
}
|
||||
}
|
||||
|
||||
// used to position header text with premium icon correctly
|
||||
.column-header {
|
||||
display: flex;
|
||||
gap: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
import React, { useCallback, useContext, useMemo } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { useQuery } from "react-query";
|
||||
import { Row } from "react-table";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import softwareAPI, {
|
||||
ISoftwareApiParams,
|
||||
ISoftwareVersionsResponse,
|
||||
} from "services/entities/software";
|
||||
import { AppContext } from "context/app";
|
||||
import {
|
||||
GITHUB_NEW_ISSUE_LINK,
|
||||
VULNERABLE_DROPDOWN_OPTIONS,
|
||||
} from "utilities/constants";
|
||||
import { getNextLocationPath } from "utilities/helpers";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import TableDataError from "components/DataError";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
||||
|
||||
import EmptySoftwareTable from "../components/EmptySoftwareTable";
|
||||
|
||||
import generateSoftwareVersionsTableHeaders from "./SoftwareVersionsTableConfig";
|
||||
|
||||
const baseClass = "software-versions";
|
||||
|
||||
interface IRowProps extends Row {
|
||||
original: {
|
||||
id?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ISoftwareVersionsQueryKey extends ISoftwareApiParams {
|
||||
scope: "software-versions";
|
||||
}
|
||||
|
||||
interface ISoftwareVersionsProps {
|
||||
router: InjectedRouter;
|
||||
isSoftwareEnabled: boolean;
|
||||
query: string;
|
||||
perPage: number;
|
||||
orderDirection: "asc" | "desc";
|
||||
orderKey: string;
|
||||
showVulnerableSoftware: boolean;
|
||||
currentPage: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
const SoftwareVersions = ({
|
||||
router,
|
||||
isSoftwareEnabled,
|
||||
query,
|
||||
perPage,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
showVulnerableSoftware,
|
||||
currentPage,
|
||||
teamId,
|
||||
}: ISoftwareVersionsProps) => {
|
||||
const { isSandboxMode, noSandboxHosts, isPremiumTier } = useContext(
|
||||
AppContext
|
||||
);
|
||||
|
||||
// request to get software versions data
|
||||
const {
|
||||
data: softwareVersionsData,
|
||||
isLoading: isSoftwareVersionsLoading,
|
||||
isError: isSoftwareVersionsError,
|
||||
} = useQuery<
|
||||
ISoftwareVersionsResponse,
|
||||
Error,
|
||||
ISoftwareVersionsResponse,
|
||||
ISoftwareVersionsQueryKey[]
|
||||
>(
|
||||
[
|
||||
{
|
||||
scope: "software-versions",
|
||||
page: currentPage,
|
||||
perPage,
|
||||
query,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
teamId,
|
||||
vulnerable: showVulnerableSoftware,
|
||||
},
|
||||
],
|
||||
({ queryKey }) => softwareAPI.getSoftwareVersions(queryKey[0]),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
// stale time can be adjusted if fresher data is desired based on
|
||||
// software inventory interval
|
||||
staleTime: 30000,
|
||||
}
|
||||
);
|
||||
|
||||
// determines if a user be able to search in the table
|
||||
const searchable =
|
||||
isSoftwareEnabled &&
|
||||
(!!softwareVersionsData?.software ||
|
||||
query !== "" ||
|
||||
showVulnerableSoftware);
|
||||
|
||||
const softwareTableHeaders = useMemo(
|
||||
() =>
|
||||
generateSoftwareVersionsTableHeaders(
|
||||
router,
|
||||
isPremiumTier,
|
||||
isSandboxMode,
|
||||
teamId
|
||||
),
|
||||
[isPremiumTier, isSandboxMode, router, teamId]
|
||||
);
|
||||
|
||||
// TODO: figure out why this is not working
|
||||
const handleVulnFilterDropdownChange = (isFilterVulnerable: string) => {
|
||||
router.replace(
|
||||
getNextLocationPath({
|
||||
pathPrefix: PATHS.SOFTWARE_VERSIONS,
|
||||
routeTemplate: "",
|
||||
queryParams: {
|
||||
query,
|
||||
teamId,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
vulnerable: isFilterVulnerable,
|
||||
page: 0, // resets page index
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowSelect = (row: IRowProps) => {
|
||||
const hostsBySoftwareParams = {
|
||||
software_version_id: row.original.id,
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
const path = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(
|
||||
hostsBySoftwareParams
|
||||
)}`;
|
||||
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const determineQueryParamChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
|
||||
switch (key) {
|
||||
case "searchQuery":
|
||||
return val !== query;
|
||||
case "sortDirection":
|
||||
return val !== orderDirection;
|
||||
case "sortHeader":
|
||||
return val !== orderKey;
|
||||
case "vulnerable":
|
||||
return val !== showVulnerableSoftware.toString();
|
||||
case "pageIndex":
|
||||
return val !== currentPage;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return changedEntry?.[0] ?? "";
|
||||
},
|
||||
[currentPage, orderDirection, orderKey, query, showVulnerableSoftware]
|
||||
);
|
||||
|
||||
const generateNewQueryParams = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
return {
|
||||
query: newTableQuery.searchQuery,
|
||||
team_id: teamId,
|
||||
order_direction: newTableQuery.sortDirection,
|
||||
order_key: newTableQuery.sortHeader,
|
||||
vulnerable: showVulnerableSoftware.toString(),
|
||||
page: newTableQuery.pageIndex,
|
||||
};
|
||||
},
|
||||
[showVulnerableSoftware, teamId]
|
||||
);
|
||||
|
||||
// NOTE: this is called once on initial render and every time the query changes
|
||||
const onQueryChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
// we want to determine which query param has changed in order to
|
||||
// reset the page index to 0 if any other param has changed.
|
||||
const changedParam = determineQueryParamChange(newTableQuery);
|
||||
|
||||
// if nothing has changed, don't update the route. this can happen when
|
||||
// this handler is called on the inital render.
|
||||
if (changedParam === "") return;
|
||||
|
||||
const newRoute = getNextLocationPath({
|
||||
pathPrefix: PATHS.SOFTWARE_VERSIONS,
|
||||
routeTemplate: "",
|
||||
queryParams: generateNewQueryParams(newTableQuery),
|
||||
});
|
||||
|
||||
router.replace(newRoute);
|
||||
},
|
||||
[determineQueryParamChange, generateNewQueryParams, router]
|
||||
);
|
||||
|
||||
const getItemsCountText = () => {
|
||||
const count = softwareVersionsData?.count;
|
||||
if (!softwareVersionsData || !count) return "";
|
||||
|
||||
return count === 1 ? `${count} item` : `${count} items`;
|
||||
};
|
||||
|
||||
const getLastUpdatedText = () => {
|
||||
if (!softwareVersionsData || !softwareVersionsData.counts_updated_at)
|
||||
return "";
|
||||
return (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={softwareVersionsData.counts_updated_at}
|
||||
whatToRetrieve={"software"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSoftwareCount = () => {
|
||||
const itemText = getItemsCountText();
|
||||
const lastUpdatedText = getLastUpdatedText();
|
||||
|
||||
if (!itemText) return null;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__count`}>
|
||||
<span>{itemText}</span>
|
||||
{lastUpdatedText}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderVulnFilterDropdown = () => {
|
||||
return (
|
||||
<Dropdown
|
||||
value={showVulnerableSoftware}
|
||||
className={`${baseClass}__vuln_dropdown`}
|
||||
options={VULNERABLE_DROPDOWN_OPTIONS}
|
||||
searchable={false}
|
||||
onChange={handleVulnFilterDropdownChange}
|
||||
tableFilterDropdown
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTableFooter = () => {
|
||||
return (
|
||||
<div>
|
||||
Seeing unexpected software or vulnerabilities?{" "}
|
||||
<CustomLink
|
||||
url={GITHUB_NEW_ISSUE_LINK}
|
||||
text="File an issue on GitHub"
|
||||
newTab
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (isSoftwareVersionsError) {
|
||||
return <TableDataError className={`${baseClass}__table-error`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
columnConfigs={softwareTableHeaders}
|
||||
data={softwareVersionsData?.software || []}
|
||||
isLoading={isSoftwareVersionsLoading}
|
||||
resultsTitle={"items"}
|
||||
emptyComponent={() => (
|
||||
<EmptySoftwareTable
|
||||
isSoftwareDisabled={!isSoftwareEnabled}
|
||||
isFilterVulnerable={showVulnerableSoftware}
|
||||
isSandboxMode={isSandboxMode}
|
||||
isCollectingSoftware={false} // TODO: update with new API
|
||||
isSearching={query !== ""}
|
||||
noSandboxHosts={noSandboxHosts}
|
||||
/>
|
||||
)}
|
||||
defaultSortHeader={orderKey}
|
||||
defaultSortDirection={orderDirection}
|
||||
defaultPageIndex={currentPage}
|
||||
defaultSearchQuery={query}
|
||||
manualSortBy
|
||||
pageSize={perPage}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disableNextPage={!softwareVersionsData?.meta.has_next_results}
|
||||
searchable={searchable}
|
||||
inputPlaceHolder="Search by name or vulnerabilities (CVEs)"
|
||||
onQueryChange={onQueryChange}
|
||||
// additionalQueries serves as a trigger for the useDeepEffect hook
|
||||
// to fire onQueryChange for events happeing outside of
|
||||
// the TableContainer.
|
||||
additionalQueries={showVulnerableSoftware ? "vulnerable" : ""}
|
||||
customControl={searchable ? renderVulnFilterDropdown : undefined}
|
||||
stackControls
|
||||
renderCount={renderSoftwareCount}
|
||||
renderFooter={renderTableFooter}
|
||||
disableMultiRowSelect
|
||||
onSelectSingleRow={handleRowSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareVersions;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./SoftwareVersions";
|
||||
+10
@@ -26,6 +26,7 @@ import Slider from "components/forms/fields/Slider";
|
||||
import Radio from "components/forms/fields/Radio";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
|
||||
@@ -473,6 +474,15 @@ const ManageAutomationsModal = ({
|
||||
/>
|
||||
</div>
|
||||
{integrationEnabled ? renderTicket() : renderWebhook()}
|
||||
<p>
|
||||
Vulnerability automations currently run for software
|
||||
vulnerabilities. Interested in automations for OS vulnerabilities?{" "}
|
||||
<CustomLink
|
||||
url="https://www.fleetdm.com/support"
|
||||
text="Let us know"
|
||||
newTab
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<div
|
||||
|
||||
+12
-13
@@ -1,5 +1,9 @@
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import React from "react";
|
||||
|
||||
import { QueryParams } from "utilities/url";
|
||||
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
|
||||
import SoftwareIcon from "../icons/SoftwareIcon";
|
||||
|
||||
const baseClass = "software-details-summary";
|
||||
@@ -20,23 +24,21 @@ const DataSet = ({ title, value }: IDescriptionSetProps) => {
|
||||
};
|
||||
|
||||
interface ISoftwareDetailsSummaryProps {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
type?: string;
|
||||
hosts: number;
|
||||
/** The query param name that will be added when user clicks on "View all hosts" link */
|
||||
queryParam: string;
|
||||
/** The query param that will be added when user clicks on "View all hosts" link */
|
||||
queryParams: QueryParams;
|
||||
name?: string;
|
||||
source?: string;
|
||||
versions?: number;
|
||||
}
|
||||
|
||||
const SoftwareDetailsSummary = ({
|
||||
id,
|
||||
title,
|
||||
type,
|
||||
hosts,
|
||||
queryParam,
|
||||
queryParams,
|
||||
name,
|
||||
source,
|
||||
versions,
|
||||
@@ -47,18 +49,15 @@ const SoftwareDetailsSummary = ({
|
||||
<dl className={`${baseClass}__info`}>
|
||||
<h1>{title}</h1>
|
||||
<dl className={`${baseClass}__description-list`}>
|
||||
<DataSet
|
||||
title="Type"
|
||||
// value={formatSoftwareType(software.source)} TODO: format value
|
||||
value={type}
|
||||
/>
|
||||
{type && <DataSet title="Type" value={type} />}
|
||||
|
||||
{versions && <DataSet title="Versions" value={versions} />}
|
||||
<DataSet title="Hosts" value={hosts === 0 ? "---" : hosts} />
|
||||
</dl>
|
||||
</dl>
|
||||
<div>
|
||||
<ViewAllHostsLink
|
||||
queryParams={{ [queryParam]: id }}
|
||||
queryParams={queryParams}
|
||||
className={`${baseClass}__hosts-link`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { ISoftwareVulnerability } from "interfaces/software";
|
||||
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
|
||||
|
||||
import TableContainer from "components/TableContainer";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import generateTableConfig from "./SoftwareVulnerabilitiesTableConfig";
|
||||
|
||||
const baseClass = "software-vulnerabilities-table";
|
||||
|
||||
interface INoVulnsDetectedProps {
|
||||
itemName: string;
|
||||
}
|
||||
|
||||
const NoVulnsDetected = ({ itemName }: INoVulnsDetectedProps): JSX.Element => {
|
||||
return (
|
||||
<EmptyTable
|
||||
header={`No vulnerabilities detected for this ${itemName}`}
|
||||
info={
|
||||
<>
|
||||
Expecting to see vulnerabilities?{" "}
|
||||
<CustomLink
|
||||
url={GITHUB_NEW_ISSUE_LINK}
|
||||
text="File an issue on GitHub"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISoftwareVulnerabilitiesTableProps {
|
||||
data: ISoftwareVulnerability[];
|
||||
/** Name displayed on the empty state */
|
||||
itemName: string;
|
||||
isLoading: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SoftwareVulnerabilitiesTable = ({
|
||||
data,
|
||||
itemName,
|
||||
isLoading,
|
||||
className,
|
||||
}: ISoftwareVulnerabilitiesTableProps) => {
|
||||
const { isPremiumTier, isSandboxMode } = useContext(AppContext);
|
||||
|
||||
const classNames = classnames(baseClass, className);
|
||||
|
||||
const tableHeaders = useMemo(
|
||||
() => generateTableConfig(Boolean(isPremiumTier), Boolean(isSandboxMode)),
|
||||
[isPremiumTier, isSandboxMode]
|
||||
);
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<TableContainer
|
||||
columnConfigs={tableHeaders}
|
||||
data={data}
|
||||
defaultSortHeader={isPremiumTier ? "epss_probability" : "cve"}
|
||||
defaultSortDirection={"desc"}
|
||||
emptyComponent={() => <NoVulnsDetected itemName={itemName} />}
|
||||
isAllPagesSelected={false}
|
||||
isLoading={isLoading}
|
||||
isClientSidePagination
|
||||
pageSize={20}
|
||||
resultsTitle={"vulnerabilities"}
|
||||
showMarkAllPages={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareVulnerabilitiesTable;
|
||||
+3
-4
@@ -2,6 +2,7 @@ import React from "react";
|
||||
|
||||
import { formatFloatAsPercentage } from "utilities/helpers";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import { ISoftwareVulnerability } from "interfaces/software";
|
||||
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
@@ -9,7 +10,6 @@ import TooltipWrapper from "components/TooltipWrapper";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip";
|
||||
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
|
||||
import { ISoftwareVulnerability } from "interfaces/software";
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
@@ -62,7 +62,7 @@ const formatSeverity = (float: number | null) => {
|
||||
return `${severity} (${float.toFixed(1)})`;
|
||||
};
|
||||
|
||||
const generateSoftwareVersionDetailsTableConfig = (
|
||||
const generateTableConfig = (
|
||||
isPremiumTier: boolean,
|
||||
isSandboxMode: boolean
|
||||
): IDataColumn[] => {
|
||||
@@ -189,7 +189,6 @@ const generateSoftwareVersionDetailsTableConfig = (
|
||||
title: "Published",
|
||||
accessor: "cve_published",
|
||||
disableSortBy: false,
|
||||
sortType: "boolean",
|
||||
Header: (headerProps: IHeaderProps): JSX.Element => {
|
||||
const titleWithToolTip = (
|
||||
<TooltipWrapper
|
||||
@@ -228,4 +227,4 @@ const generateSoftwareVersionDetailsTableConfig = (
|
||||
return isPremiumTier ? tableHeaders.concat(premiumHeaders) : tableHeaders;
|
||||
};
|
||||
|
||||
export default generateSoftwareVersionDetailsTableConfig;
|
||||
export default generateTableConfig;
|
||||
@@ -0,0 +1,15 @@
|
||||
.software-vulnerabilities-table {
|
||||
|
||||
// keeps table data within the table container at smaller screen sizes
|
||||
.data-table {
|
||||
&__wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// used to position header text with premium icon correctly
|
||||
.column-header {
|
||||
display: flex;
|
||||
gap: $pad-small;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SoftwareVulnerabilitiesTable";
|
||||
@@ -9,6 +9,9 @@ import ReactTooltip from "react-tooltip";
|
||||
const baseClass = "version-cell";
|
||||
|
||||
const generateText = (versions: ISoftwareTitleVersion[]) => {
|
||||
if (!versions) {
|
||||
return <TextCell value="Unavailable" greyed />;
|
||||
}
|
||||
const text =
|
||||
versions.length !== 1 ? `${versions.length} versions` : versions[0].version;
|
||||
return <TextCell value={text} greyed={versions.length !== 1} />;
|
||||
@@ -18,7 +21,7 @@ const generateTooltip = (
|
||||
versions: ISoftwareTitleVersion[],
|
||||
tooltipId: string
|
||||
) => {
|
||||
if (versions.length <= 1) {
|
||||
if (!versions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -45,7 +48,7 @@ const VersionCell = ({ versions }: IVersionCellProps) => {
|
||||
|
||||
// only one version, no need for tooltip
|
||||
const cellText = generateText(versions);
|
||||
if (versions.length <= 1) {
|
||||
if (!versions) {
|
||||
return <>{cellText}</>;
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -162,9 +162,7 @@ const HostsFilterBlock = ({
|
||||
if (!osId && !(osName && osVersion)) return null;
|
||||
|
||||
let os: IOperatingSystemVersion | undefined;
|
||||
if (osId) {
|
||||
os = osVersions?.find((v) => v.os_id === osId);
|
||||
} else if (osName && osVersion) {
|
||||
if (osName && osVersion) {
|
||||
const name: string = osName;
|
||||
const vers: string = osVersion;
|
||||
|
||||
|
||||
@@ -60,9 +60,10 @@ import WindowsAutomaticEnrollmentPage from "pages/admin/IntegrationsPage/cards/A
|
||||
import HostQueryReport from "pages/hosts/details/HostQueryReport";
|
||||
import SoftwarePage from "pages/SoftwarePage";
|
||||
import SoftwareTitles from "pages/SoftwarePage/SoftwareTitles";
|
||||
import SoftwareVersions from "pages/SoftwarePage/SoftwareVersions";
|
||||
import SoftwareOS from "pages/SoftwarePage/SoftwareOS";
|
||||
import SoftwareTitleDetailsPage from "pages/SoftwarePage/SoftwareTitleDetailsPage";
|
||||
import SoftwareVersionDetailsPage from "pages/SoftwarePage/SoftwareVersionDetailsPage";
|
||||
import SoftwareOSDetailsPage from "pages/SoftwarePage/SoftwareOSDetailsPage";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
|
||||
@@ -217,12 +218,14 @@ const routes = (
|
||||
<IndexRedirect to="titles" />
|
||||
<Route component={SoftwarePage}>
|
||||
<Route path="titles" component={SoftwareTitles} />
|
||||
<Route path="versions" component={SoftwareVersions} />
|
||||
<Route path="versions" component={SoftwareTitles} />
|
||||
<Route path="os" component={SoftwareOS} />
|
||||
{/* This redirect keeps the old software/:id working */}
|
||||
<Redirect from=":id" to="versions/:id" />
|
||||
</Route>
|
||||
<Route path="titles/:id" component={SoftwareTitleDetailsPage} />
|
||||
<Route path="versions/:id" component={SoftwareVersionDetailsPage} />
|
||||
<Route path="os/details" component={SoftwareOSDetailsPage} />
|
||||
</Route>
|
||||
<Route component={AuthGlobalAdminMaintainerRoutes}>
|
||||
<Route path="packs">
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
// Software pages
|
||||
SOFTWARE: `${URL_PREFIX}/software`,
|
||||
SOFTWARE_TITLES: `${URL_PREFIX}/software/titles`,
|
||||
SOFTWARE_OS: `${URL_PREFIX}/software/os`,
|
||||
SOFTWARE_VERSIONS: `${URL_PREFIX}/software/versions`,
|
||||
SOFTWARE_TITLE_DETAILS: (id: string): string => {
|
||||
return `${URL_PREFIX}/software/titles/${id}`;
|
||||
@@ -54,6 +55,9 @@ export default {
|
||||
SOFTWARE_VERSION_DETAILS: (id: string): string => {
|
||||
return `${URL_PREFIX}/software/versions/${id}`;
|
||||
},
|
||||
SOFTWARE_OS_DETAILS: (name: string, version: string): string => {
|
||||
return `${URL_PREFIX}/software/os/details?name=${name}&version=${version}`;
|
||||
},
|
||||
|
||||
EDIT_PACK: (packId: number): string => {
|
||||
return `${URL_PREFIX}/packs/${packId}/edit`;
|
||||
|
||||
@@ -12,37 +12,82 @@ export const OS_VERSIONS_API_SUPPORTED_PLATFORMS = [
|
||||
"chrome",
|
||||
];
|
||||
|
||||
export interface IGetOSVersionsRequest {
|
||||
id?: number;
|
||||
export interface IGetOSVersionsQueryParams {
|
||||
platform?: OsqueryPlatform;
|
||||
teamId?: number;
|
||||
os_name?: string;
|
||||
os_version?: string;
|
||||
order_key?: string;
|
||||
order_direction?: string;
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}
|
||||
|
||||
export interface IGetOSVersionsQueryKey extends IGetOSVersionsRequest {
|
||||
export interface IGetOSVersionsDetailsQueryParams {
|
||||
os_name?: string;
|
||||
os_version?: string;
|
||||
}
|
||||
|
||||
export interface IGetOSVersionsQueryKey extends IGetOSVersionsQueryParams {
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface IOSVersionsResponse {
|
||||
count: number;
|
||||
counts_updated_at: string;
|
||||
os_versions: IOperatingSystemVersion[];
|
||||
meta: {
|
||||
has_next_results: boolean;
|
||||
has_previous_results: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const getOSVersions = ({
|
||||
id,
|
||||
platform,
|
||||
teamId,
|
||||
}: IGetOSVersionsRequest = {}): Promise<IOSVersionsResponse> => {
|
||||
os_name,
|
||||
os_version,
|
||||
order_key,
|
||||
order_direction,
|
||||
page,
|
||||
per_page,
|
||||
}: IGetOSVersionsQueryParams = {}): Promise<IOSVersionsResponse> => {
|
||||
const { OS_VERSIONS } = endpoints;
|
||||
let path = OS_VERSIONS;
|
||||
|
||||
const queryParams = { id, platform, team_id: teamId };
|
||||
const queryString = buildQueryStringFromParams(queryParams);
|
||||
const queryString = buildQueryStringFromParams({
|
||||
platform,
|
||||
team_id: teamId,
|
||||
os_name,
|
||||
os_version,
|
||||
order_key,
|
||||
order_direction,
|
||||
page,
|
||||
per_page,
|
||||
});
|
||||
|
||||
if (queryString) path += `?${queryString}`;
|
||||
|
||||
return sendRequest("GET", path);
|
||||
};
|
||||
|
||||
const getOSVersion = ({
|
||||
os_name,
|
||||
os_version,
|
||||
}: IGetOSVersionsDetailsQueryParams): Promise<IOSVersionsResponse> => {
|
||||
const { OS_VERSIONS } = endpoints;
|
||||
let path = OS_VERSIONS;
|
||||
|
||||
const queryString = buildQueryStringFromParams({
|
||||
os_name,
|
||||
os_version,
|
||||
});
|
||||
|
||||
if (queryString) path += `?${queryString}`;
|
||||
return sendRequest("GET", path);
|
||||
};
|
||||
|
||||
export default {
|
||||
getOSVersions,
|
||||
getOSVersion,
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ export const FREQUENCY_DROPDOWN_OPTIONS = [
|
||||
|
||||
export const GITHUB_NEW_ISSUE_LINK =
|
||||
"https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md";
|
||||
export const SUPPORT_LINK = "https://fleetdm.com/support";
|
||||
|
||||
/** July 28, 2016 is the date of the initial commit to fleet/fleet. */
|
||||
export const INITIAL_FLEET_DATE = "2016-07-28T00:00:00Z";
|
||||
|
||||
@@ -81,7 +81,11 @@ export default {
|
||||
`/${API_VERSION}/fleet/mdm/hosts/${id}/encryption_key`,
|
||||
|
||||
ME: `/${API_VERSION}/fleet/me`,
|
||||
|
||||
// OS Version endpoints
|
||||
OS_VERSIONS: `/${API_VERSION}/fleet/os_versions`,
|
||||
OS_VERSION: (id: number) => `/${API_VERSION}/fleet/os_versions/${id}`,
|
||||
|
||||
OSQUERY_OPTIONS: `/${API_VERSION}/fleet/spec/osquery_options`,
|
||||
PACKS: `/${API_VERSION}/fleet/packs`,
|
||||
PERFORM_REQUIRED_PASSWORD_RESET: `/${API_VERSION}/fleet/perform_required_password_reset`,
|
||||
|
||||
Reference in New Issue
Block a user