mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
333674b051
## Addresses second major part of #15011 (item 3) – Host query report _Note for reviewers: The most important files here are:_ - HostQueryReport.tsx - HQRTable.tsx - HQRTableConfig.tsx _The rest are associated API services, interfaces, helpers, routes, styles, and miscellanious code improvements I made along the way._ ____________ ### See linked issue for enumeration of feature-related tasks <img width="1230" alt="Screenshot 2023-12-08 at 4 23 50 PM" src="https://github.com/fleetdm/fleet/assets/61553566/4ae4b41b-9209-4afa-ae50-8844d01ff8fd"> <img width="1230" alt="collecting" src="https://github.com/fleetdm/fleet/assets/61553566/061ac2bc-899f-4b29-91ba-36ebecf5ce58"> <img width="1230" alt="Screenshot 2023-12-08 at 4 24 39 PM" src="https://github.com/fleetdm/fleet/assets/61553566/f8b25e01-fe3b-47e6-b980-eba9538b1a01"> <img width="1230" alt="Screenshot 2023-12-08 at 4 25 01 PM" src="https://github.com/fleetdm/fleet/assets/61553566/46360274-8500-494c-8fb7-3a1d45347ce0"> Re-routes to host details > queries if: - query reports are globally disabled: https://github.com/fleetdm/fleet/assets/61553566/ac67da8c-57bc-4d9b-96be-daf3b198e704 - query has `Discard data` enabled: https://github.com/fleetdm/fleet/assets/61553566/b797dd24-9893-4360-bf40-b80298848864 - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
import React, { useState, useEffect, useContext } from "react";
|
|
import { useQuery, useMutation } from "react-query";
|
|
import { InjectedRouter, Params } from "react-router/lib/Router";
|
|
import { useErrorHandler } from "react-error-boundary";
|
|
|
|
import { AppContext } from "context/app";
|
|
import { PolicyContext } from "context/policy";
|
|
import useTeamIdParam from "hooks/useTeamIdParam";
|
|
import { IHost, IHostResponse } from "interfaces/host";
|
|
import { ILabel } from "interfaces/label";
|
|
import {
|
|
IPolicyFormData,
|
|
IPolicy,
|
|
IStoredPolicyResponse,
|
|
} from "interfaces/policy";
|
|
import { ITarget } from "interfaces/target";
|
|
import { ITeam } from "interfaces/team";
|
|
import globalPoliciesAPI from "services/entities/global_policies";
|
|
import teamPoliciesAPI from "services/entities/team_policies";
|
|
import hostAPI from "services/entities/hosts";
|
|
import statusAPI from "services/entities/status";
|
|
import { DOCUMENT_TITLE_SUFFIX, LIVE_POLICY_STEPS } from "utilities/constants";
|
|
|
|
import QuerySidePanel from "components/side_panels/QuerySidePanel";
|
|
import QueryEditor from "pages/policies/PolicyPage/screens/QueryEditor";
|
|
import SelectTargets from "components/LiveQuery/SelectTargets";
|
|
import MainContent from "components/MainContent";
|
|
import SidePanelContent from "components/SidePanelContent";
|
|
import Spinner from "components/Spinner/Spinner";
|
|
import CustomLink from "components/CustomLink";
|
|
import RunQuery from "pages/policies/PolicyPage/screens/RunQuery";
|
|
import { DEFAULT_POLICY } from "pages/policies/constants";
|
|
|
|
interface IPolicyPageProps {
|
|
router: InjectedRouter;
|
|
params: Params;
|
|
location: {
|
|
pathname: string;
|
|
search: string;
|
|
query: { host_ids: string; team_id: string };
|
|
hash?: string;
|
|
};
|
|
}
|
|
|
|
const baseClass = "policy-page";
|
|
|
|
const PolicyPage = ({
|
|
router,
|
|
params: { id: paramsPolicyId },
|
|
location,
|
|
}: IPolicyPageProps): JSX.Element => {
|
|
const policyId = paramsPolicyId ? parseInt(paramsPolicyId, 10) : null; // TODO(sarah): What should happen if this doesn't parse (e.g. the string is "foo")?
|
|
const handlePageError = useErrorHandler();
|
|
const {
|
|
isGlobalAdmin,
|
|
isGlobalMaintainer,
|
|
isAnyTeamMaintainerOrTeamAdmin,
|
|
config,
|
|
} = useContext(AppContext);
|
|
const {
|
|
lastEditedQueryBody,
|
|
policyTeamId,
|
|
selectedOsqueryTable,
|
|
setSelectedOsqueryTable,
|
|
setLastEditedQueryId,
|
|
setLastEditedQueryName,
|
|
setLastEditedQueryDescription,
|
|
setLastEditedQueryBody,
|
|
setLastEditedQueryResolution,
|
|
setLastEditedQueryCritical,
|
|
setLastEditedQueryPlatform,
|
|
setPolicyTeamId,
|
|
} = useContext(PolicyContext);
|
|
|
|
const {
|
|
isRouteOk,
|
|
isTeamAdmin,
|
|
isTeamMaintainer,
|
|
isTeamObserver,
|
|
teamIdForApi,
|
|
isObserverPlus,
|
|
} = useTeamIdParam({
|
|
location,
|
|
router,
|
|
includeAllTeams: true,
|
|
includeNoTeam: false,
|
|
permittedAccessByTeamRole: {
|
|
admin: true,
|
|
maintainer: true,
|
|
observer: true,
|
|
observer_plus: true,
|
|
},
|
|
});
|
|
|
|
// // TODO(Sarah): What should happen if a user without save permissions tries to directly navigate
|
|
// // to the new policy page? Should we redirect to the manage policies page?
|
|
// const hasSavePermissions =
|
|
// isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
|
|
//
|
|
// useEffect(() => {
|
|
// if (!isRouteOk) {
|
|
// return;
|
|
// }
|
|
// if (trimEnd(location.pathname, "/").endsWith("/new")) {
|
|
// !hasSavePermissions && router.push(paths.MANAGE_POLICIES);
|
|
// }
|
|
// }, [hasSavePermissions, isRouteOk, location.pathname, router]);
|
|
|
|
useEffect(() => {
|
|
if (!isRouteOk) {
|
|
return;
|
|
}
|
|
if (policyTeamId !== teamIdForApi) {
|
|
setPolicyTeamId(teamIdForApi || 0);
|
|
}
|
|
}, [isRouteOk, teamIdForApi, policyTeamId, setPolicyTeamId]);
|
|
|
|
useEffect(() => {
|
|
if (lastEditedQueryBody === "") {
|
|
setLastEditedQueryBody(DEFAULT_POLICY.query);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
// cleanup when component unmounts
|
|
return () => {
|
|
setLastEditedQueryCritical(false);
|
|
setLastEditedQueryPlatform(null);
|
|
};
|
|
}, []);
|
|
|
|
const [step, setStep] = useState(LIVE_POLICY_STEPS[1]);
|
|
const [selectedTargets, setSelectedTargets] = useState<ITarget[]>([]);
|
|
const [targetedHosts, setTargetedHosts] = useState<IHost[]>([]);
|
|
const [targetedLabels, setTargetedLabels] = useState<ILabel[]>([]);
|
|
const [targetedTeams, setTargetedTeams] = useState<ITeam[]>([]);
|
|
const [targetsTotalCount, setTargetsTotalCount] = useState(0);
|
|
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
|
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
|
|
false
|
|
);
|
|
|
|
const {
|
|
isLoading: isStoredPolicyLoading,
|
|
data: storedPolicy,
|
|
error: storedPolicyError,
|
|
} = useQuery<IStoredPolicyResponse, Error, IPolicy>(
|
|
["policy", policyId],
|
|
() => globalPoliciesAPI.load(policyId as number), // Note: Team members have access to policies through global API
|
|
{
|
|
enabled: isRouteOk && !!policyId, // Note: this justifies the number type assertions above
|
|
refetchOnWindowFocus: false,
|
|
retry: false,
|
|
select: (data: IStoredPolicyResponse) => data.policy,
|
|
onSuccess: (returnedQuery) => {
|
|
setLastEditedQueryId(returnedQuery.id);
|
|
setLastEditedQueryName(returnedQuery.name);
|
|
setLastEditedQueryDescription(returnedQuery.description);
|
|
setLastEditedQueryBody(returnedQuery.query);
|
|
setLastEditedQueryResolution(returnedQuery.resolution);
|
|
setLastEditedQueryCritical(returnedQuery.critical);
|
|
setLastEditedQueryPlatform(returnedQuery.platform);
|
|
// TODO(sarah): What happens if the team id in the policy response doesn't match the
|
|
// url param? In theory, the backend should ensure this doesn't happen.
|
|
setPolicyTeamId(returnedQuery.team_id || 0);
|
|
},
|
|
onError: (error) => handlePageError(error),
|
|
}
|
|
);
|
|
|
|
useQuery<IHostResponse, Error, IHost>(
|
|
"hostFromURL",
|
|
() =>
|
|
hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)), // TODO(sarah): What should happen if this doesn't parse (e.g. the string is "foo")? Also, note that "1,2,3" parses as 1.
|
|
{
|
|
enabled: isRouteOk && !!location.query.host_ids,
|
|
retry: false,
|
|
select: (data: IHostResponse) => data.host,
|
|
onSuccess: (host) => {
|
|
const targets = selectedTargets;
|
|
host.target_type = "hosts";
|
|
targets.push(host);
|
|
setSelectedTargets([...targets]);
|
|
},
|
|
}
|
|
);
|
|
|
|
const { mutateAsync: createPolicy } = useMutation(
|
|
(formData: IPolicyFormData) => {
|
|
return formData.team_id
|
|
? teamPoliciesAPI.create(formData)
|
|
: globalPoliciesAPI.create(formData);
|
|
}
|
|
);
|
|
|
|
const detectIsFleetQueryRunnable = () => {
|
|
statusAPI.live_query().catch(() => {
|
|
setIsLiveQueryRunnable(false);
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
detectIsFleetQueryRunnable();
|
|
}, []);
|
|
|
|
// Updates title that shows up on browser tabs
|
|
useEffect(() => {
|
|
// e.g., Policy details | Antivirus healthy (Linux) | Fleet for osquery
|
|
document.title = `Policy details | ${storedPolicy?.name} | ${DOCUMENT_TITLE_SUFFIX}`;
|
|
}, [location.pathname, storedPolicy?.name]);
|
|
|
|
useEffect(() => {
|
|
setShowOpenSchemaActionText(!isSidebarOpen);
|
|
}, [isSidebarOpen]);
|
|
|
|
const onOsqueryTableSelect = (tableName: string) => {
|
|
setSelectedOsqueryTable(tableName);
|
|
};
|
|
|
|
const onCloseSchemaSidebar = () => {
|
|
setIsSidebarOpen(false);
|
|
};
|
|
|
|
const onOpenSchemaSidebar = () => {
|
|
setIsSidebarOpen(true);
|
|
};
|
|
|
|
const renderLiveQueryWarning = (): JSX.Element | null => {
|
|
if (isLiveQueryRunnable || config?.server_settings.live_query_disabled) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className={`${baseClass}__warning`}>
|
|
<div className={`${baseClass}__message`}>
|
|
<p>
|
|
Fleet is unable to run a live query. Refresh the page or log in
|
|
again. If this keeps happening please{" "}
|
|
<CustomLink
|
|
url="https://github.com/fleetdm/fleet/issues/new/choose"
|
|
text="file an issue"
|
|
newTab
|
|
/>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderScreen = () => {
|
|
const step1Opts = {
|
|
router,
|
|
baseClass,
|
|
policyIdForEdit: policyId,
|
|
showOpenSchemaActionText,
|
|
storedPolicy,
|
|
isStoredPolicyLoading,
|
|
isTeamAdmin,
|
|
isTeamMaintainer,
|
|
isTeamObserver,
|
|
isObserverPlus,
|
|
storedPolicyError,
|
|
createPolicy,
|
|
onOsqueryTableSelect,
|
|
goToSelectTargets: () => setStep(LIVE_POLICY_STEPS[2]),
|
|
onOpenSchemaSidebar,
|
|
renderLiveQueryWarning,
|
|
};
|
|
|
|
const step2Opts = {
|
|
baseClass,
|
|
selectedTargets,
|
|
targetedHosts,
|
|
targetedLabels,
|
|
targetedTeams,
|
|
targetsTotalCount,
|
|
goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]),
|
|
goToRunQuery: () => setStep(LIVE_POLICY_STEPS[3]),
|
|
setSelectedTargets,
|
|
setTargetedHosts,
|
|
setTargetedLabels,
|
|
setTargetedTeams,
|
|
setTargetsTotalCount,
|
|
};
|
|
|
|
const step3Opts = {
|
|
selectedTargets,
|
|
storedPolicy,
|
|
setSelectedTargets,
|
|
goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]),
|
|
targetsTotalCount,
|
|
};
|
|
|
|
switch (step) {
|
|
case LIVE_POLICY_STEPS[2]:
|
|
return <SelectTargets {...step2Opts} />;
|
|
case LIVE_POLICY_STEPS[3]:
|
|
return <RunQuery {...step3Opts} />;
|
|
default:
|
|
return <QueryEditor {...step1Opts} />;
|
|
}
|
|
};
|
|
|
|
const isFirstStep = step === LIVE_POLICY_STEPS[1];
|
|
const showSidebar =
|
|
isFirstStep &&
|
|
isSidebarOpen &&
|
|
(isGlobalAdmin || isGlobalMaintainer || isAnyTeamMaintainerOrTeamAdmin);
|
|
|
|
if (!isRouteOk) {
|
|
return <Spinner />;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<MainContent className={baseClass}>
|
|
<div className={`${baseClass}__wrapper`}>{renderScreen()}</div>
|
|
</MainContent>
|
|
{showSidebar && (
|
|
<SidePanelContent>
|
|
<QuerySidePanel
|
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
|
selectedOsqueryTable={selectedOsqueryTable}
|
|
onClose={onCloseSchemaSidebar}
|
|
/>
|
|
</SidePanelContent>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default PolicyPage;
|