Manage Policies Page: New policy modal (#3108)

This commit is contained in:
RachelElysia 2021-12-05 17:12:55 -06:00 committed by GitHub
parent 5a2ed6f395
commit eba5d1b1b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 319 additions and 63 deletions

View File

@ -0,0 +1 @@
* In the Add a policy modal, users are prompted to add common policies or create a new one

View File

@ -1,5 +1,5 @@
.input-field {
line-height: 34px;
line-height: 1.5;
background-color: $ui-light-grey;
border: solid 1px $ui-fleet-blue-15;
border-radius: 4px;

View File

@ -48,3 +48,11 @@ export interface IPolicyFormData {
query?: string | number | boolean | any[] | undefined;
team_id?: number;
}
export interface IPolicyNew {
id?: number;
name: string;
description: string;
query: string;
resolution: string;
}

View File

@ -100,6 +100,12 @@ interface IHostResponse {
host: IHost;
}
const TAGGED_TEMPLATES = {
queryByHostRoute: (hostId: number | undefined | null) => {
return `${hostId ? `?host_ids=${hostId}` : ""}`;
},
};
const HostDetailsPage = ({
router,
params: { host_id },
@ -117,6 +123,7 @@ const HostDetailsPage = ({
setLastEditedQueryName,
setLastEditedQueryDescription,
setLastEditedQueryBody,
setLastEditedQueryResolution,
setPolicyTeamId,
} = useContext(PolicyContext);
const canTransferTeam =
@ -401,6 +408,7 @@ const HostDetailsPage = ({
"Returns yes or no for detecting operating system and version"
);
setLastEditedQueryBody(osPolicy);
setLastEditedQueryResolution("");
router.replace(NEW_POLICY);
};
@ -453,6 +461,17 @@ const HostDetailsPage = ({
return router.push(`${PATHS.MANAGE_HOSTS}/labels/${label.id}`);
};
const onQueryHostCustom = () => {
router.push(PATHS.NEW_QUERY + TAGGED_TEMPLATES.queryByHostRoute(host?.id));
};
const onQueryHostSaved = (selectedQuery: IQuery) => {
router.push(
PATHS.EDIT_QUERY(selectedQuery) +
TAGGED_TEMPLATES.queryByHostRoute(host?.id)
);
};
const onTransferHostSubmit = async (team: ITeam) => {
const teamId = typeof team.id === "number" ? team.id : null;
@ -1303,14 +1322,15 @@ const HostDetailsPage = ({
</TabsWrapper>
{showDeleteHostModal && renderDeleteHostModal()}
{showQueryHostModal && (
{showQueryHostModal && host && (
<SelectQueryModal
host={host}
onCancel={() => setShowQueryHostModal(false)}
queries={fleetQueries}
dispatch={dispatch}
queries={fleetQueries || []}
queryErrors={fleetQueriesError}
isOnlyObserver={isOnlyObserver}
onQueryHostCustom={onQueryHostCustom}
onQueryHostSaved={onQueryHostSaved}
/>
)}
{!!host && showTransferHostModal && (

View File

@ -1,58 +1,48 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { push } from "react-router-redux";
import React, { useState, useCallback } from "react";
import { filter, includes } from "lodash";
import PATHS from "router/paths";
import queryInterface from "interfaces/query";
import hostInterface from "interfaces/host";
import { IHost } from "interfaces/host";
import { IQuery } from "interfaces/query";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import OpenNewTabIcon from "../../../../../assets/images/open-new-tab-12x12@2x.png";
import ErrorIcon from "../../../../../assets/images/icon-error-16x16@2x.png";
export interface ISelectQueryModalProps {
host: IHost;
onCancel: () => void;
onQueryHostCustom: () => void;
onQueryHostSaved: (selectedQuery: IQuery) => void;
queries: IQuery[] | [];
queryErrors: any | null;
isOnlyObserver: boolean | undefined;
}
const baseClass = "select-query-modal";
const onQueryHostCustom = (host, dispatch) => {
return dispatch(
push({
pathname: PATHS.NEW_QUERY,
query: { host_ids: [host.id] },
})
);
};
const onQueryHostSaved = (host, selectedQuery, dispatch) => {
return dispatch(
push({
pathname: PATHS.EDIT_QUERY(selectedQuery),
query: { host_ids: [host.id] },
})
);
};
const SelectQueryModal = ({
host,
onCancel,
dispatch,
onQueryHostCustom,
onQueryHostSaved,
queries,
queryErrors,
isOnlyObserver,
}) => {
}: ISelectQueryModalProps) => {
let queriesAvailableToRun = queries;
const [queriesFilter, setQueriesFilter] = useState("");
if (isOnlyObserver) {
queriesAvailableToRun = queries.filter(
(query) => query.observer_can_run === true
);
}
const [queriesFilter, setQueriesFilter] = useState("");
const getQueries = () => {
if (!queriesFilter) {
return queriesAvailableToRun;
@ -71,10 +61,21 @@ const SelectQueryModal = ({
});
};
const onFilterQueries = useCallback(
(filterString: string): void => {
setQueriesFilter(filterString);
},
[setQueriesFilter]
);
const queriesFiltered = getQueries();
const queriesCount = queriesFiltered.length;
const customQueryButton = () => {
return (
<Button
onClick={() => onQueryHostCustom(host, dispatch)}
onClick={() => onQueryHostCustom()}
variant="brand"
className={`${baseClass}__custom-query-button`}
>
@ -83,16 +84,7 @@ const SelectQueryModal = ({
);
};
const onFilterQueries = (event) => {
setQueriesFilter(event);
return false;
};
const queriesFiltered = getQueries();
const queriesCount = queriesFiltered.length;
const results = () => {
const results = (): JSX.Element => {
if (queryErrors) {
return (
<div className={`${baseClass}__no-queries`}>
@ -137,10 +129,12 @@ const SelectQueryModal = ({
key={query.id}
variant="unstyled-modal-query"
className="modal-query-button"
onClick={() => onQueryHostSaved(host, query, dispatch)}
onClick={() => onQueryHostSaved(query)}
>
<span className="info__header">{query.name}</span>
<span className="info__data">{query.description}</span>
<>
<span className="info__header">{query.name}</span>
<span className="info__data">{query.description}</span>
</>
</Button>
);
});
@ -198,7 +192,7 @@ const SelectQueryModal = ({
</div>
);
}
return null;
return <></>;
};
return (
@ -212,13 +206,4 @@ const SelectQueryModal = ({
);
};
SelectQueryModal.propTypes = {
dispatch: PropTypes.func,
host: hostInterface,
queries: PropTypes.arrayOf(queryInterface),
onCancel: PropTypes.func,
queryErrors: PropTypes.object, // eslint-disable-line react/forbid-prop-types
isOnlyObserver: PropTypes.bool,
};
export default SelectQueryModal;

View File

@ -25,6 +25,13 @@
padding: $pad-xxlarge;
border-radius: $pad-small;
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
.info {
display: flex;

View File

@ -31,6 +31,7 @@ import InfoBanner from "components/InfoBanner/InfoBanner";
import IconToolTip from "components/IconToolTip";
import TeamsDropdown from "components/TeamsDropdown";
import PoliciesListWrapper from "./components/PoliciesListWrapper";
import AddPolicyModal from "./components/AddPolicyModal";
import RemovePoliciesModal from "./components/RemovePoliciesModal";
const baseClass = "manage-policies-page";
@ -72,6 +73,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
setLastEditedQueryName,
setLastEditedQueryDescription,
setLastEditedQueryBody,
setLastEditedQueryResolution,
setPolicyTeamId,
} = useContext(PolicyContext);
@ -121,6 +123,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
const [selectedPolicyIds, setSelectedPolicyIds] = useState<
number[] | never[]
>([]);
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
const [showRemovePoliciesModal, setShowRemovePoliciesModal] = useState(false);
const [showInheritedPolicies, setShowInheritedPolicies] = useState(false);
const [updateInterval, setUpdateInterval] = useState<string>(
@ -180,6 +183,8 @@ const ManagePolicyPage = (managePoliciesPageProps: {
setPolicyTeamId(id);
};
const toggleAddPolicyModal = () => setShowAddPolicyModal(!showAddPolicyModal);
const toggleRemovePoliciesModal = () =>
setShowRemovePoliciesModal(!showRemovePoliciesModal);
@ -190,7 +195,8 @@ const ManagePolicyPage = (managePoliciesPageProps: {
setLastEditedQueryName("");
setLastEditedQueryDescription("");
setLastEditedQueryBody(DEFAULT_POLICY.query);
router.push(PATHS.NEW_POLICY);
setLastEditedQueryResolution("");
toggleAddPolicyModal();
};
const onRemovePoliciesClick = (selectedTableIds: number[]): void => {
@ -362,7 +368,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
<div className={`${baseClass}__action-button-container`}>
<Button
variant="brand"
className={`${baseClass}__add-policy-btn`}
className={`${baseClass}__select-policy-button`}
onClick={onAddPolicyClick}
>
Add a policy
@ -411,6 +417,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
policiesList={teamPolicies}
isLoading={isLoadingTeamPolicies}
onRemovePoliciesClick={onRemovePoliciesClick}
toggleAddPolicyModal={toggleAddPolicyModal}
canAddOrRemovePolicy={canAddOrRemovePolicy(
currentUser,
selectedTeamId
@ -426,6 +433,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
policiesList={globalPolicies}
isLoading={isLoadingGlobalPolicies}
onRemovePoliciesClick={onRemovePoliciesClick}
toggleAddPolicyModal={toggleAddPolicyModal}
canAddOrRemovePolicy={canAddOrRemovePolicy(
currentUser,
selectedTeamId
@ -475,6 +483,14 @@ const ManagePolicyPage = (managePoliciesPageProps: {
/>
</div>
)}
{showAddPolicyModal && (
<AddPolicyModal
onCancel={toggleAddPolicyModal}
router={router}
teamId={selectedTeamId}
teamName={selectedTeamData?.name}
/>
)}
{showRemovePoliciesModal && (
<RemovePoliciesModal
onCancel={toggleRemovePoliciesModal}

View File

@ -0,0 +1,84 @@
import React, { useContext } from "react";
import { Link } from "react-router";
import PATHS from "router/paths";
import { DEFAULT_POLICY, DEFAULT_POLICIES } from "utilities/constants";
import { IPolicyNew } from "interfaces/policy";
import { PolicyContext } from "context/policy";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
export interface IAddPolicyModalProps {
onCancel: () => void;
router: any;
teamId: number;
teamName?: string;
}
const baseClass = "add-policy-modal";
const AddPolicyModal = ({
onCancel,
router,
teamId,
teamName,
}: IAddPolicyModalProps) => {
const {
setLastEditedQueryName,
setLastEditedQueryDescription,
setLastEditedQueryBody,
setLastEditedQueryResolution,
setPolicyTeamId,
} = useContext(PolicyContext);
const onAddPolicy = (selectedPolicy: IPolicyNew) => {
teamName
? setLastEditedQueryName(`${selectedPolicy.name} (${teamName})`)
: setLastEditedQueryName(selectedPolicy.name);
setLastEditedQueryDescription(selectedPolicy.description);
setLastEditedQueryBody(selectedPolicy.query);
setLastEditedQueryResolution(selectedPolicy.resolution);
setPolicyTeamId(teamId);
router.push(PATHS.NEW_POLICY);
};
const policiesAvailable = DEFAULT_POLICIES.map((policy) => {
return (
<Button
key={policy.key}
variant="unstyled-modal-query"
className="modal-policy-button"
onClick={() => onAddPolicy(policy)}
>
<>
<span className="info__header">{policy.name}</span>
<span className="info__data">{policy.description}</span>
</>
</Button>
);
});
return (
<Modal
title="Add a policy"
onExit={onCancel}
className={`${baseClass}__modal`}
>
<>
Choose a policy template to get started or{" "}
<Link to={PATHS.NEW_POLICY} className={`${baseClass}__back-link`}>
create your own policy
</Link>
.
<div className={`${baseClass}__policy-selection`}>
{policiesAvailable}
</div>
</>
</Modal>
);
};
export default AddPolicyModal;

View File

@ -0,0 +1,59 @@
.add-policy-modal {
#error-icon {
height: 12px;
width: 12px;
margin-right: 8px;
}
#new-tab-icon {
height: 12px;
width: 12px;
margin-left: 6px;
}
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
&__modal {
@include position(absolute, 22px null null null);
background-color: $core-white;
width: 658px;
padding: $pad-xxlarge;
border-radius: $pad-small;
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
.info {
display: flex;
&__header {
display: block;
color: $core-fleet-black;
font-weight: $bold;
font-size: $x-small;
text-align: left;
}
&__data {
display: block;
color: $core-fleet-black;
font-weight: normal;
font-size: $x-small;
text-align: left;
margin-top: 10px;
}
}
}
&__policy-selection {
padding: $pad-large 0;
}
}

View File

@ -0,0 +1 @@
export { default } from "./AddPolicyModal";

View File

@ -27,6 +27,7 @@ interface IPoliciesListWrapperProps {
canAddOrRemovePolicy?: boolean;
tableType?: string;
selectedTeamData: ITeam | undefined;
toggleAddPolicyModal?: () => void;
}
const PoliciesListWrapper = ({
@ -37,6 +38,7 @@ const PoliciesListWrapper = ({
canAddOrRemovePolicy,
tableType,
selectedTeamData,
toggleAddPolicyModal,
}: IPoliciesListWrapperProps): JSX.Element => {
const { MANAGE_HOSTS } = paths;

View File

@ -33,15 +33,19 @@ const NewPolicyModal = ({
onCreatePolicy,
setIsNewPolicyModalOpen,
}: INewPolicyModalProps): JSX.Element => {
const { lastEditedQueryName, lastEditedQueryDescription } = useContext(
PolicyContext
);
const {
lastEditedQueryName,
lastEditedQueryDescription,
lastEditedQueryResolution,
} = useContext(PolicyContext);
const [name, setName] = useState<string>(lastEditedQueryName);
const [description, setDescription] = useState<string>(
lastEditedQueryDescription
);
const [resolution, setResolution] = useState<string>("");
const [resolution, setResolution] = useState<string>(
lastEditedQueryResolution
);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
useDeepEffect(() => {

View File

@ -41,6 +41,7 @@ const QueryPage = ({
location: { query: URLQuerySearch },
}: IQueryPageProps): JSX.Element => {
const queryIdForEdit = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
const {
isGlobalAdmin,
isGlobalMaintainer,

View File

@ -11,6 +11,74 @@ export enum PolicyResponse {
export const DEFAULT_GRAVATAR_LINK =
"https://fleetdm.com/images/permanent/icon-avatar-default-128x128-2x.png";
export const DEFAULT_POLICIES = [
{
key: 1,
query: `SELECT 1 FROM disk_encryption WHERE user_uuid IS NOT "" AND filevault_status = 'on' LIMIT 1`,
name: "Is Filevault enabled on macOS devices?",
description:
"Checks to make sure that the Filevault feature is enabled on macOS devices.",
resolution:
"Choose Apple menu > System Preferences, then click Security & Privacy. Click the FileVault tab. Click the Lock icon, then enter an administrator name and password. Click Turn On FileVault.",
},
{
key: 2,
query: "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1",
name: "Is Gatekeeper enabled on macOS devices?",
description:
"Checks to make sure that the Gatekeeper feature is enabled on macOS devices. Gatekeeper tries to ensure only trusted software is run on a mac machine.",
resolution:
"On the failing device, run the following command in the Terminal app: /usr/sbin / spctl--master- enable",
},
{
key: 3,
query: "SELECT 1 FROM bitlocker_info WHERE protection_status = 1;",
name: "Is disk encryption enabled on Windows devices?",
description:
"Checks to make sure that device encryption is enabled on Windows devices.",
resolution:
"Option 1: Select the Start button. Select Settings > Update & Security > Device encryption. If Device encryption doesn't appear, skip to Option 2. If device encryption is turned off, select Turn on. Option 2: Select the Start button. Under Windows System, select Control Panel. Select System and Security. Under BitLocker Drive Encryption, select Manage BitLocker. Select Turn on BitLocker and then follow the instructions.",
},
{
key: 4,
query:
"SELECT 1 FROM sip_config WHERE config_flag = 'sip' AND enabled = 1;",
name: "Is System Integrity Protection (SIP) enabled on macOS devices?",
description: "Checks to make sure that the SIP is enabled.",
resolution:
"On the failing device, run the following command in the Terminal app: /usr/sbin/spctl --master-enable",
},
{
key: 5,
query:
"SELECT 1 FROM managed_policies WHERE domain = 'com.apple.loginwindow' AND name = 'com.apple.login.mcx.DisableAutoLoginClient' AND value = 1 LIMIT 1",
name: "Is automatic login disabled on macOS devices?",
description:
"Required: Youre already enforcing a policy via Moble Device Management (MDM). Checks to make sure that the device user cannot log in to the device without a password. Its good practice to have both this policy and the “Is Filevault enabled on macOS devices?” policy enabled.",
resolution:
"The following example profile includes a setting to disable automatic login: https://github.com/gregneagle/profiles/blob/fecc73d66fa17b6fa78b782904cb47cdc1913aeb/loginwindow.mobileconfig#L64-L65",
},
{
key: 6,
query:
"SELECT 1 FROM managed_policies WHERE domain = 'com.apple.MCX' AND name = 'DisableGuestAccount' AND value = 0 LIMIT 1;",
name: "Are guest users not activated on macOS devices?",
description:
"Required: Youre already enforcing a policy via Moble Device Management (MDM). Checks to make sure that guest accounts cannot be used to log in to the device without a password.",
resolution:
"The following example profile includes a setting to disable automatic login: https://github.com/gregneagle/profiles/blob/fecc73d66fa17b6fa78b782904cb47cdc1913aeb/loginwindow.mobileconfig#L68-L71",
},
{
key: 7,
query:
"SELECT 1 FROM managed_policies WHERE domain = 'com.apple.Terminal' AND name = 'SecureKeyboardEntry' AND value=1 LIMIT 1;",
name: "Is secure keyboard entry enabled on macOS devices?",
description:
"Required: Youre already enforcing a policy via Moble Device Management (MDM). Checks to make sure that the Secure Keyboard Entry setting is enabled.",
resolution: "",
},
];
export const FREQUENCY_DROPDOWN_OPTIONS = [
{ value: 900, label: "Every 15 minutes" },
{ value: 3600, label: "Every hour" },