From eba5d1b1b62b9e41c96aa5f9f03c77b4df33b148 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Sun, 5 Dec 2021 17:12:55 -0600 Subject: [PATCH] Manage Policies Page: New policy modal (#3108) --- changes/issue-2594-add-policy-modal-revamp | 1 + .../forms/fields/InputField/_styles.scss | 2 +- frontend/interfaces/policy.ts | 8 ++ .../hosts/HostDetailsPage/HostDetailsPage.tsx | 26 +++++- ...ectQueryModal.jsx => SelectQueryModal.tsx} | 91 ++++++++----------- .../SelectQueryModal/_styles.scss | 7 ++ .../SelectQueryModal/{index.js => index.ts} | 0 .../ManagePoliciesPage/ManagePoliciesPage.tsx | 20 +++- .../AddPolicyModal/AddPolicyModal.tsx | 84 +++++++++++++++++ .../components/AddPolicyModal/_styles.scss | 59 ++++++++++++ .../components/AddPolicyModal/index.ts | 1 + .../PoliciesListWrapper.tsx | 2 + .../NewPolicyModal/NewPolicyModal.tsx | 12 ++- .../pages/queries/QueryPage/QueryPage.tsx | 1 + frontend/utilities/constants.ts | 68 ++++++++++++++ 15 files changed, 319 insertions(+), 63 deletions(-) create mode 100644 changes/issue-2594-add-policy-modal-revamp rename frontend/pages/hosts/HostDetailsPage/SelectQueryModal/{SelectQueryModal.jsx => SelectQueryModal.tsx} (78%) rename frontend/pages/hosts/HostDetailsPage/SelectQueryModal/{index.js => index.ts} (100%) create mode 100644 frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx create mode 100644 frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss create mode 100644 frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/index.ts diff --git a/changes/issue-2594-add-policy-modal-revamp b/changes/issue-2594-add-policy-modal-revamp new file mode 100644 index 000000000..89c9ccc38 --- /dev/null +++ b/changes/issue-2594-add-policy-modal-revamp @@ -0,0 +1 @@ +* In the Add a policy modal, users are prompted to add common policies or create a new one diff --git a/frontend/components/forms/fields/InputField/_styles.scss b/frontend/components/forms/fields/InputField/_styles.scss index a2285a19a..89c91e1c1 100644 --- a/frontend/components/forms/fields/InputField/_styles.scss +++ b/frontend/components/forms/fields/InputField/_styles.scss @@ -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; diff --git a/frontend/interfaces/policy.ts b/frontend/interfaces/policy.ts index dc55dd6f8..f03fe2ed9 100644 --- a/frontend/interfaces/policy.ts +++ b/frontend/interfaces/policy.ts @@ -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; +} diff --git a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx index e7db3a7d9..0cc26fe76 100644 --- a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx @@ -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 = ({ {showDeleteHostModal && renderDeleteHostModal()} - {showQueryHostModal && ( + {showQueryHostModal && host && ( setShowQueryHostModal(false)} - queries={fleetQueries} - dispatch={dispatch} + queries={fleetQueries || []} queryErrors={fleetQueriesError} isOnlyObserver={isOnlyObserver} + onQueryHostCustom={onQueryHostCustom} + onQueryHostSaved={onQueryHostSaved} /> )} {!!host && showTransferHostModal && ( diff --git a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.jsx b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.tsx similarity index 78% rename from frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.jsx rename to frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.tsx index 2acb4d3ed..7600dbfcc 100644 --- a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.jsx +++ b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.tsx @@ -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 ( ); }); @@ -198,7 +192,7 @@ const SelectQueryModal = ({ ); } - 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; diff --git a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/_styles.scss b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/_styles.scss index bc281ea13..ffe5f0a95 100644 --- a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/_styles.scss +++ b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/_styles.scss @@ -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; diff --git a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/index.js b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/index.ts similarity index 100% rename from frontend/pages/hosts/HostDetailsPage/SelectQueryModal/index.js rename to frontend/pages/hosts/HostDetailsPage/SelectQueryModal/index.ts diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index b08bdc4ef..02c4fa9dd 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -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( @@ -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: {
)} + {showAddPolicyModal && ( + + )} {showRemovePoliciesModal && ( 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 ( + + ); + }); + + return ( + + <> + Choose a policy template to get started or{" "} + + create your own policy + + . +
+ {policiesAvailable} +
+ +
+ ); +}; + +export default AddPolicyModal; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss new file mode 100644 index 000000000..9500f3906 --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss @@ -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; + } +} diff --git a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/index.ts b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/index.ts new file mode 100644 index 000000000..ac6cb2343 --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/index.ts @@ -0,0 +1 @@ +export { default } from "./AddPolicyModal"; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx index 4a12ce286..188c0ba9d 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx @@ -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; diff --git a/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx b/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx index d673d4207..162c8fe70 100644 --- a/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx +++ b/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx @@ -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(lastEditedQueryName); const [description, setDescription] = useState( lastEditedQueryDescription ); - const [resolution, setResolution] = useState(""); + const [resolution, setResolution] = useState( + lastEditedQueryResolution + ); const [errors, setErrors] = useState<{ [key: string]: string }>({}); useDeepEffect(() => { diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index 3a93d0b91..e79f60a61 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -41,6 +41,7 @@ const QueryPage = ({ location: { query: URLQuerySearch }, }: IQueryPageProps): JSX.Element => { const queryIdForEdit = paramsQueryId ? parseInt(paramsQueryId, 10) : null; + const { isGlobalAdmin, isGlobalMaintainer, diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index c9d1f6f10..3cceb8ac0 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -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: You’re 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. It’s 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: You’re 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: You’re 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" },