Check team config for software UI (#8338)

This commit is contained in:
gillespi314 2022-10-19 14:00:39 -05:00 committed by GitHub
parent 6d4c885f22
commit 9f20f01e37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 190 additions and 149 deletions

View File

@ -13,7 +13,7 @@ interface ITeamsDropdownHeaderProps {
router: InjectedRouter;
location: {
pathname: string;
query: { team_id?: string; vulnerable?: boolean };
query: { team_id?: string; vulnerable?: string };
search: string;
};
baseClass: string;

View File

@ -113,6 +113,11 @@ export interface IConfigFormData {
transparency_url: string;
}
export interface IConfigFeatures {
enable_host_users: boolean;
enable_software_inventory: boolean;
}
export interface IConfig {
org_info: {
org_name: string;
@ -154,10 +159,7 @@ export interface IConfig {
host_expiry_enabled: boolean;
host_expiry_window: number;
};
features: {
enable_host_users: boolean;
enable_software_inventory: boolean;
};
features: IConfigFeatures;
agent_options: string;
update_interval: {
osquery_detail: number;
@ -220,3 +222,5 @@ export type IAutomationsConfig = Pick<
IConfig,
"webhook_settings" | "integrations"
>;
export const CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS = 30;

View File

@ -1,7 +1,7 @@
import PropTypes from "prop-types";
import { IConfigFeatures, IWebhookSettings } from "./config";
import enrollSecretInterface, { IEnrollSecret } from "./enroll_secret";
import { IIntegrations } from "./integration";
import { IWebhookFailingPolicies } from "./webhook";
export default PropTypes.shape({
id: PropTypes.number.isRequired,
@ -33,6 +33,7 @@ export interface ITeam extends ITeamSummary {
display_text?: string;
count?: number;
created_at?: string;
features?: IConfigFeatures;
agent_options?: {
[key: string]: any;
};
@ -42,13 +43,19 @@ export interface ITeam extends ITeamSummary {
role?: string; // role value is included when the team is in the context of a user
}
/**
* The webhook settings of a team
*/
export type ITeamWebhookSettings = Pick<
IWebhookSettings,
"vulnerabilities_webhook" | "failing_policies_webhook"
>;
/**
* The integrations and webhook settings of a team
*/
export interface ITeamAutomationsConfig {
webhook_settings: {
failing_policies_webhook: IWebhookFailingPolicies;
};
webhook_settings: ITeamWebhookSettings;
integrations: IIntegrations;
}

View File

@ -196,7 +196,11 @@ const Homepage = (): JSX.Element => {
}
);
const isSoftwareEnabled = config?.features?.enable_software_inventory;
const featuresConfig = currentTeam?.id
? teams?.find((t) => t.id === currentTeam.id)?.features
: config?.features;
const isSoftwareEnabled = !!featuresConfig?.enable_software_inventory;
const SOFTWARE_DEFAULT_SORT_DIRECTION = "desc";
const SOFTWARE_DEFAULT_SORT_HEADER = "hosts_count";
const SOFTWARE_DEFAULT_PAGE_SIZE = 8;

View File

@ -61,7 +61,7 @@ const Software = ({
) : (
<TableContainer
columns={tableHeaders}
data={software?.software || []}
data={(isSoftwareEnabled && software?.software) || []}
isLoading={isSoftwareFetching}
defaultSortHeader={"hosts_count"}
defaultSortDirection={SOFTWARE_DEFAULT_SORT_DIRECTION}
@ -89,7 +89,7 @@ const Software = ({
) : (
<TableContainer
columns={tableHeaders}
data={software?.software || []}
data={(isSoftwareEnabled && software?.software) || []}
isLoading={isSoftwareFetching}
defaultSortHeader={SOFTWARE_DEFAULT_SORT_HEADER}
defaultSortDirection={SOFTWARE_DEFAULT_SORT_DIRECTION}

View File

@ -9,14 +9,12 @@ import classnames from "classnames";
import { pick } from "lodash";
import PATHS from "router/paths";
import configAPI from "services/entities/config";
import hostAPI from "services/entities/hosts";
import queryAPI from "services/entities/queries";
import teamAPI, { ILoadTeamsResponse } from "services/entities/teams";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { NotificationContext } from "context/notification";
import { IConfig } from "interfaces/config";
import {
IHost,
IDeviceMappingResponse,
@ -97,11 +95,12 @@ const HostDetailsPage = ({
}: IHostDetailsProps): JSX.Element => {
const hostIdFromURL = parseInt(host_id, 10);
const {
config,
currentUser,
isGlobalAdmin,
isPremiumTier,
isOnlyObserver,
isGlobalMaintainer,
currentUser,
} = useContext(AppContext);
const {
setLastEditedQueryName,
@ -197,14 +196,6 @@ const HostDetailsPage = ({
}
);
const { data: features } = useQuery<
IConfig,
Error,
{ enable_host_users: boolean; enable_software_inventory: boolean }
>(["config"], () => configAPI.loadAll(), {
select: (data: IConfig) => data.features,
});
const refetchExtensions = () => {
deviceMapping !== null && refetchDeviceMapping();
macadmins !== null && refetchMacadmins();
@ -298,6 +289,10 @@ const HostDetailsPage = ({
}
);
const featuresConfig = host?.team_id
? teams?.find((t) => t.id === host.team_id)?.features
: config?.features;
useEffect(() => {
setUsersState(() => {
return (
@ -603,14 +598,16 @@ const HostDetailsPage = ({
usersState={usersState}
isLoading={isLoadingHost}
onUsersTableSearchChange={onUsersTableSearchChange}
hostUsersEnabled={features?.enable_host_users}
hostUsersEnabled={featuresConfig?.enable_host_users}
/>
</TabPanel>
<TabPanel>
<SoftwareCard
isLoading={isLoadingHost}
software={hostSoftware}
softwareInventoryEnabled={features?.enable_software_inventory}
softwareInventoryEnabled={
featuresConfig?.enable_software_inventory
}
deviceType={host?.platform === "darwin" ? "macos" : ""}
/>
{macadmins && (

View File

@ -8,9 +8,10 @@ import { PolicyContext } from "context/policy";
import { TableContext } from "context/table";
import { NotificationContext } from "context/notification";
import { IAutomationsConfig, IConfig } from "interfaces/config";
import { IConfig, IWebhookSettings } from "interfaces/config";
import { IIntegrations } from "interfaces/integration";
import { IPolicyStats, ILoadAllPoliciesResponse } from "interfaces/policy";
import { ITeamAutomationsConfig, ITeamConfig } from "interfaces/team";
import { ITeamConfig } from "interfaces/team";
import PATHS from "router/paths";
import configAPI from "services/entities/config";
@ -200,9 +201,10 @@ const ManagePolicyPage = ({
const toggleShowInheritedPolicies = () =>
setShowInheritedPolicies(!showInheritedPolicies);
const handleUpdateAutomations = async (
requestBody: IAutomationsConfig | ITeamAutomationsConfig
) => {
const handleUpdateAutomations = async (requestBody: {
webhook_settings: Pick<IWebhookSettings, "failing_policies_webhook">;
integrations: IIntegrations;
}) => {
setIsUpdatingAutomations(true);
try {
await (teamId

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { Link } from "react-router";
import { isEmpty, noop, omit } from "lodash";
import { IAutomationsConfig } from "interfaces/config";
import { IAutomationsConfig, IWebhookSettings } from "interfaces/config";
import { IIntegration, IIntegrations } from "interfaces/integration";
import { IPolicy } from "interfaces/policy";
import { ITeamAutomationsConfig } from "interfaces/team";
@ -29,7 +29,10 @@ interface IManageAutomationsModalProps {
isUpdatingAutomations: boolean;
showPreviewPayloadModal: boolean;
onExit: () => void;
handleSubmit: (formData: IAutomationsConfig | ITeamAutomationsConfig) => void;
handleSubmit: (formData: {
webhook_settings: Pick<IWebhookSettings, "failing_policies_webhook">;
integrations: IIntegrations;
}) => void;
togglePreviewPayloadModal: () => void;
}

View File

@ -11,18 +11,23 @@ import { useDebouncedCallback } from "use-debounce";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import { IConfig } from "interfaces/config";
import {
IConfig,
CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS,
} from "interfaces/config";
import {
IJiraIntegration,
IZendeskIntegration,
IIntegration,
IIntegrations,
} from "interfaces/integration";
import { ITeamConfig } from "interfaces/team";
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; // @ts-ignore
import configAPI from "services/entities/config";
import softwareAPI, {
ISoftwareResponse,
ISoftwareCountResponse,
} from "services/entities/software";
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
import {
GITHUB_NEW_ISSUE_LINK,
VULNERABLE_DROPDOWN_OPTIONS,
@ -50,7 +55,7 @@ interface IManageSoftwarePageProps {
router: InjectedRouter;
location: {
pathname: string;
query: { vulnerable?: boolean };
query: { vulnerable?: string };
search: string;
};
}
@ -66,6 +71,11 @@ interface ISoftwareQueryKey {
teamId?: number;
}
interface ISoftwareConfigQueryKey {
scope: string;
teamId?: number;
}
interface ISoftwareAutomations {
webhook_settings: {
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
@ -89,9 +99,8 @@ const ManageSoftwarePage = ({
}: IManageSoftwarePageProps): JSX.Element => {
const {
availableTeams,
config: globalConfig,
currentTeam,
isGlobalAdmin,
isGlobalMaintainer,
isOnGlobalTeam,
isPremiumTier,
} = useContext(AppContext);
@ -99,17 +108,10 @@ const ManageSoftwarePage = ({
const DEFAULT_SORT_HEADER = isPremiumTier ? "vulnerabilities" : "hosts_count";
const [isSoftwareEnabled, setIsSoftwareEnabled] = useState(false);
const [
isVulnerabilityAutomationsEnabled,
setIsVulnerabilityAutomationsEnabled,
] = useState(false);
const [
recentVulnerabilityMaxAge,
setRecentVulnerabilityMaxAge,
] = useState<number>();
// TODO: refactor usage of vulnerable query param in accordance with new patterns for query params
// and management of URL state
const [filterVuln, setFilterVuln] = useState(
location?.query?.vulnerable || false
location?.query?.vulnerable === "true" || false
);
const [searchQuery, setSearchQuery] = useState("");
const [sortDirection, setSortDirection] = useState<
@ -123,44 +125,68 @@ const ManageSoftwarePage = ({
const [showPreviewPayloadModal, setShowPreviewPayloadModal] = useState(false);
const [showPreviewTicketModal, setShowPreviewTicketModal] = useState(false);
// TODO: experiment to see if we need this state and effect or can we rely solely on the router/location for the dropdown state?
useEffect(() => {
setFilterVuln(!!location.query.vulnerable);
setFilterVuln(location?.query?.vulnerable === "true" || false);
// TODO: handle invalid values for vulnerable param
}, [location]);
const { data: config } = useQuery(["config"], configAPI.loadAll, {
onSuccess: (data) => {
setIsSoftwareEnabled(data?.features?.enable_software_inventory);
let jiraIntegrationEnabled = false;
if (data.integrations.jira) {
jiraIntegrationEnabled = data?.integrations.jira.some(
(integration: IIntegration) => {
return integration.enable_software_vulnerabilities;
}
);
}
let zendeskIntegrationEnabled = false;
if (data.integrations.zendesk) {
zendeskIntegrationEnabled = data?.integrations.zendesk.some(
(integration: IIntegration) => {
return integration.enable_software_vulnerabilities;
}
);
}
setIsVulnerabilityAutomationsEnabled(
data?.webhook_settings?.vulnerabilities_webhook
.enable_vulnerabilities_webhook ||
jiraIntegrationEnabled ||
zendeskIntegrationEnabled
);
// Convert from nanosecond to nearest day
setRecentVulnerabilityMaxAge(
Math.round(
data?.vulnerabilities?.recent_vulnerability_max_age / 86400000000000
)
);
// softwareConfig is either the global config or the team config of the currently selected team
const {
data: softwareConfig,
error: softwareConfigError,
isFetching: isFetchingSoftwareConfig,
refetch: refetchSoftwareConfig,
} = useQuery<
IConfig | ILoadTeamResponse,
Error,
IConfig | ITeamConfig,
ISoftwareConfigQueryKey[]
>(
[{ scope: "softwareConfig", teamId: currentTeam?.id }],
({ queryKey }) => {
const { teamId } = queryKey[0];
return teamId ? teamsAPI.load(teamId) : configAPI.loadAll();
},
});
{
select: (data) => ("team" in data ? data.team : data),
}
);
const isSoftwareConfigLoaded =
!isFetchingSoftwareConfig && !softwareConfigError && !!softwareConfig;
const isSoftwareEnabled = !!softwareConfig?.features
?.enable_software_inventory;
const vulnWebhookSettings =
softwareConfig?.webhook_settings?.vulnerabilities_webhook;
const isVulnWebhookEnabled = !!vulnWebhookSettings?.enable_vulnerabilities_webhook;
const isVulnIntegrationEnabled = (integrations?: IIntegrations) => {
return (
!!integrations?.jira?.some((j) => j.enable_software_vulnerabilities) ||
!!integrations?.zendesk?.some((z) => z.enable_software_vulnerabilities)
);
};
const isAnyVulnAutomationEnabled =
isVulnWebhookEnabled ||
isVulnIntegrationEnabled(softwareConfig?.integrations);
const recentVulnerabilityMaxAge = (() => {
let maxAgeInNanoseconds: number | undefined;
if (softwareConfig && "vulnerabilities" in softwareConfig) {
maxAgeInNanoseconds =
softwareConfig.vulnerabilities.recent_vulnerability_max_age;
} else {
maxAgeInNanoseconds =
globalConfig?.vulnerabilities.recent_vulnerability_max_age;
}
return maxAgeInNanoseconds
? Math.round(maxAgeInNanoseconds / 86400000000000) // convert from nanoseconds to days
: CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS;
})();
const {
data: software,
@ -191,8 +217,9 @@ const ManageSoftwarePage = ({
({ queryKey }) => softwareAPI.load(queryKey[0]),
{
enabled:
isOnGlobalTeam ||
!!availableTeams?.find((t) => t.id === currentTeam?.id),
isSoftwareConfigLoaded &&
(isOnGlobalTeam ||
!!availableTeams?.find((t) => t.id === currentTeam?.id)),
keepPreviousData: true,
staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
}
@ -221,8 +248,9 @@ const ManageSoftwarePage = ({
},
{
enabled:
isOnGlobalTeam ||
!!availableTeams?.find((t) => t.id === currentTeam?.id),
isSoftwareConfigLoaded &&
(isOnGlobalTeam ||
!!availableTeams?.find((t) => t.id === currentTeam?.id)),
keepPreviousData: true,
staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
refetchOnWindowFocus: false,
@ -231,21 +259,6 @@ const ManageSoftwarePage = ({
}
);
const canAddOrRemoveSoftwareWebhook = isGlobalAdmin || isGlobalMaintainer;
const {
data: softwareVulnerabilitiesWebhook,
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
refetch: refetchSoftwareVulnerabilitiesWebhook,
} = useQuery<IConfig, Error, IWebhookSoftwareVulnerabilities>(
["config"],
() => configAPI.loadAll(),
{
enabled: canAddOrRemoveSoftwareWebhook,
select: (data: IConfig) => data.webhook_settings.vulnerabilities_webhook,
}
);
const onQueryChange = useDebouncedCallback(
async ({
pageIndex: newPageIndex,
@ -270,8 +283,9 @@ const ManageSoftwarePage = ({
300
);
const toggleManageAutomationsModal = () =>
const toggleManageAutomationsModal = useCallback(() => {
setShowManageAutomationsModal(!showManageAutomationsModal);
}, [setShowManageAutomationsModal, showManageAutomationsModal]);
const togglePreviewPayloadModal = useCallback(() => {
setShowPreviewPayloadModal(!showPreviewPayloadModal);
@ -281,10 +295,6 @@ const ManageSoftwarePage = ({
setShowPreviewTicketModal(!showPreviewTicketModal);
}, [setShowPreviewTicketModal, showPreviewTicketModal]);
const onManageAutomationsClick = () => {
toggleManageAutomationsModal();
};
const onCreateWebhookSubmit = async (
configSoftwareAutomations: ISoftwareAutomations
) => {
@ -295,7 +305,7 @@ const ManageSoftwarePage = ({
"success",
"Successfully updated vulnerability automations."
);
refetchSoftwareVulnerabilitiesWebhook();
refetchSoftwareConfig();
});
} catch {
renderFlash(
@ -311,28 +321,34 @@ const ManageSoftwarePage = ({
setPageIndex(0);
};
const renderHeaderButtons = (
state: IHeaderButtonsState
): JSX.Element | null => {
if (
!softwareError &&
state.isGlobalAdmin &&
(!state.isPremiumTier || state.teamId === 0) &&
!state.isLoading
) {
return (
<Button
onClick={onManageAutomationsClick}
className={`${baseClass}__manage-automations button`}
variant="brand"
>
<span>Manage automations</span>
</Button>
);
}
return null;
};
// TODO: refactor/replace team dropdown header component in accordance with new patterns
const renderHeaderButtons = useCallback(
(state: IHeaderButtonsState): JSX.Element | null => {
const {
teamId,
isLoading,
isGlobalAdmin,
isPremiumTier: isPremium,
} = state;
const canManageAutomations =
isGlobalAdmin && (!isPremium || teamId === 0);
if (canManageAutomations && !softwareError && !isLoading) {
return (
<Button
onClick={toggleManageAutomationsModal}
className={`${baseClass}__manage-automations button`}
variant="brand"
>
<span>Manage automations</span>
</Button>
);
}
return null;
},
[softwareError, toggleManageAutomationsModal]
);
// TODO: refactor/replace team dropdown header component in accordance with new patterns
const renderHeaderDescription = (state: ITeamsDropdownState) => {
return (
<p>
@ -351,6 +367,7 @@ const ManageSoftwarePage = ({
);
};
// TODO: refactor/replace team dropdown header component in accordance with new patterns
const renderHeader = useCallback(() => {
return (
<TeamsDropdownHeader
@ -363,12 +380,12 @@ const ManageSoftwarePage = ({
buttons={(state) =>
renderHeaderButtons({
...state,
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
isLoading: !isSoftwareConfigLoaded,
})
}
/>
);
}, [router, location, isLoadingSoftwareVulnerabilitiesWebhook]);
}, [router, location, isSoftwareConfigLoaded, renderHeaderButtons]);
const renderSoftwareCount = useCallback(() => {
const count = softwareCount;
@ -386,7 +403,6 @@ const ManageSoftwarePage = ({
);
}
// TODO: Use setInterval to keep last updated time current?
if (count) {
return (
<div
@ -412,7 +428,7 @@ const ManageSoftwarePage = ({
isSoftwareEnabled,
]);
// TODO: retool this with react-router location descriptor objects
// TODO: refactor in accordance with new patterns for query params and management of URL state
const buildUrlQueryString = (queryString: string, vulnerable: boolean) => {
queryString = queryString.startsWith("?")
? queryString.slice(1)
@ -438,6 +454,7 @@ const ManageSoftwarePage = ({
return queryString;
};
// TODO: refactor in accordance with new patterns for query params and management of URL state
const onVulnFilterChange = useCallback(
(vulnerable: boolean) => {
setFilterVuln(vulnerable);
@ -499,14 +516,17 @@ const ManageSoftwarePage = ({
[isPremiumTier]
);
return !availableTeams || !config ? (
return !availableTeams ||
!globalConfig ||
(!softwareConfig && !softwareConfigError) ? (
<Spinner />
) : (
<MainContent>
<div className={`${baseClass}__wrapper`}>
{renderHeader()}
<div className={`${baseClass}__table`}>
{softwareError && !isFetchingSoftware ? (
{(softwareError && !isFetchingSoftware) ||
(softwareConfigError && !isFetchingSoftwareConfig) ? (
<TableDataError />
) : (
<TableContainer
@ -552,18 +572,9 @@ const ManageSoftwarePage = ({
togglePreviewTicketModal={togglePreviewTicketModal}
showPreviewPayloadModal={showPreviewPayloadModal}
showPreviewTicketModal={showPreviewTicketModal}
softwareVulnerabilityAutomationEnabled={
isVulnerabilityAutomationsEnabled
}
softwareVulnerabilityWebhookEnabled={
softwareVulnerabilitiesWebhook &&
softwareVulnerabilitiesWebhook.enable_vulnerabilities_webhook
}
currentDestinationUrl={
(softwareVulnerabilitiesWebhook &&
softwareVulnerabilitiesWebhook.destination_url) ||
""
}
softwareVulnerabilityAutomationEnabled={isAnyVulnAutomationEnabled}
softwareVulnerabilityWebhookEnabled={isVulnWebhookEnabled}
currentDestinationUrl={vulnWebhookSettings?.destination_url || ""}
recentVulnerabilityMaxAge={recentVulnerabilityMaxAge}
/>
)}

View File

@ -11,7 +11,10 @@ import {
IIntegrations,
IIntegrationType,
} from "interfaces/integration";
import { IConfig } from "interfaces/config";
import {
IConfig,
CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS,
} from "interfaces/config";
import configAPI from "services/entities/config";
import ReactTooltip from "react-tooltip";
@ -26,7 +29,7 @@ import InputField from "components/forms/fields/InputField";
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
import useDeepEffect from "hooks/useDeepEffect";
import _, { size } from "lodash";
import { size } from "lodash";
import PreviewPayloadModal from "../PreviewPayloadModal";
import PreviewTicketModal from "../PreviewTicketModal";
@ -327,7 +330,9 @@ const ManageAutomationsModal = ({
<p>
A ticket will be created in your <b>Integration</b> if a detected
vulnerability (CVE) was published in the last{" "}
{recentVulnerabilityMaxAge || "30"} days.
{recentVulnerabilityMaxAge ||
CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS}{" "}
days.
</p>
</div>
{(jiraIntegrationsIndexed && jiraIntegrationsIndexed.length > 0) ||

View File

@ -5,10 +5,12 @@ import { pick } from "lodash";
import { buildQueryStringFromParams } from "utilities/url";
import { IEnrollSecret } from "interfaces/enroll_secret";
import { IIntegrations } from "interfaces/integration";
import {
INewMembersBody,
IRemoveMembersBody,
ITeamConfig,
ITeamWebhookSettings,
} from "interfaces/team";
interface ILoadTeamsParams {
@ -33,6 +35,12 @@ export interface ITeamFormData {
name: string;
}
export interface IUpdateTeamFormData {
name: string;
webhook_settings: Partial<ITeamWebhookSettings>;
integrations: IIntegrations;
}
export default {
create: (formData: ITeamFormData) => {
const { TEAMS } = endpoints;
@ -65,7 +73,7 @@ export default {
return sendRequest("GET", path);
},
update: (
{ name, webhook_settings, integrations }: Partial<ITeamConfig>,
{ name, webhook_settings, integrations }: Partial<IUpdateTeamFormData>,
teamId?: number
): Promise<ITeamConfig> => {
if (typeof teamId === "undefined") {