diff --git a/.gitignore b/.gitignore index e8cef5c36c55..50c2efb4b8d6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,8 @@ yarn-error.log* # vscode debug logs debug.log -app.log \ No newline at end of file +app.log + +# Cursor IDE +.cursor/rules + diff --git a/cspell.json b/cspell.json index 8d5d275d003e..69e05ddee5f8 100644 --- a/cspell.json +++ b/cspell.json @@ -1,65 +1,78 @@ { - "version": "0.2", - "ignorePaths": [], - "dictionaryDefinitions": [], - "dictionaries": [], - "words": [ - "ADMS", - "AITM", - "Augmentt", - "Autotask", - "Choco", - "CIPP", - "CIPP-API", - "Datto", - "Entra", - "ESET", - "GDAP", - "HIBP", - "Hudu", - "ImmyBot", - "Intune", - "LCID", - "OBEE", - "Passwordless", - "pwpush", - "Rewst", - "Sherweb", - "Syncro", - "TERRL", - "Yubikey" - ], - "ignoreWords": [ - "Addins", - "CIPPAPI", - "PSTN", - "TNEF", - "exo_individualsharing", - "exo_mailboxaudit", - "exo_mailtipsenabled", - "exo_outlookaddins", - "exo_storageproviderrestricted", - "locationcipp", - "mdo_antiphishingpolicies", - "mdo_autoforwardingmode", - "mdo_blockmailforward", - "mdo_commonattachmentsfilter", - "mdo_highconfidencephishaction", - "mdo_highconfidencespamaction", - "mdo_phishthresholdlevel", - "mdo_phisspamacation", - "mdo_safeattachmentpolicy", - "mdo_safeattachments", - "mdo_safedocuments", - "mdo_safelinksforOfficeApps", - "mdo_safelinksforemail", - "mdo_spam_notifications_only_for_admins", - "mdo_zapmalware", - "mdo_zapphish", - "mdo_zapspam", - "microsoftonline", - "mip_search_auditlog", - "winmail" - ], - "import": [] + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [], + "words": [ + "ADMS", + "AITM", + "Augmentt", + "Automapping", + "Autotask", + "Choco", + "cipp", + "CIPP", + "CIPP-API", + "Datto", + "Entra", + "ESET", + "GDAP", + "HIBP", + "Hudu", + "ImmyBot", + "Intune", + "LCID", + "OBEE", + "passwordless", + "Passwordless", + "pwpush", + "Reshare", + "Rewst", + "Sherweb", + "Syncro", + "TERRL", + "Yubikey", + "DMARC" + ], + "ignoreWords": [ + "Addins", + "Disablex", + "Displayname", + "CIPPAPI", + "PSTN", + "TNEF", + "Equivio", + "defaultvalues", + "Excludedfile", + "exo_individualsharing", + "exo_mailboxaudit", + "exo_mailtipsenabled", + "exo_outlookaddins", + "exo_storageproviderrestricted", + "donotchange", + "locationcipp", + "mdo_antiphishingpolicies", + "mdo_autoforwardingmode", + "mdo_blockmailforward", + "mdo_commonattachmentsfilter", + "mdo_highconfidencephishaction", + "mdo_highconfidencespamaction", + "mdo_phishthresholdlevel", + "mdo_phisspamacation", + "mdo_safeattachmentpolicy", + "mdo_safeattachments", + "mdo_safedocuments", + "mdo_safelinksforOfficeApps", + "mdo_safelinksforemail", + "mdo_spam_notifications_only_for_admins", + "mdo_zapmalware", + "mdo_zapphish", + "mdo_zapspam", + "microsoftonline", + "mip_search_auditlog", + "winmail", + "onmicrosoft.com", + "MOERA" + ], + "import": [] } diff --git a/generate-placeholders.js b/generate-placeholders.js index 2f6b614fe9a8..34e28fb31ccf 100644 --- a/generate-placeholders.js +++ b/generate-placeholders.js @@ -70,6 +70,8 @@ const pages = [ { title: "Defender Deployment", path: "/security/defender/deployment" }, { title: "Vulnerabilities", path: "/security/defender/list-defender-tvm" }, { title: "Device Compliance", path: "/security/reports/list-device-compliance" }, + { title: "Safe Links", path: "/security/safelinks/safelinks" }, + { title: "Safe Links Templates", path: "/security/safelinks/safelinks-template" }, { title: "Applications", path: "/endpoint/applications/list" }, { title: "Application Queue", path: "/endpoint/applications/queue" }, { title: "Add Choco App", path: "/endpoint/applications/add-choco-app" }, @@ -99,6 +101,7 @@ const pages = [ { title: "Deleted Mailboxes", path: "/email/administration/deleted-mailboxes" }, { title: "Mailbox Rules", path: "/email/administration/mailbox-rules" }, { title: "Contacts", path: "/email/administration/contacts" }, + { title: "Contact Templates", path: "/email/administration/contacts-template" }, { title: "Quarantine", path: "/email/administration/quarantine" }, { title: "Tenant Allow/Block Lists", path: "/email/administration/tenant-allow-block-lists" }, { title: "Mailbox Restore Wizard", path: "/email/tools/mailbox-restore-wizard" }, @@ -121,7 +124,6 @@ const pages = [ { title: "Message Trace", path: "/email/reports/message-trace" }, { title: "Anti-Phishing Filters", path: "/email/reports/antiphishing-filters" }, { title: "Malware Filters", path: "/email/reports/malware-filters" }, - { title: "Safe Links Filters", path: "/email/reports/safelinks-filters" }, { title: "Safe Attachments Filters", path: "/email/reports/safeattachments-filters" }, { title: "Shared Mailbox with Enabled Account", diff --git a/package.json b/package.json index 624037bd4884..437a7553b68a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "8.0.2", + "version": "8.0.3", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { @@ -36,7 +36,7 @@ "@mui/system": "6.4.7", "@mui/x-date-pickers": "7.27.3", "@musement/iso-duration": "^1.0.0", - "@react-pdf/renderer": "4.3.0", + "@react-pdf/renderer": "^4.3.0", "@reduxjs/toolkit": "2.6.1", "@tanstack/query-sync-storage-persister": "^5.76.0", "@tanstack/react-query": "^5.51.11", @@ -112,4 +112,4 @@ "eslint": "9.22.0", "eslint-config-next": "15.2.2" } -} \ No newline at end of file +} diff --git a/public/reportImages/board.jpg b/public/reportImages/board.jpg new file mode 100644 index 000000000000..05c3e51ddc34 Binary files /dev/null and b/public/reportImages/board.jpg differ diff --git a/public/reportImages/city.jpg b/public/reportImages/city.jpg new file mode 100644 index 000000000000..155439cf30f2 Binary files /dev/null and b/public/reportImages/city.jpg differ diff --git a/public/reportImages/glasses.jpg b/public/reportImages/glasses.jpg new file mode 100644 index 000000000000..b41d200ea650 Binary files /dev/null and b/public/reportImages/glasses.jpg differ diff --git a/public/reportImages/laptop.jpg b/public/reportImages/laptop.jpg new file mode 100644 index 000000000000..31a14fc4383e Binary files /dev/null and b/public/reportImages/laptop.jpg differ diff --git a/public/reportImages/soc.jpg b/public/reportImages/soc.jpg new file mode 100644 index 000000000000..f8da4eba5139 Binary files /dev/null and b/public/reportImages/soc.jpg differ diff --git a/public/reportImages/working.jpg b/public/reportImages/working.jpg new file mode 100644 index 000000000000..c979c21b1ea0 Binary files /dev/null and b/public/reportImages/working.jpg differ diff --git a/public/sponsors/domotz-dark.png b/public/sponsors/domotz-dark.png new file mode 100644 index 000000000000..ac26f6f0f6ad Binary files /dev/null and b/public/sponsors/domotz-dark.png differ diff --git a/public/sponsors/domotz-light.png b/public/sponsors/domotz-light.png new file mode 100644 index 000000000000..dab40b067807 Binary files /dev/null and b/public/sponsors/domotz-light.png differ diff --git a/public/version.json b/public/version.json index 6e2baa8faabc..8857000ffcc6 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.0.3" -} \ No newline at end of file + "version": "8.1.0" +} diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index 35554764168e..2081c91b2a11 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -1,5 +1,4 @@ import { - keepPreviousData, useInfiniteQuery, useMutation, useQuery, diff --git a/src/components/CSVReader.jsx b/src/components/CSVReader.jsx index d34d9ee79007..be3f6f67f02a 100644 --- a/src/components/CSVReader.jsx +++ b/src/components/CSVReader.jsx @@ -1,86 +1,67 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useCSVReader, lightenDarkenColor, formatFileSize } from "react-papaparse"; +import { Box, Typography, useTheme } from "@mui/material"; +import { CloudUpload } from "@mui/icons-material"; -const GREY = "#CCC"; -const GREY_LIGHT = "rgba(255, 255, 255, 0.4)"; +/* + * These colors define our remove button states. The light version is + * calculated rather than hardcoded - a small touch that ensures + * consistent color relationships no matter what base color we use. + * + * Sometimes it's these little details that make a component feel polished. + */ const DEFAULT_REMOVE_HOVER_COLOR = "#A01919"; const REMOVE_HOVER_COLOR_LIGHT = lightenDarkenColor(DEFAULT_REMOVE_HOVER_COLOR, 40); -const GREY_DIM = "#686868"; -const styles = { - zone: { - alignItems: "center", - border: `2px dashed`, - borderRadius: 20, - display: "flex", - flexDirection: "column", - height: "100%", - justifyContent: "center", - padding: 20, - }, - file: { - background: "linear-gradient(to bottom, #aaa, #aaa)", - borderRadius: 20, - display: "flex", - height: 60, - width: 120, - position: "relative", - zIndex: 10, - flexDirection: "column", - justifyContent: "center", - }, - info: { - alignItems: "center", - display: "flex", - flexDirection: "column", - paddingLeft: 10, - paddingRight: 10, - }, - size: { - borderRadius: 3, - marginBottom: "0.5em", - justifyContent: "center", - display: "flex", - }, - name: { - borderRadius: 3, - fontSize: 12, - marginBottom: "0.5em", - }, - progressBar: { - bottom: 14, - position: "absolute", - width: "100%", - paddingLeft: 10, - paddingRight: 10, - }, - zoneHover: { - borderColor: GREY_DIM, - }, - default: { - borderColor: GREY, - }, - remove: { - height: 23, - position: "absolute", - right: 6, - top: 6, - width: 23, - }, -}; - -export default function CSVReader(props) { +/* + * This component has evolved from a simple file input to a polished + * upload zone that maintains state between wizard steps. It's a good + * example of how components grow with requirements while trying to + * keep their core purpose clear. + * + * The journey to this version taught us about: + * - Proper event handling with third-party libraries + * - State persistence in multi-step forms + * - The value of simple solutions (sessionStorage vs complex state) + */ +export default function CSVReader({ config, onDrop, onRemove }) { const { CSVReader } = useCSVReader(); const [zoneHover, setZoneHover] = useState(false); const [removeHoverColor, setRemoveHoverColor] = useState(DEFAULT_REMOVE_HOVER_COLOR); + const [storedFile, setStoredFile] = useState(null); + const theme = useTheme(); + + /* + * On mount, we check sessionStorage for file details. This lets us + * restore the preview when users navigate back to this step. + * + * It's a simple solution that works well - sometimes the best + * approaches don't need complex state management. The fact that + * it "just works" is a feature, not a bug. + */ + useEffect(() => { + const fileName = sessionStorage.getItem('csvFileName'); + const fileSize = sessionStorage.getItem('csvFileSize'); + if (fileName && fileSize) { + console.log('Restoring file preview:', fileName); + setStoredFile({ + name: fileName, + size: parseInt(fileSize, 10) + }); + } + }, []); return ( { - //call the ondrop function from the props, passing the results. - props.onDrop(results.data); + config={config} + onUploadAccepted={(results, file) => { + console.log('File accepted:', file.name); + onDrop(results.data); setZoneHover(false); + setStoredFile(file); + // Store file details for persistence between steps + sessionStorage.setItem('csvFileName', file.name); + sessionStorage.setItem('csvFileSize', file.size.toString()); }} onDragOver={(event) => { event.preventDefault(); @@ -92,46 +73,91 @@ export default function CSVReader(props) { }} > {({ getRootProps, acceptedFile, ProgressBar, getRemoveFileProps, Remove }) => ( - <> -
- {acceptedFile ? ( - <> -
-
- {formatFileSize(acceptedFile.size)} - {acceptedFile.name} -
-
- -
-
{ - event.preventDefault(); - setRemoveHoverColor(REMOVE_HOVER_COLOR_LIGHT); - }} - onMouseOut={(event) => { - event.preventDefault(); - setRemoveHoverColor(DEFAULT_REMOVE_HOVER_COLOR); - }} - > - -
-
- - ) : ( - "Drop CSV file here or click to upload" - )} -
- + + {(acceptedFile || storedFile) ? ( + + + + + + {acceptedFile?.name || storedFile?.name} + + + {formatFileSize(acceptedFile?.size || storedFile?.size)} + + + + + {acceptedFile && } + + {/* + * The remove button's event handling taught us about working with + * third-party libraries. Instead of fighting the library's patterns, + * we adapted to work with them. A good reminder that sometimes + * the best solution is to follow the path of least resistance. + */} + { + console.log('Removing file'); + setStoredFile(null); + sessionStorage.removeItem('csvFileName'); + sessionStorage.removeItem('csvFileSize'); + // Notify parent that file was removed + onRemove?.(); + } + })} + > + + + + ) : ( + + + + Drop CSV file here + + + or click to browse + + + )} + )}
); -} +} \ No newline at end of file diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index 8a123e5064b2..cc7c0639aa83 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -204,7 +204,6 @@ CippBannerListCard.propTypes = { actionButton: PropTypes.element, propertyItems: PropTypes.array, table: PropTypes.object, - actionButton: PropTypes.element, isFetching: PropTypes.bool, children: PropTypes.node, cardLabelBoxActions: PropTypes.element, diff --git a/src/components/CippCards/CippDomainCards.jsx b/src/components/CippCards/CippDomainCards.jsx index 3aa81e3019fe..b6d8adb58bf4 100644 --- a/src/components/CippCards/CippDomainCards.jsx +++ b/src/components/CippCards/CippDomainCards.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Button, Collapse, @@ -477,7 +477,7 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) waiting: !!domain && enableHttps, }); - // Adjust grid item size based on fullwidth prop + // Adjust Grid size based on fullwidth prop const gridItemSize = fullwidth ? 12 : 4; return ( diff --git a/src/components/CippCards/CippExchangeInfoCard.jsx b/src/components/CippCards/CippExchangeInfoCard.jsx index 1a368b99324d..cfb08497dcf9 100644 --- a/src/components/CippCards/CippExchangeInfoCard.jsx +++ b/src/components/CippCards/CippExchangeInfoCard.jsx @@ -14,7 +14,7 @@ import { PropertyListItem } from "/src/components/property-list-item"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; import { Check as CheckIcon, Close as CloseIcon, Sync } from "@mui/icons-material"; import { LinearProgressWithLabel } from "../linearProgressWithLabel"; -import { Stack } from "@mui/system"; +import { Stack, Grid } from "@mui/system"; export const CippExchangeInfoCard = (props) => { const { exchangeData, isLoading = false, isFetching = false, handleRefresh, ...other } = props; @@ -63,12 +63,36 @@ export const CippExchangeInfoCard = (props) => { + ) : ( - exchangeData?.RecipientTypeDetails || "N/A" + + + + Mailbox Type: + + + {exchangeData?.RecipientTypeDetails || "N/A"} + + + + + Hidden from GAL: + + + {getCippFormatting(exchangeData?.HiddenFromAddressLists, "HiddenFromAddressLists")} + + + + + Blocked For Spam: + + + {getCippFormatting(exchangeData?.BlockedForSpam, "BlockedForSpam")} + + + ) } /> @@ -100,82 +124,119 @@ export const CippExchangeInfoCard = (props) => { /> - ) : ( - getCippFormatting(exchangeData?.HiddenFromAddressLists, "HiddenFromAddressLists") - ) - } - /> - - ) : ( - getCippFormatting(exchangeData?.ForwardAndDeliver, "ForwardAndDeliver") - ) - } - /> - - ) : ( - exchangeData?.ForwardingAddress || "N/A" - ) - } - /> - - ) : ( - getCippFormatting(exchangeData?.ArchiveMailBox, "ArchiveMailBox") - ) - } - /> - - ) : ( - getCippFormatting(exchangeData?.AutoExpandingArchive, "AutoExpandingArchive") - ) - } - /> - - ) : exchangeData?.TotalArchiveItemSize != null ? ( - `${exchangeData.TotalArchiveItemSize} GB` - ) : ( - "N/A" - ) + + ) : (() => { + const forwardingAddress = exchangeData?.ForwardingAddress; + const forwardAndDeliver = exchangeData?.ForwardAndDeliver; + + // Determine forwarding type and clean address + let forwardingType = "None"; + let cleanAddress = ""; + + if (forwardingAddress) { + if (forwardingAddress.startsWith("smtp:")) { + forwardingType = "External"; + cleanAddress = forwardingAddress.replace("smtp:", ""); + } else { + forwardingType = "Internal"; + cleanAddress = forwardingAddress; + } + } + + return ( + + + + Forwarding Status: + + + {forwardingType === "None" + ? getCippFormatting(false, "ForwardingStatus") + : `${forwardingType} Forwarding` + } + + + {forwardingType !== "None" && ( + <> + + + Keep Copy in Mailbox: + + + {getCippFormatting(forwardAndDeliver, "ForwardAndDeliver")} + + + + + Forwarding Address: + + + {cleanAddress} + + + + )} + + ); + })() } /> + + {/* Archive section - always show status */} - ) : exchangeData?.TotalArchiveItemCount != null ? ( - exchangeData.TotalArchiveItemCount + ) : ( - "N/A" + + + + Archive Mailbox Enabled: + + + {getCippFormatting(exchangeData?.ArchiveMailBox, "ArchiveMailBox")} + + + {exchangeData?.ArchiveMailBox && ( + <> + + + Auto Expanding Archive: + + + {getCippFormatting(exchangeData?.AutoExpandingArchive, "AutoExpandingArchive")} + + + + + Total Archive Item Size: + + + {exchangeData?.TotalArchiveItemSize != null + ? `${exchangeData.TotalArchiveItemSize} GB` + : "N/A"} + + + + + Total Archive Item Count: + + + {exchangeData?.TotalArchiveItemCount != null + ? exchangeData.TotalArchiveItemCount + : "N/A"} + + + + )} + ) } /> - {/* Combine all mailbox hold types into a single PropertyListItem */} + { ) } /> - {/* Combine protocols into a single PropertyListItem */} { ) } /> - - ) : ( - getCippFormatting(exchangeData?.BlockedForSpam, "BlockedForSpam") - ) - } - /> ); diff --git a/src/components/CippCards/CippInfoBar.jsx b/src/components/CippCards/CippInfoBar.jsx index c8889e498c4e..0bfb8c5e62e1 100644 --- a/src/components/CippCards/CippInfoBar.jsx +++ b/src/components/CippCards/CippInfoBar.jsx @@ -95,7 +95,7 @@ export const CippInfoBar = ({ data, isFetching }) => { }} > - + {item?.offcanvas?.propertyItems?.length > 0 && ( { const { diff --git a/src/components/CippCards/CippRemediationCard.jsx b/src/components/CippCards/CippRemediationCard.jsx index d18739c033ca..d864719f80e1 100644 --- a/src/components/CippCards/CippRemediationCard.jsx +++ b/src/components/CippCards/CippRemediationCard.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { Button, Typography, List, ListItem, SvgIcon } from "@mui/material"; import CippButtonCard from "./CippButtonCard"; // Adjust the import path as needed import { CippApiDialog } from "../CippComponents/CippApiDialog"; diff --git a/src/components/CippCards/CippUniversalSearch.jsx b/src/components/CippCards/CippUniversalSearch.jsx index ccba59600fc0..589ac2c117bf 100644 --- a/src/components/CippCards/CippUniversalSearch.jsx +++ b/src/components/CippCards/CippUniversalSearch.jsx @@ -34,33 +34,28 @@ export const CippUniversalSearch = React.forwardRef( }; return ( - - - - - + + - {search.isFetching && ( - - - - )} - {search.isSuccess && search?.data?.length > 0 ? ( - - ) : ( - search.isSuccess && "No results found." - )} + {search.isFetching && ( + + - - + )} + {search.isSuccess && search?.data?.length > 0 ? ( + + ) : ( + search.isSuccess && "No results found." + )} + ); } ); @@ -97,7 +92,7 @@ const Results = ({ items = [], searchValue }) => { {displayedResults.map((item, key) => ( - + ))} diff --git a/src/components/CippCards/CippUserInfoCard.jsx b/src/components/CippCards/CippUserInfoCard.jsx index 97e6917e9126..5239c63e2a13 100644 --- a/src/components/CippCards/CippUserInfoCard.jsx +++ b/src/components/CippCards/CippUserInfoCard.jsx @@ -1,163 +1,280 @@ import PropTypes from "prop-types"; -import { Avatar, Card, CardHeader, Divider, Skeleton, Stack } from "@mui/material"; +import { Avatar, Card, CardHeader, Divider, Skeleton, Typography, Alert } from "@mui/material"; +import { AccountCircle } from "@mui/icons-material"; import { PropertyList } from "/src/components/property-list"; import { PropertyListItem } from "/src/components/property-list-item"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { Stack, Grid } from "@mui/system"; export const CippUserInfoCard = (props) => { const { user, tenant, isFetching = false, ...other } = props; + // Helper function to check if a section has any data + const hasWorkInfo = user?.jobTitle || user?.department || user?.manager?.displayName; + const hasAddressInfo = user?.streetAddress || user?.postalCode || user?.city || user?.country || user?.officeLocation; + const hasContactInfo = user?.mobilePhone || (user?.businessPhones && user?.businessPhones.length > 0); + + // Handle image URL - only set if user and tenant exist, otherwise let Avatar fall back to children + const imageUrl = user?.id && tenant ? `/api/ListUserPhoto?TenantFilter=${tenant}&UserId=${user.id}` : undefined; + return ( - - {isFetching ? ( - - ) : ( - - {user?.displayName?.[0] + user?.surname?.[0] || ""} - - )} - - - ) : ( - getCippFormatting(user?.accountEnabled, "accountEnabled") - ) - } - /> - - ) : ( - getCippFormatting(user?.onPremisesSyncEnabled, "onPremisesSyncEnabled") - ) - } - /> + ) : ( - getCippFormatting(user?.displayName, "displayName") + + {/* Avatar section */} + + + + + + + + + {/* Status information section */} + + + + + Account Enabled: + + + {getCippFormatting(user?.accountEnabled, "accountEnabled")} + + + + + + Synced from AD: + + + {getCippFormatting(user?.onPremisesSyncEnabled, "onPremisesSyncEnabled")} + + + + + ) } /> + + {/* Basic Identity Information */} + ) : ( - getCippFormatting(user?.userPrincipalName, "userPrincipalName") + + + + Display Name: + + + {getCippFormatting(user?.displayName, "displayName") || "N/A"} + + + + + Email Address: + + + {getCippFormatting(user?.proxyAddresses, "proxyAddresses") || "N/A"} + + + + + User Principal Name: + + + {getCippFormatting(user?.userPrincipalName, "userPrincipalName") || "N/A"} + + + ) } /> + + {/* Licenses */} + ) : !user?.assignedLicenses || user?.assignedLicenses.length === 0 ? ( + + No licenses assigned to this user + ) : ( getCippFormatting(user?.assignedLicenses, "assignedLicenses") ) } /> + + {/* Work Information Section */} : user?.id || "N/A"} - /> - + + ) : !hasWorkInfo ? ( + + No work information available + ) : ( - getCippFormatting(user?.proxyAddresses, "proxyAddresses") + + {user?.jobTitle && ( + + + Job Title: + + + {user.jobTitle} + + + )} + {user?.department && ( + + + Department: + + + {user.department} + + + )} + {user?.manager?.displayName && ( + + + Manager: + + + {user.manager.displayName} + + + )} + ) } /> - : user?.jobTitle || "N/A"} - /> - : user?.department || "N/A"} - /> + + {/* Contact Information Section */} : user?.manager?.displayName || "N/A"} - /> - : user?.streetAddress || "N/A" - } - /> - : user?.postalCode || "N/A"} - /> - : user?.city || "N/A"} - /> - : user?.country || "N/A"} - /> - : user?.officeLocation || "N/A" + isFetching ? ( + + ) : !hasContactInfo ? ( + + No contact information available + + ) : ( + + {user?.mobilePhone && ( + + + Mobile Phone: + + + {user.mobilePhone} + + + )} + {user?.businessPhones && user.businessPhones.length > 0 && ( + + + Business Phones: + + + {user.businessPhones.join(", ")} + + + )} + + ) } /> + + {/* Address Information Section */} : user?.mobilePhone || "N/A"} - /> - + + ) : !hasAddressInfo ? ( + + No address information available + ) : ( - user?.businessPhones?.join(", ") || "N/A" + + {user?.streetAddress && ( + + + Street Address: + + + {user.streetAddress} + + + )} + {user?.city && ( + + + City: + + + {user.city} + + + )} + {user?.postalCode && ( + + + Postal Code: + + + {user.postalCode} + + + )} + {user?.country && ( + + + Country: + + + {user.country} + + + )} + {user?.officeLocation && ( + + + Office Location: + + + {user.officeLocation} + + + )} + ) } /> @@ -168,5 +285,6 @@ export const CippUserInfoCard = (props) => { CippUserInfoCard.propTypes = { user: PropTypes.object, + tenant: PropTypes.string, isFetching: PropTypes.bool, }; diff --git a/src/components/CippComponents/AppApprovalTemplateForm.jsx b/src/components/CippComponents/AppApprovalTemplateForm.jsx index 4efe18a1ecbb..7ddedee2c19e 100644 --- a/src/components/CippComponents/AppApprovalTemplateForm.jsx +++ b/src/components/CippComponents/AppApprovalTemplateForm.jsx @@ -1,6 +1,5 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Alert, Skeleton, Stack, Typography, Button, Box } from "@mui/material"; -import { ApiGetCall } from "/src/api/ApiCall"; import { CippFormComponent } from "./CippFormComponent"; import { CippApiResults } from "./CippApiResults"; import { Grid } from "@mui/system"; diff --git a/src/components/CippComponents/BPASyncDialog.jsx b/src/components/CippComponents/BPASyncDialog.jsx index 014e033747e6..43a16450416a 100644 --- a/src/components/CippComponents/BPASyncDialog.jsx +++ b/src/components/CippComponents/BPASyncDialog.jsx @@ -1,14 +1,12 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { Dialog, DialogContent, DialogTitle, Button, DialogActions, - Alert, - CircularProgress, } from "@mui/material"; -import { CheckCircle, Error, Sync } from "@mui/icons-material"; +import { Sync } from "@mui/icons-material"; import { useForm, FormProvider } from "react-hook-form"; import { CippFormTenantSelector } from "./CippFormTenantSelector"; import { ApiPostCall } from "/src/api/ApiCall"; diff --git a/src/components/CippComponents/CIPPDeviceCodeButton.js b/src/components/CippComponents/CIPPDeviceCodeButton.js index e262b69c7912..16711f8d1332 100644 --- a/src/components/CippComponents/CIPPDeviceCodeButton.js +++ b/src/components/CippComponents/CIPPDeviceCodeButton.js @@ -2,12 +2,11 @@ import { useState, useEffect } from "react"; import { Alert, Button, - Stack, Typography, CircularProgress, Box, } from "@mui/material"; -import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; +import { ApiGetCall } from "../../api/ApiCall"; /** * CIPPDeviceCodeButton - A button component for Microsoft 365 OAuth authentication using device code flow diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 001d00d167b0..33ce316d5cfa 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -27,6 +27,7 @@ export const CippApiDialog = (props) => { dialogAfterEffect, allowResubmit = false, children, + defaultvalues, ...other } = props; const router = useRouter(); @@ -38,12 +39,17 @@ export const CippApiDialog = (props) => { if (mdDown) { other.fullScreen = true; } + + const formHook = useForm({ + defaultValues: defaultvalues || {} + }); + useEffect(() => { if (createDialog.open) { setIsFormSubmitted(false); - formHook.reset(); + formHook.reset(defaultvalues || {}); } - }, [createDialog.open]); + }, [createDialog.open, defaultvalues]); const [getRequestInfo, setGetRequestInfo] = useState({ url: "", @@ -193,7 +199,6 @@ export const CippApiDialog = (props) => { } }, [actionPostRequest.isSuccess, actionGetRequest.isSuccess]); - const formHook = useForm(); const onSubmit = (data) => handleActionClick(row, api, data); const selectedType = api.type === "POST" ? actionPostRequest : actionGetRequest; diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx index 3a9dd724b6f1..bc0d12572885 100644 --- a/src/components/CippComponents/CippApiResults.jsx +++ b/src/components/CippComponents/CippApiResults.jsx @@ -20,7 +20,6 @@ import React from "react"; import { CippTableDialog } from "./CippTableDialog"; import { EyeIcon } from "@heroicons/react/24/outline"; import { useDialog } from "../../hooks/use-dialog"; -import { useRouter } from "next/router"; const extractAllResults = (data) => { const results = []; diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx index b0a7a4ded9bc..3e489d043304 100644 --- a/src/components/CippComponents/CippAppPermissionBuilder.jsx +++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx @@ -70,7 +70,18 @@ const CippAppPermissionBuilder = ({ setExpanded(newExpanded ? panel : false); }; + const deprecatedServicePrincipals = [ + "00000002-0000-0000-c000-000000000000", // Windows Azure Active Directory + "a0c73c16-a7e3-4564-9a95-2bdf47383716", // Microsoft Exchange Online Remote PowerShell + "1b730954-1685-4b74-9bfd-dac224a7b894", // Azure Active Directory PowerShell + ]; + const currentSelectedSp = useWatch({ control: formControl.control, name: "servicePrincipal" }); + + // Check if selected service principal is in the deprecated list + const isDeprecatedSp = + currentSelectedSp && deprecatedServicePrincipals.includes(currentSelectedSp.value); + const { data: servicePrincipals = [], isSuccess: spSuccess, @@ -544,115 +555,50 @@ const CippAppPermissionBuilder = ({ return ( <> - {spInfoFetching && } - {servicePrincipal && spInfoSuccess && !spInfoFetching && ( - <> - - Manage the permissions for the {servicePrincipal.displayName}. - + + Manage the permissions for the {servicePrincipal.displayName}. + - - - - - - - - - {servicePrincipal?.appRoles?.length > 0 ? ( - <> - - - - !appTable?.find((perm) => perm.id === role.id)) - .map((role) => ({ - label: role.value, - value: role.id, - }))} - formControl={formControl} - multiple={false} - /> - - - -
- handleAddRow("applicationPermissions", currentAppPermission) - } - > - -
-
-
-
- , - noConfirm: true, - customFunction: (row) => handleRemoveRow("applicationPermissions", row), - }, - ]} - /> -
- - ) : ( - } sx={{ mb: 3 }}> - No Application Permissions found. - - )} -
- + + + + + + + + + {servicePrincipal?.appRoles?.length > 0 ? ( + <> - {spInfo?.Results?.publishedPermissionScopes?.length === 0 && ( - }> - No Published Delegated Permissions found. - - )} - + !delegatedTable?.find((perm) => perm.id === scope.id)) - .map((scope) => ({ - label: scope.value, - value: scope.id, + options={(spInfo?.Results?.appRoles || []) + .filter((role) => !appTable?.find((perm) => perm.id === role.id)) + .map((role) => ({ + label: role.value, + value: role.id, }))} formControl={formControl} multiple={false} /> - +
- handleAddRow("delegatedPermissions", currentDelegatedPermission) + handleAddRow("applicationPermissions", currentAppPermission) } > - +
+
+
+
- -
- - )} + , + noConfirm: true, + customFunction: (row) => handleRemoveRow("delegatedPermissions", row), + }, + ]} + isFetching={spInfoFetching} + /> + +
+ + +
); }; @@ -737,24 +745,33 @@ const CippAppPermissionBuilder = ({
- + - +
{ - setSelectedApp([ - ...selectedApp, - servicePrincipals?.Results?.find( - (sp) => sp.appId === currentSelectedSp.value - ), - ]); - formControl.setValue("servicePrincipal", null); + // Only add if not deprecated + if (!isDeprecatedSp) { + setSelectedApp([ + ...selectedApp, + servicePrincipals?.Results?.find( + (sp) => sp.appId === currentSelectedSp.value + ), + ]); + formControl.setValue("servicePrincipal", null); + } }} > + + + ); +}; + +export default CippForwardingSection; diff --git a/src/components/CippComponents/CippGeoLocation.jsx b/src/components/CippComponents/CippGeoLocation.jsx index 633ce3ee60cd..7a1609a2bedf 100644 --- a/src/components/CippComponents/CippGeoLocation.jsx +++ b/src/components/CippComponents/CippGeoLocation.jsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from "react"; -import { Card, CardContent, CardHeader, Skeleton } from "@mui/material"; +import { useEffect, useState } from "react"; +import { Skeleton } from "@mui/material"; import { Grid } from "@mui/system"; import dynamic from "next/dynamic"; import { ApiPostCall } from "/src/api/ApiCall"; @@ -61,7 +61,7 @@ export default function CippGeoLocation({ ipAddress, cardProps }) { return ( - + {geoLookup.isPending ? ( ) : ( @@ -78,7 +78,7 @@ export default function CippGeoLocation({ ipAddress, cardProps }) { )} - + { +const CippMailboxPermissionsDialog = ({ formHook, combinedOptions, isUserGroupLoading }) => { const fullAccess = useWatch({ control: formHook.control, name: "permissions.AddFullAccess", }); - const userSettingsDefaults = useSettings(); - - const usersList = ApiGetCall({ - url: "/api/ListGraphRequest", - data: { - Endpoint: `users`, - tenantFilter: userSettingsDefaults.currentTenant, - $select: "id,displayName,userPrincipalName,mail", - noPagination: true, - $top: 999, - }, - queryKey: `UserNames-${userSettingsDefaults.currentTenant}`, - }); - return ( @@ -32,38 +16,30 @@ const CippMailboxPermissionsDialog = ({ formHook }) => { label="Add Full Access" name="permissions.AddFullAccess" formControl={formHook} - isFetching={usersList.isFetching} - options={ - usersList?.data?.Results?.map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } + isFetching={isUserGroupLoading} + creatable={false} + options={combinedOptions} + /> + + + - {fullAccess?.length > 0 && ( - - - - )} ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } + isFetching={isUserGroupLoading} + creatable={false} + options={combinedOptions} /> @@ -72,13 +48,9 @@ const CippMailboxPermissionsDialog = ({ formHook }) => { label="Add Send On Behalf Permissions" name="permissions.AddSendOnBehalf" formControl={formHook} - isFetching={usersList.isFetching} - options={ - usersList?.data?.Results?.map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } + isFetching={isUserGroupLoading} + creatable={false} + options={combinedOptions} /> diff --git a/src/components/CippComponents/CippMessageViewer.jsx b/src/components/CippComponents/CippMessageViewer.jsx index fb9d68c338f9..366c17dcc507 100644 --- a/src/components/CippComponents/CippMessageViewer.jsx +++ b/src/components/CippComponents/CippMessageViewer.jsx @@ -333,7 +333,7 @@ export const CippMessageViewer = ({ emailSource }) => { justifyContent: "space-between", }} > - + @@ -417,7 +417,7 @@ export const CippMessageViewer = ({ emailSource }) => {
)}
- + {emlContent.date && isValidDate(emlContent.date) @@ -436,7 +436,7 @@ export const CippMessageViewer = ({ emailSource }) => { {emlContent.attachments && emlContent.attachments.length > 0 && ( - + {emlContent?.attachments?.map((attachment, index) => ( @@ -484,7 +484,7 @@ export const CippMessageViewer = ({ emailSource }) => { {(emlContent?.text || emlContent?.html) && ( - + {messageHtml ? ( {emailStyle} diff --git a/src/components/CippComponents/CippNotificationForm.jsx b/src/components/CippComponents/CippNotificationForm.jsx index 0eef55732c76..bd7a5ce3887c 100644 --- a/src/components/CippComponents/CippNotificationForm.jsx +++ b/src/components/CippComponents/CippNotificationForm.jsx @@ -74,7 +74,7 @@ export const CippNotificationForm = ({ <> - + - + - + - + - + {showTestButton && ( - + diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index 6031f6497a27..caa30e6b3036 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -95,7 +95,7 @@ export const CippOffCanvas = (props) => { sx={{ overflowY: "auto", maxHeight: "100%", display: "flex", flexDirection: "column" }} > - + {extendedInfo.length > 0 && ( { /> )} - + {typeof children === "function" ? children(extendedData) : children} diff --git a/src/components/CippComponents/CippPermissionPreview.jsx b/src/components/CippComponents/CippPermissionPreview.jsx index 9f2afeb69e06..6347fff47c6e 100644 --- a/src/components/CippComponents/CippPermissionPreview.jsx +++ b/src/components/CippComponents/CippPermissionPreview.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Alert, Skeleton, @@ -9,13 +9,11 @@ import { List, ListItem, ListItemText, - Divider, Tab, Tabs, Chip, SvgIcon, } from "@mui/material"; -import { ApiGetCall } from "/src/api/ApiCall"; import { ShieldCheckIcon } from "@heroicons/react/24/outline"; import { CippCardTabPanel } from "./CippCardTabPanel"; diff --git a/src/components/CippComponents/CippSettingsSideBar.jsx b/src/components/CippComponents/CippSettingsSideBar.jsx index 3a3c998c7c38..563983ba6e10 100644 --- a/src/components/CippComponents/CippSettingsSideBar.jsx +++ b/src/components/CippComponents/CippSettingsSideBar.jsx @@ -26,6 +26,7 @@ export const CippSettingsSideBar = (props) => { const saveSettingsPost = ApiPostCall({ url: "/api/ExecUserSettings", + relatedQueryKeys: "userSettings", }); const handleSaveChanges = () => { const shippedValues = { diff --git a/src/components/CippComponents/CippTablePage.jsx b/src/components/CippComponents/CippTablePage.jsx index eb75f6cc1ad1..5ab4223b933d 100644 --- a/src/components/CippComponents/CippTablePage.jsx +++ b/src/components/CippComponents/CippTablePage.jsx @@ -1,6 +1,5 @@ import { Alert, Card, Divider } from "@mui/material"; import { Box, Container, Stack } from "@mui/system"; -import Head from "next/head"; import { CippDataTable } from "../CippTable/CippDataTable"; import { useSettings } from "../../hooks/use-settings"; import { CippHead } from "./CippHead"; diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 5f6c190e474b..28f4f654b6b9 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -52,12 +52,30 @@ export const CippUserActions = () => { }, { //tested - label: "Create Temporary Access Password", type: "POST", icon: , url: "/api/ExecCreateTAP", data: { ID: "userPrincipalName" }, + fields: [ + { + type: "number", + name: "lifetimeInMinutes", + label: "Lifetime (Minutes)", + placeholder: "Leave blank for default" + }, + { + type: "switch", + name: "isUsableOnce", + label: "One-time use only" + }, + { + type: "datePicker", + name: "startDateTime", + label: "Start Date/Time (leave blank for immediate)", + dateTimeType: "datetime" + } + ], confirmText: "Are you sure you want to create a Temporary Access Password?", multiPost: false, }, @@ -167,7 +185,33 @@ export const CippUserActions = () => { type: "POST", icon: , url: "/api/EditGroup", - data: { addMember: "userPrincipalName" }, + customDataformatter: (row, action, formData) => { + let addMember = []; + if (Array.isArray(row)) { + row + .map((r) => ({ + label: r.displayName, + value: r.userPrincipalName, + addedFields: { + id: r.id, + }, + })) + .forEach((r) => addMember.push(r)); + } else { + addMember.push({ + label: row.displayName, + value: row.userPrincipalName, + addedFields: { + id: row.id, + }, + }); + } + return { + addMember: addMember, + tenantFilter: tenant, + groupId: formData.groupId, + }; + }, fields: [ { type: "autoComplete", @@ -184,10 +228,53 @@ export const CippUserActions = () => { groupName: "displayName", }, queryKey: `groups-${tenant}`, + showRefresh: true, }, }, ], confirmText: "Are you sure you want to add the user to this group?", + multiPost: true, + }, + { + label: "Manage Licenses", + type: "POST", + url: "/api/ExecBulkLicense", + icon: , + data: { userIds: "id" }, + multiPost: true, + fields: [ + { + type: "radio", + name: "LicenseOperation", + label: "License Operation", + options: [ + { label: "Add Licenses", value: "Add" }, + { label: "Remove Licenses", value: "Remove" }, + { label: "Replace Licenses", value: "Replace" }, + ], + required: true, + }, + { + type: "switch", + name: "RemoveAllLicenses", + label: "Remove All Existing Licenses", + }, + { + type: "autoComplete", + name: "Licenses", + label: "Select Licenses", + multiple: true, + creatable: false, + api: { + url: "/api/ListLicenses", + labelField: "skuPartNumber", + valueField: "skuId", + queryKey: `licenses-${tenant}`, + }, + }, + ], + confirmText: "Are you sure you want to manage licenses for the selected users?", + multiPost: true, }, { label: "Disable Email Forwarding", @@ -226,7 +313,7 @@ export const CippUserActions = () => { name: "siteUrl", label: "Select a Site", multiple: false, - creatable: false, + creatable: true, api: { url: "/api/ListSites", data: { type: "SharePointSiteUsage", URLOnly: true }, diff --git a/src/components/CippComponents/DomainAnalyserDialog.jsx b/src/components/CippComponents/DomainAnalyserDialog.jsx new file mode 100644 index 000000000000..803baf43f154 --- /dev/null +++ b/src/components/CippComponents/DomainAnalyserDialog.jsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import { Dialog, DialogContent, DialogTitle, Button, DialogActions } from "@mui/material"; +import { Refresh } from "@mui/icons-material"; +import { useForm, FormProvider } from "react-hook-form"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "./CippApiResults"; + +export const DomainAnalyserDialog = ({ createDialog }) => { + const methods = useForm({ + defaultValues: { + tenantFilter: { + value: "AllTenants", + label: "*All Tenants", + }, + }, + }); + + // Use methods for form handling and control + const { handleSubmit, control } = methods; + + const [isRunning, setIsRunning] = useState(false); + const domainAnalyserResults = ApiPostCall({ + urlFromData: true, + }); + + const handleForm = (values) => { + setIsRunning(true); + domainAnalyserResults.mutate({ + url: "/api/ExecDomainAnalyser", + queryKey: `domain-analyser-${values.tenantFilter}`, + data: values.tenantFilter ? { TenantFilter: values.tenantFilter } : {}, + }); + }; + + // Reset running state when dialog is closed + const handleClose = () => { + setIsRunning(false); + createDialog.handleClose(); + }; + + return ( + + + + Run Domain Analysis + +
+

+ This will run a Domain Analysis to check for DNS configuration issues. Select a + tenant (or all tenants) below. +

+ +
+ +
+ + + + + +
+
+ ); +}; diff --git a/src/components/CippFormPages/CippAddEditContact.jsx b/src/components/CippFormPages/CippAddEditContact.jsx new file mode 100644 index 000000000000..cbc96616d37c --- /dev/null +++ b/src/components/CippFormPages/CippAddEditContact.jsx @@ -0,0 +1,194 @@ +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { getCippValidator } from "/src/utils/get-cipp-validator"; +import countryList from "/src/data/countryList.json"; + +const countryOptions = countryList.map(({ Code, Name }) => ({ + label: Name, + value: Code, +})); + +const ContactFormLayout = ({ formControl, formType = "add" }) => { + return ( + + {/* Display Name */} + + + + + {/* First Name and Last Name */} + + + + + + + + + + {/* Email */} + + getCippValidator(value, "email"), + }} + /> + + + {/* Hide from GAL */} + + + + + + + {/* Company Information */} + + + + + + + + {/* Website */} + + !value || getCippValidator(value, "url"), + }} + /> + + + + + {/* Address Information */} + + + + + + + + + + + + + + + + + + + {/* Phone Numbers */} + + + + + + + + + + {/* Mail Tip */} + + + + + ); +}; + +export default ContactFormLayout; diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index e86eaec59b74..e1b42686c2ee 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -9,7 +9,7 @@ import { Grid } from "@mui/system"; import { ApiGetCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; import { useWatch } from "react-hook-form"; -import { use, useEffect, useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { useRouter } from "next/router"; const CippAddEditUser = (props) => { @@ -64,7 +64,7 @@ const CippAddEditUser = (props) => { return ( - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + - + { /> - + Settings - + { compareType="is" compareValue={true} > - + { - + { formControl={formControl} /> - + { formControl={formControl} /> - + {integrationSettings?.data?.Sherweb?.Enabled === true && ( @@ -184,7 +184,7 @@ const CippAddEditUser = (props) => { compareValue="(0 available)" labelCompare={true} > - + { compareType="is" compareValue={true} > - + This will Purchase a new Sherweb License for the user, according to the terms and conditions with Sherweb. When the license becomes available, CIPP will assign the license to this user. - + { )} - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { {userSettingsDefaults?.userAttributes ?.filter((attribute) => attribute.value !== "sponsor") .map((attribute, idx) => ( - + { ))} {/* Set Manager */} - + { /> {userSettingsDefaults?.userAttributes?.some((attribute) => attribute.value === "sponsor") && ( - + { /> )} - + { /> {formType === "edit" && ( - + { )} {formType === "edit" && ( - + { )} {/* Schedule User Creation */} {formType === "add" && ( - + { compareType="is" compareValue={true} > - + { formControl={formControl} /> - + ( + + + +); + const CippAddGroupForm = (props) => { const { formControl } = props; return ( - + { fullWidth /> - + { fullWidth /> - + { }} /> - + { /> - + { /> - + { select={"id,userPrincipalName,displayName"} /> - + { compareType="is" compareValue="distribution" > - + { compareType="contains" compareValue="dynamic" > - + { {/* Hidden field to store the template GUID when editing */} - + { fullWidth /> - + { fullWidth /> - + { /> - + { compareType="is" compareValue="distribution" > - + { compareType="contains" compareValue="dynamic" > - + { return ( - + @@ -178,7 +178,7 @@ const CippCustomDataMappingForm = ({ formControl }) => { - + {selectedExtensionSyncDataset && ( { const userSettingsDefaults = useSettings(); - const { formControl, currentSettings, userId, calPermissions, isFetching } = props; + const { formControl, currentSettings, userId, calPermissions, isFetching, oooRequest } = props; // State to manage the expanded panels const [expandedPanel, setExpandedPanel] = useState(null); const [relatedQueryKeys, setRelatedQueryKeys] = useState([]); @@ -37,22 +36,6 @@ const CippExchangeSettingsForm = (props) => { // Calculate if date fields should be disabled const areDateFieldsDisabled = autoReplyState?.value !== "Scheduled"; - - useEffect(() => { - console.log('Auto Reply State changed:', { - autoReplyState, - areDateFieldsDisabled, - fullFormValues: formControl.getValues() - }); - }, [autoReplyState]); - - // Add debug logging for form values - useEffect(() => { - const subscription = formControl.watch((value, { name, type }) => { - console.log('Form value changed:', { name, type, value }); - }); - return () => subscription.unsubscribe(); - }, [formControl]); const handleExpand = (panel) => { setExpandedPanel((prev) => (prev === panel ? null : panel)); @@ -64,17 +47,52 @@ const CippExchangeSettingsForm = (props) => { Endpoint: `users`, tenantFilter: userSettingsDefaults.currentTenant, $select: "id,displayName,userPrincipalName,mail", - noPagination: true, $top: 999, }, queryKey: `UserNames-${userSettingsDefaults.currentTenant}`, }); + const contactsList = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `contacts`, + tenantFilter: userSettingsDefaults.currentTenant, + $select: "displayName,mail,mailNickname", + $top: 999, + }, + queryKey: `TenantContacts-${userSettingsDefaults.currentTenant}`, + }); + const postRequest = ApiPostCall({ datafromUrl: true, relatedQueryKeys: relatedQueryKeys, }); + // Handle form reset and set dropdown state after successful API calls + useEffect(() => { + if (postRequest.isSuccess) { + // If this was an OOO submission, preserve the submitted values + if (relatedQueryKeys.includes(`ooo-${userId}`)) { + const submittedValues = formControl.getValues(); + const oooFields = ['AutoReplyState', 'InternalMessage', 'ExternalMessage', 'StartTime', 'EndTime']; + + // Reset the form + formControl.reset(); + + // Restore the submitted OOO values + oooFields.forEach(field => { + const value = submittedValues.ooo?.[field]; + if (value !== undefined) { + formControl.setValue(`ooo.${field}`, value); + } + }); + } else { + // For non-OOO submissions, just reset normally + formControl.reset(); + } + } + }, [postRequest.isSuccess, relatedQueryKeys, userId, formControl]); + const handleSubmit = (type) => { if (type === "calendar") { setRelatedQueryKeys([`CalendarPermissions-${userId}`]); @@ -117,105 +135,53 @@ const CippExchangeSettingsForm = (props) => { data: data, queryKey: "MailboxPermissions", }); - - // Reset the form - formControl.reset(); }; // Data for each section const sections = [ { id: "mailboxForwarding", - cardLabelBox: currentSettings?.ForwardAndDeliver ? : "-", + cardLabelBox: { + cardLabelBoxHeader: isFetching ? ( + + ) : (currentSettings?.ForwardingAddress) ? ( + + ) : ( + + ), + }, text: "Mailbox Forwarding", - subtext: "Configure email forwarding options", + subtext: (currentSettings?.ForwardingAddress) + ? "Email forwarding is configured for this mailbox" + : "No email forwarding configured for this mailbox", formContent: ( - - - - - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - - - - - - - - - - - - - + ), }, { id: "outOfOffice", - cardLabelBox: "OOO", + cardLabelBox: { + cardLabelBoxHeader: OOO, + }, text: "Out Of Office", subtext: "Set automatic replies for when you are away", formContent: ( - + { ]} /> - + {
- + { - + { rows={4} /> - + { rows={4} /> - + @@ -293,22 +259,29 @@ const CippExchangeSettingsForm = (props) => { }, { id: "recipientLimits", - cardLabelBox: "RL", + cardLabelBox: { + cardLabelBoxHeader: RL, + }, text: "Recipient Limits", subtext: "Set the maximum number of recipients per message", formContent: ( - + - + @@ -354,14 +327,14 @@ const CippExchangeSettingsForm = (props) => { sx={{ alignItems: "center", borderRadius: 1, - color: "primary.contrastText", + color: "text.secondary", display: "flex", height: 40, justifyContent: "center", width: 40, }} > - {section.cardLabelBox} + {section.cardLabelBox.cardLabelBoxHeader} {/* Main Text and Subtext */} diff --git a/src/components/CippFormPages/CippFormPage.jsx b/src/components/CippFormPages/CippFormPage.jsx index 448eb0604498..2a85e6362ee9 100644 --- a/src/components/CippFormPages/CippFormPage.jsx +++ b/src/components/CippFormPages/CippFormPage.jsx @@ -21,6 +21,7 @@ const CippFormPage = (props) => { const { title, backButtonTitle, + titleButton, formPageType = "Add", children, queryKey, @@ -118,11 +119,14 @@ const CippFormPage = (props) => { )} -
+
{!hidePageType && <>{formPageType} - } {title} + {titleButton && titleButton}
)} diff --git a/src/components/CippFormPages/CippFormSkeleton.jsx b/src/components/CippFormPages/CippFormSkeleton.jsx index 77a04daf9b0d..5fba2ef46370 100644 --- a/src/components/CippFormPages/CippFormSkeleton.jsx +++ b/src/components/CippFormPages/CippFormSkeleton.jsx @@ -8,7 +8,7 @@ const CippFormSkeleton = ({ layout }) => { {layout.map((columns, rowIndex) => ( {Array.from({ length: columns }).map((_, columnIndex) => ( - + ))} diff --git a/src/components/CippFormPages/CippInviteGuest.jsx b/src/components/CippFormPages/CippInviteGuest.jsx index 6e3f597fcbfb..7d62d071da0b 100644 --- a/src/components/CippFormPages/CippInviteGuest.jsx +++ b/src/components/CippFormPages/CippInviteGuest.jsx @@ -6,7 +6,7 @@ const CippInviteUser = (props) => { return ( - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { + if (!value || value === '') return []; + if (typeof value === 'string') { + return value.split(',').map(item => item.trim()).filter(item => item !== ''); + } + return value; + }, + + // Process domain fields - handle both string and object values + processDomainField: (field) => { + if (!field) return []; + + if (typeof field === 'string') { + // Handle comma-separated string + return safeLinksDataUtils.formatStringToArray(field); + } else if (Array.isArray(field)) { + // If already an array of strings, return it + if (field.length > 0 && typeof field[0] === 'string') { + return field; + } + // If an array of objects from the domain selector, extract the ids + return field.map(item => item.id || item); + } + return []; + }, + + // Process group fields if they're returned as objects + processGroupField: (field) => { + if (Array.isArray(field)) { + // If the field is already an array of IDs, return it + if (field.length > 0 && typeof field[0] === 'string') { + return field; + } + // If the field is an array of objects, extract the IDs + return field.map(item => item.id || item); + } + return []; + }, + + // Create custom data formatter for different form types + createDataFormatter: (formControl, formType = 'add', additionalFields = {}) => { + return (values) => { + const ruleValues = formControl.getValues(); + + // Base data structure + const baseData = { + // Common fields + PolicyName: values.PolicyName, + tenantFilter: values.tenantFilter, + + // Policy fields + EnableSafeLinksForEmail: values.EnableSafeLinksForEmail, + EnableSafeLinksForTeams: values.EnableSafeLinksForTeams, + EnableSafeLinksForOffice: values.EnableSafeLinksForOffice, + TrackClicks: values.TrackClicks, + AllowClickThrough: values.AllowClickThrough, + ScanUrls: values.ScanUrls, + EnableForInternalSenders: values.EnableForInternalSenders, + DeliverMessageAfterScan: values.DeliverMessageAfterScan, + DisableUrlRewrite: values.DisableUrlRewrite, + DoNotRewriteUrls: Array.isArray(values.DoNotRewriteUrls) ? values.DoNotRewriteUrls : [], + AdminDisplayName: values.AdminDisplayName, + CustomNotificationText: values.CustomNotificationText, + EnableOrganizationBranding: values.EnableOrganizationBranding, + + // Rule fields + RuleName: ruleValues.RuleName, + Priority: ruleValues.Priority, + Comments: ruleValues.Comments, + + // Process user, group and domain fields + SentTo: ruleValues.SentTo, + ExceptIfSentTo: ruleValues.ExceptIfSentTo, + SentToMemberOf: safeLinksDataUtils.processGroupField(ruleValues.SentToMemberOf), + ExceptIfSentToMemberOf: safeLinksDataUtils.processGroupField(ruleValues.ExceptIfSentToMemberOf), + RecipientDomainIs: safeLinksDataUtils.processDomainField(ruleValues.RecipientDomainIs), + ExceptIfRecipientDomainIs: safeLinksDataUtils.processDomainField(ruleValues.ExceptIfRecipientDomainIs), + }; + + // Add form-specific fields + switch (formType) { + case 'add': + return { + ...baseData, + State: ruleValues.State, + }; + + case 'edit': + return { + ...baseData, + State: ruleValues.State, + }; + + case 'template': + return { + ...baseData, + ID: additionalFields.ID, + TemplateName: values.TemplateName, + TemplateDescription: values.TemplateDescription, + State: ruleValues.State ? "Enabled" : "Disabled", + }; + + case 'createTemplate': + return { + ...baseData, + TemplateName: values.TemplateName, + TemplateDescription: values.TemplateDescription, + // If no policy description provided, use template description as fallback + AdminDisplayName: values.AdminDisplayName || values.Description, + State: ruleValues.State, + }; + + default: + return baseData; + } + }; + }, + + // Helper to populate form with existing data + populateFormData: (formControl, data, userSettingsDefaults, formType = 'edit' ) => { + const baseData = { + tenantFilter: userSettingsDefaults.currentTenant, + PolicyName: data.PolicyName, + EnableSafeLinksForEmail: data.EnableSafeLinksForEmail, + EnableSafeLinksForTeams: data.EnableSafeLinksForTeams, + EnableSafeLinksForOffice: data.EnableSafeLinksForOffice, + TrackClicks: data.TrackClicks, + AllowClickThrough: data.AllowClickThrough, + ScanUrls: data.ScanUrls, + EnableForInternalSenders: data.EnableForInternalSenders, + DeliverMessageAfterScan: data.DeliverMessageAfterScan, + DisableUrlRewrite: data.DisableUrlRewrite, + DoNotRewriteUrls: data.DoNotRewriteUrls, + AdminDisplayName: data.AdminDisplayName, + CustomNotificationText: data.CustomNotificationText, + EnableOrganizationBranding: data.EnableOrganizationBranding, + RuleName: data.RuleName, + Priority: data.Priority, + Comments: data.Comments, + State: data.State, + SentTo: data.SentTo || [], + ExceptIfSentTo: data.ExceptIfSentTo || [], + SentToMemberOf: data.SentToMemberOf || [], + ExceptIfSentToMemberOf: data.ExceptIfSentToMemberOf || [], + RecipientDomainIs: data.RecipientDomainIs || [], + ExceptIfRecipientDomainIs: data.ExceptIfRecipientDomainIs || [], + }; + + // Add template-specific fields + if (formType === 'template') { + baseData.TemplateName = data.TemplateName; + baseData.TemplateDescription = data.TemplateDescription; + } + + formControl.reset(baseData); + }, +}; + +export const SafeLinksForm = ({ formControl, formType = "add" }) => { + const { watch, setError, clearErrors } = formControl; + const doNotRewriteUrls = watch("DoNotRewriteUrls"); + const policyName = watch("PolicyName"); + const [isUrlsValid, setIsUrlsValid] = useState(true); + const userSettingsDefaults = useSettings(); + + // Fetch existing policies for name validation (only for add/createTemplate forms) + const shouldFetchPolicies = formType === "add" || formType === "createTemplate"; + const existingPolicies = ApiGetCall({ + url: `/api/ListSafeLinksPolicy?tenantFilter=${userSettingsDefaults.currentTenant}`, + queryKey: `SafeLinksPolicy-List-${userSettingsDefaults.currentTenant}`, + enabled: shouldFetchPolicies, + }); + + // Fetch existing templates for name validation (only for createTemplate forms) + const shouldFetchTemplates = formType === "createTemplate"; + const existingTemplates = ApiGetCall({ + url: `/api/ListSafeLinksPolicyTemplates`, + queryKey: `SafeLinksTemplates-List`, + enabled: shouldFetchTemplates, + }); + + // Create validator for checking duplicate policy names + const validatePolicyName = (value) => { + if (!shouldFetchPolicies || !value) return true; + + // If still loading, allow validation to pass (it will re-validate when data loads) + if (existingPolicies.isFetching) return true; + + // If API call failed, allow validation to pass (don't block user due to API issues) + if (existingPolicies.error) return true; + + if (existingPolicies.isSuccess && existingPolicies.data) { + const existingNames = existingPolicies.data.map(policy => policy.PolicyName?.toLowerCase()).filter(Boolean); + if (existingNames.includes(value.toLowerCase())) { + return "A policy with this name already exists"; + } + + const lowerValue = value.toLowerCase(); + if (lowerValue.startsWith("built-in protection policy") || + lowerValue.startsWith("standard preset security policy") || + lowerValue.startsWith("strict preset security policy")) { + return "This name is reserved for built-in policies"; + } + } + return true; + }; + + // Create validator for checking duplicate template names + const validateTemplateName = (value) => { + if (!shouldFetchTemplates || !value) return true; + + // If still loading, allow validation to pass (it will re-validate when data loads) + if (existingTemplates.isFetching) return true; + + // If API call failed, allow validation to pass (don't block user due to API issues) + if (existingTemplates.error) return true; + + if (existingTemplates.isSuccess && existingTemplates.data) { + const existingNames = existingTemplates.data.map(template => template.name?.toLowerCase()).filter(Boolean); + if (existingNames.includes(value.toLowerCase())) { + return "A template with this name already exists"; + } + } + return true; + }; + + // Helper function to validate a URL/domain entry + const validateDoNotRewriteUrl = (entry) => { + if (!entry) return true; + + // For entries with wildcards, use wildcard validators + if (entry.includes('*') || entry.includes('~')) { + const wildcardUrlResult = getCippValidator(entry, "wildcardUrl"); + const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); + + if (wildcardUrlResult !== true && wildcardDomainResult !== true) { + return false; + } + return true; + } + + // For standard entries, check normal validators + const hostnameResult = getCippValidator(entry, "hostname"); + const urlResult = getCippValidator(entry, "url"); + const domainResult = getCippValidator(entry, "domain"); + + if (hostnameResult !== true && urlResult !== true && domainResult !== true) { + return false; + } + + return true; + }; + + // Re-validate policy name when existing policies data changes + useEffect(() => { + if (shouldFetchPolicies && (existingPolicies.isSuccess || existingPolicies.error)) { + formControl.trigger('PolicyName'); + } + }, [existingPolicies.isSuccess, existingPolicies.error, existingPolicies.data, shouldFetchPolicies, formControl]); + + // Re-validate template name when existing templates data changes + useEffect(() => { + if (shouldFetchTemplates && (existingTemplates.isSuccess || existingTemplates.error)) { + formControl.trigger('TemplateName'); + } + }, [existingTemplates.isSuccess, existingTemplates.error, existingTemplates.data, shouldFetchTemplates, formControl]); + + // Validate URLs in useEffect and update the validation Enabled + useEffect(() => { + if (!doNotRewriteUrls || doNotRewriteUrls.length === 0) { + clearErrors("DoNotRewriteUrls"); + setIsUrlsValid(true); + return; + } + + let hasInvalidEntry = false; + + for (const item of doNotRewriteUrls) { + const entry = typeof item === 'string' ? item : (item?.value || item?.label || ''); + if (!entry) continue; + + const isValid = validateDoNotRewriteUrl(entry); + if (!isValid) { + hasInvalidEntry = true; + break; + } + } + + if (hasInvalidEntry) { + setError("DoNotRewriteUrls", { + type: "validate", + message: "Not a valid URL, domain, or pattern" + }); + setIsUrlsValid(false); + } else { + clearErrors("DoNotRewriteUrls"); + setIsUrlsValid(true); + } + }, [doNotRewriteUrls, setError, clearErrors]); + + // Set the rule-related values whenever the policy name changes + useEffect(() => { + if (policyName) { + // Always set SafeLinksPolicy to match the policy name + formControl.setValue('SafeLinksPolicy', policyName); + + // Only auto-generate the rule name for new policies + if (formType === "add" || formType === "createTemplate") { + const ruleName = `${policyName}_Rule`; + formControl.setValue('RuleName', ruleName); + } + } + }, [policyName, formType, formControl]); + + // Show template-specific fields + const showTemplateFields = formType === "template" || formType === "createTemplate"; + + return ( + + {/* Template Fields (if applicable) */} + {showTemplateFields && ( + <> + + Template Information + + + + + + + + + )} + + {/* Policy Settings Section */} + + Safe Links Policy Configuration + + + Policy Settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + isUrlsValid || "Not a valid URL, domain, or pattern" + } + }} + /> + + + {/* Rule Settings Section */} + + Safe Links Rule Configuration + + + Rule Information + + + + + + + + + + + + + + + Applies To: + + + + + + + + + + + + Exceptions: + + + + + + + + + + + + {/* Information Cards */} + + } + label="Propagation Time" + value="Changes to Safe Links policies and rules may take up to 6 hours to propagate throughout your organization." + isFetching={false} + /> + + + ); +}; + +export default SafeLinksForm; \ No newline at end of file diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index 580ad9116d0b..c7492a6e4eec 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { Box, Button, Divider, Skeleton, SvgIcon, Typography } from "@mui/material"; import { Grid } from "@mui/system"; import { useWatch } from "react-hook-form"; @@ -147,7 +146,7 @@ const CippSchedulerForm = (props) => { {(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && ( )} - + { /> - + { /> - + { }} /> - + { }} /> - + { /> {selectedCommand?.addedFields?.Synopsis && ( - + PowerShell Command: @@ -234,11 +233,11 @@ const CippSchedulerForm = (props) => { compareType="isNot" compareValue={true} formControl={formControl} + key={idx} > {param.Type === "System.Boolean" || param.Type === "System.Management.Automation.SwitchParameter" ? ( @@ -279,10 +278,10 @@ const CippSchedulerForm = (props) => { ))} - + - + { compareValue={true} formControl={formControl} > - + { /> - + { ]} /> - + + + + } + > + + + Customize your organization's branding for reports and documents. Changes will be applied + to all generated reports. + + + {/* Logo Upload Section */} + + + Logo + + + + + + {logoPreview && ( + + Logo preview + + )} + + + Recommended: PNG format, max 2MB, optimal size 200x100px + + + + + {/* Color Picker Section */} + + + Brand Color + + + formControl.setValue("colour", e.target.value)} + style={{ + width: "50px", + height: "40px", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + }} + /> + + + + This color will be used for accents and highlights in reports + + + + {/* Preview Section */} + + + Preview + + + {logoPreview && ( + Logo + )} + + + Your Organization + + + Executive Report Preview + + + + + + {/* API Results inside the card */} + + + + ); +}; + +export default CippBrandingSettings; diff --git a/src/components/CippSettings/CippCustomRoles.jsx b/src/components/CippSettings/CippCustomRoles.jsx index 8b5422b15229..aae96ba842bc 100644 --- a/src/components/CippSettings/CippCustomRoles.jsx +++ b/src/components/CippSettings/CippCustomRoles.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { Box, diff --git a/src/components/CippSettings/CippPasswordSettings.jsx b/src/components/CippSettings/CippPasswordSettings.jsx index e8ae0508d511..0f3a8d6f4976 100644 --- a/src/components/CippSettings/CippPasswordSettings.jsx +++ b/src/components/CippSettings/CippPasswordSettings.jsx @@ -26,6 +26,7 @@ const CippPasswordSettings = () => { const passwordTypes = ["Classic", "Correct-Battery-Horse"]; return passwordTypes.map((type) => ( + + + + + } /> - {manualFields && ( - - {fields.map((field) => ( - <> - - { - if (e.key === "Enter") { - if (e.target.value === "") return false; - handleAddItem(); - setTimeout(() => { - formControl.setValue(`addrow.${field}`, ""); - }, 500); - } + + + Manual Import + + + {validationErrors.length > 0 && ( + + + Please fix the following validation errors: + + {validationErrors.map((error, index) => ( + + • {error} + + ))} + + )} + {manualInputs.map((row, rowIndex) => ( + + {/* Row identifier */} + - - - ))} - - - - - )} - {!manualFields && ( - <> - - - - - - setOpen(false)}> - Add a new row - - + > + {rowIndex + 1} + {fields.map((field) => ( - - + { + if (!inputRefs.current[rowIndex]) { + inputRefs.current[rowIndex] = {}; + } + inputRefs.current[rowIndex][field.propertyName] = el; + }} + label={field.friendlyName} + value={row[field.propertyName] || ''} + onChange={(e) => handleManualInputChange(rowIndex, field.propertyName, e.target.value)} + onKeyDown={(e) => field.propertyName === 'productKey' && handleKeyPress(e, rowIndex)} + fullWidth + size="small" /> - + ))} - - - - - + + ))} + + - - - - )} + + + + + + + + - { ) : ( - + {firstHalf.map(([key, value]) => ( { ))} - + {secondHalf.map(([key, value]) => ( { ]; useEffect(() => { if (watcher?.value) { + console.log(watcher); formControl.setValue("groupType", watcher.addedFields.groupType); formControl.setValue("Displayname", watcher.addedFields.Displayname); formControl.setValue("Description", watcher.addedFields.Description); formControl.setValue("username", watcher.addedFields.username); formControl.setValue("allowExternal", watcher.addedFields.allowExternal); - formControl.setValue("MembershipRules", watcher.addedFields.MembershipRules); + formControl.setValue("membershipRules", watcher.addedFields.membershipRules); } }, [watcher]); return ( - + { valueField: "GUID", addedField: { groupType: "groupType", - Displayname: "Displayname", - Description: "Description", + Displayname: "displayName", + Description: "description", username: "username", allowExternal: "allowExternal", - MembershipRules: "MembershipRules", + membershipRules: "membershipRules", }, + showRefresh: true, }} /> - + { validators={{ required: "Please select a group type" }} /> - + { validators={{ required: "Group display name is required" }} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - - + + - - - - + + + + - - - - + + + + - - + + { useEffect(() => { if (disableForwarding) { formControl.setValue("forward", null); - formControl.setValue("keepCopy", false); + formControl.setValue("KeepCopy", false); } }, [disableForwarding, formControl]); return ( - + @@ -133,7 +133,7 @@ export const CippWizardOffboarding = (props) => { - + @@ -155,6 +155,7 @@ export const CippWizardOffboarding = (props) => { dataKey: "Results", labelField: (option) => `${option.displayName} (${option.userPrincipalName})`, valueField: "id", + queryKey: "Offboarding-Users", data: { Endpoint: "users", manualPagination: true, @@ -179,6 +180,7 @@ export const CippWizardOffboarding = (props) => { url: "/api/ListGraphRequest", dataKey: "Results", tenantFilter: currentTenant ? currentTenant.value : undefined, + queryKey: "Offboarding-Users", data: { Endpoint: "users", manualPagination: true, @@ -203,6 +205,7 @@ export const CippWizardOffboarding = (props) => { valueField: "id", url: "/api/ListGraphRequest", dataKey: "Results", + queryKey: "Offboarding-Users", data: { Endpoint: "users", manualPagination: true, @@ -244,6 +247,7 @@ export const CippWizardOffboarding = (props) => { valueField: "id", url: "/api/ListGraphRequest", dataKey: "Results", + queryKey: "Offboarding-Users", data: { Endpoint: "users", manualPagination: true, @@ -256,7 +260,7 @@ export const CippWizardOffboarding = (props) => { /> { - + { compareType="is" compareValue={true} > - + Scheduled Offboarding Date { /> - + Send results to: { return ( {fields.map((field, index) => ( - + { + const currentDate = new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + + const brandColor = brandingSettings?.colour || "#F77F00"; + + // ENTERPRISE DESIGN SYSTEM - JOBS/RAMS/IVE PRINCIPLES + const styles = StyleSheet.create({ + // FOUNDATION - CONSISTENT STATE OWNERSHIP (FLORENCE) + page: { + flexDirection: "column", + backgroundColor: "#FFFFFF", + fontFamily: "Helvetica", + fontSize: 10, + lineHeight: 1.4, + color: "#2D3748", + padding: 40, // Consistent base padding + }, + + // COVER PAGE - PROPORTIONAL & INTENTIONAL (JOBS/RAMS/IVE) + coverPage: { + flexDirection: "column", + backgroundColor: "#FFFFFF", + fontFamily: "Helvetica", + padding: 60, + justifyContent: "space-between", + minHeight: "100%", + }, + + coverHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 80, + }, + + logoSection: { + flexDirection: "row", + alignItems: "center", + }, + + logo: { + height: 100, + marginRight: 12, + }, + + headerLogo: { + height: 30, + }, + + brandName: { + fontSize: 12, + fontWeight: "bold", + color: brandColor, + letterSpacing: 1, + textTransform: "uppercase", + }, + + dateStamp: { + fontSize: 9, + color: "#000000", + textTransform: "uppercase", + letterSpacing: 0.5, + }, + + // MODERN HERO SECTION + coverHero: { + flex: 1, + justifyContent: "flex-start", + alignItems: "flex-start", + paddingTop: 40, + }, + + coverLabel: { + backgroundColor: brandColor, + color: "#FFFFFF", + fontSize: 10, + fontWeight: "bold", + textTransform: "uppercase", + letterSpacing: 1, + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + marginBottom: 30, + alignSelf: "flex-start", + }, + + mainTitle: { + fontSize: 48, + fontWeight: "bold", + color: "#1A202C", + lineHeight: 1.1, + marginBottom: 20, + letterSpacing: -1, + textTransform: "uppercase", + }, + + titleAccent: { + color: brandColor, + }, + + subtitle: { + fontSize: 14, + color: "#000000", + fontWeight: "normal", + lineHeight: 1.5, + marginBottom: 40, + maxWidth: 400, + }, + + tenantCard: { + backgroundColor: "transparent", + padding: 0, + maxWidth: 400, + }, + + tenantName: { + fontSize: 18, + fontWeight: "bold", + color: "#000000", + marginBottom: 8, + textAlign: "center", + }, + + tenantMeta: { + fontSize: 11, + color: "#333333", + textAlign: "center", + }, + + coverFooter: { + textAlign: "center", + marginTop: 60, + }, + + confidential: { + fontSize: 9, + color: "#A0AEC0", + textTransform: "uppercase", + letterSpacing: 1, + }, + + // CONTENT PAGES - MODULAR COMPOSITION (FROST) + pageHeader: { + borderBottom: `1px solid ${brandColor}`, + paddingBottom: 12, + marginBottom: 24, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start", + pageBreakAfter: "avoid", + breakAfter: "avoid", + }, + + pageHeaderContent: { + flex: 1, + }, + + pageTitle: { + fontSize: 20, + fontWeight: "bold", + color: "#1A202C", + marginBottom: 8, + }, + + pageSubtitle: { + fontSize: 11, + color: "#4A5568", + fontWeight: "normal", + }, + + // SECTIONS - REPEATABLE PATTERNS (FROST) + section: { + marginBottom: 24, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + sectionTitle: { + fontSize: 14, + fontWeight: "bold", + color: brandColor, + marginBottom: 12, + pageBreakAfter: "avoid", + breakAfter: "avoid", + orphans: 3, + widows: 3, + }, + + bodyText: { + fontSize: 9, + color: "#2D3748", + lineHeight: 1.5, + marginBottom: 12, + textAlign: "justify", + }, + + // STATS GRID - PERFECT ALIGNMENT (SPOOL) + statsGrid: { + flexDirection: "row", + gap: 12, + marginBottom: 20, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + statCard: { + flex: 1, + backgroundColor: "#FFFFFF", + border: `1px solid #E2E8F0`, + borderRadius: 6, + padding: 16, + alignItems: "center", + borderTop: `3px solid ${brandColor}`, + }, + + statNumber: { + fontSize: 16, + fontWeight: "bold", + color: brandColor, + marginBottom: 4, + }, + + statLabel: { + fontSize: 7, + color: "#4A5568", + textTransform: "uppercase", + letterSpacing: 0.5, + textAlign: "center", + fontWeight: "bold", + }, + + // COMPLIANCE BARS - VISUAL CONFIDENCE (SPOOL) + complianceList: { + gap: 8, + }, + + complianceItem: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#FFFFFF", + padding: 10, + borderRadius: 4, + border: `1px solid #F0F0F0`, + }, + + complianceLabel: { + fontSize: 8, + color: "#2D3748", + width: 80, + fontWeight: "bold", + }, + + complianceBarContainer: { + flex: 1, + height: 6, + backgroundColor: "#E2E8F0", + marginHorizontal: 10, + borderRadius: 3, + overflow: "hidden", + }, + + complianceBar: { + height: 6, + backgroundColor: brandColor, + borderRadius: 3, + }, + + complianceValue: { + fontSize: 8, + color: "#2D3748", + width: 25, + textAlign: "right", + fontWeight: "bold", + }, + + // SECURE SCORE CARDS - ENTERPRISE GRADE + scoreGrid: { + flexDirection: "row", + gap: 12, + marginBottom: 20, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + scoreCard: { + flex: 1, + backgroundColor: "#FFFFFF", + border: `1px solid #E2E8F0`, + borderRadius: 6, + padding: 16, + alignItems: "center", + borderTop: `3px solid ${brandColor}`, + }, + + scoreNumber: { + fontSize: 20, + fontWeight: "bold", + color: brandColor, + marginBottom: 8, + }, + + scoreLabel: { + fontSize: 7, + color: "#4A5568", + textTransform: "uppercase", + letterSpacing: 0.5, + textAlign: "center", + fontWeight: "bold", + }, + + // CHART AREA - BROWSER CONSTRAINTS (RAUCH) + chartContainer: { + backgroundColor: "#FFFFFF", + border: `1px solid #E2E8F0`, + borderRadius: 6, + padding: 16, + marginBottom: 20, + alignItems: "center", + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + chartTitle: { + fontSize: 10, + fontWeight: "bold", + color: "#2D3748", + marginBottom: 12, + }, + + chartData: { + fontSize: 9, + color: "#4A5568", + textAlign: "center", + lineHeight: 1.4, + }, + + // CONTROLS TABLE - HIGH PERFORMANCE (RAUCH) + controlsTable: { + border: `1px solid #E2E8F0`, + borderRadius: 6, + overflow: "hidden", + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + tableHeader: { + flexDirection: "row", + backgroundColor: brandColor, + paddingVertical: 10, + paddingHorizontal: 12, + }, + + headerCell: { + fontSize: 7, + fontWeight: "bold", + color: "#FFFFFF", + textTransform: "uppercase", + letterSpacing: 0.5, + }, + + headerName: { + width: 100, + }, + + headerDesc: { + flex: 1, + marginLeft: 12, + }, + + headerStatus: { + width: 60, + textAlign: "center", + marginLeft: 12, + }, + + tableRow: { + flexDirection: "row", + borderBottomWidth: 1, + borderBottomColor: "#F7FAFC", + paddingVertical: 8, + paddingHorizontal: 12, + alignItems: "center", + }, + + cellName: { + width: 100, + fontSize: 8, + fontWeight: "bold", + color: "#2D3748", + }, + + cellDesc: { + flex: 1, + marginLeft: 12, + fontSize: 7, + color: "#4A5568", + lineHeight: 1.3, + }, + + cellStatus: { + width: 60, + marginLeft: 12, + alignItems: "center", + justifyContent: "center", + }, + + // STATUS TEXT - SIMPLE APPROACH + statusText: { + fontSize: 7, + fontWeight: "bold", + textAlign: "center", + textTransform: "uppercase", + letterSpacing: 0.3, + }, + + statusCompliant: { + color: "#22543D", + }, + + statusPartial: { + color: "#744210", + }, + + statusReview: { + color: "#742A2A", + }, + + // INFO BOXES - CONSISTENT PATTERNS (FROST) + infoBox: { + backgroundColor: "#FFFFFF", + border: `1px solid #E2E8F0`, + borderLeft: `4px solid ${brandColor}`, + borderRadius: 4, + padding: 12, + marginBottom: 12, + pageBreakInside: "avoid", + breakInside: "avoid", + orphans: 3, + widows: 3, + }, + + infoTitle: { + fontSize: 9, + fontWeight: "bold", + color: "#2D3748", + marginBottom: 6, + }, + + infoText: { + fontSize: 8, + color: "#4A5568", + lineHeight: 1.4, + }, + + // RECOMMENDATIONS - SCALABLE SECTIONS (FROST) + recommendationsList: { + gap: 8, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + recommendationItem: { + flexDirection: "row", + alignItems: "flex-start", + }, + + recommendationBullet: { + fontSize: 8, + color: brandColor, + marginRight: 6, + fontWeight: "bold", + marginTop: 1, + }, + + recommendationText: { + fontSize: 8, + color: "#2D3748", + lineHeight: 1.4, + flex: 1, + }, + + recommendationLabel: { + fontWeight: "bold", + }, + + // FOOTER - DETERMINISTIC PAGINATION (FLORENCE) + footer: { + position: "absolute", + bottom: 20, + left: 40, + right: 40, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + borderTop: "1px solid #E2E8F0", + paddingTop: 8, + }, + + footerText: { + fontSize: 7, + color: "#718096", + }, + + pageNumber: { + fontSize: 7, + color: "#718096", + fontWeight: "bold", + }, + + // BLACK STATISTIC PAGES - MODERN DESIGN + statPage: { + flexDirection: "column", + backgroundColor: "#000000", + fontFamily: "Helvetica", + padding: 0, + justifyContent: "center", + alignItems: "flex-start", + minHeight: "100%", + position: "relative", + }, + + statOverlay: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + padding: 60, + justifyContent: "center", + alignItems: "flex-start", + zIndex: 10, + backgroundColor: "rgba(0, 0, 0, 0.7)", + }, + + statMainText: { + fontSize: 18, + color: "#FFFFFF", + fontWeight: "bold", + lineHeight: 1.4, + marginBottom: 8, + }, + + statHighlight: { + fontSize: 72, + color: brandColor, + fontWeight: "900", + lineHeight: 1, + marginBottom: 8, + }, + + statBackground: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + opacity: 0.5, + }, + + statSubText: { + fontSize: 14, + color: "#FFFFFF", + fontWeight: "bold", + lineHeight: 1.3, + marginBottom: 40, + }, + + statFooterText: { + position: "absolute", + bottom: 60, + right: 60, + fontSize: 12, + color: "#FFFFFF", + fontWeight: "bold", + textAlign: "right", + lineHeight: 1.3, + }, + + statBrandFooter: { + position: "absolute", + bottom: 60, + left: 60, + fontSize: 8, + color: "#666666", + textTransform: "uppercase", + letterSpacing: 1, + }, + + // CENTERED IMAGE STYLE + centeredImage: { + width: 300, + height: 200, + alignSelf: "center", + marginVertical: 20, + borderRadius: 8, + }, + + // SVG CHART STYLES + svgChartContainer: { + alignItems: "center", + marginVertical: 12, + }, + + svgChart: { + width: 400, + height: 200, + marginBottom: 8, + }, + + chartSummaryText: { + fontSize: 8, + fontWeight: "bold", + color: brandColor, + textAlign: "center", + marginTop: 8, + }, + }); + + // PROCESS REAL STANDARDS DATA + const processStandardsData = (apiData) => { + // Try to fetch standards data dynamically + let standardsData = null; + try { + standardsData = require("../data/standards.json"); + } catch (error) {} + + if (!apiData || !Array.isArray(apiData) || apiData.length === 0) { + return []; + } + + const processedStandards = []; + const tenantData = apiData[0]; // Get the first tenant's data + + // Process each standard from the API response + Object.keys(tenantData).forEach((key) => { + if (key.startsWith("standards.") && key !== "tenantFilter") { + const standardKey = key; + const standardValue = tenantData[key]; + const standardDef = standardsData?.find((std) => std.name === standardKey); + + if (standardDef) { + // Determine compliance status + let status = "Review"; + if (standardValue && typeof standardValue === "object" && standardValue.Value === true) { + status = "Compliant"; + } else if (standardValue && standardValue.Value === true) { + status = "Compliant"; + } + // Get tags for display - fix the tags access + const tags = + standardDef.tag && Array.isArray(standardDef.tag) && standardDef.tag.length > 0 + ? standardDef.tag.slice(0, 2).join(", ") // Show first 2 tags + : "No tags"; + + processedStandards.push({ + name: standardDef.label, + description: + standardDef.executiveText || standardDef.helpText || "No description available", + status: status, + tags: tags, + }); + } else { + // If no definition found, still add it with basic info + let status = "Review"; + if (standardValue && typeof standardValue === "object" && standardValue.Value === true) { + status = "Compliant"; + } else if (standardValue && standardValue.Value === true) { + status = "Compliant"; + } + + // Create a proper name from the key + const displayName = standardKey + .replace("standards.", "") + .replace(/([A-Z])/g, " $1") // Add space before capital letters + .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter + .trim(); + + processedStandards.push({ + name: displayName, + description: "Security standard implementation", + status: status, + tags: "No tags", + }); + } + } + }); + + return processedStandards; + }; + + let securityControls = processStandardsData(standardsCompareData); + + const getBadgeStyle = (status) => { + switch (status) { + case "Compliant": + return [styles.statusText, styles.statusCompliant]; + case "Partial": + return [styles.statusText, styles.statusPartial]; + case "Review": + case "Review Required": + return [styles.statusText, styles.statusReview]; + default: + return styles.statusText; + } + }; + + return ( + + {/* COVER PAGE - JOBS/RAMS/IVE PERFECTION */} + + + + + {brandingSettings?.logo && ( + + )} + + {currentDate} + + + + SECURITY ASSESSMENT + + + Executive{"\n"} + Summary + + + + Security & Compliance Assessment for {tenantName || "your organization"} + + + + {tenantName || "Organization Name"} + + + + + Confidential & Proprietary + + + + {/* EXECUTIVE SUMMARY - MODULAR COMPOSITION (FROST) */} + + + + Executive Summary + + Strategic overview of your Microsoft 365 security posture + + + {brandingSettings?.logo && ( + + )} + + + + + This security assessment for{" "} + {tenantName || "your organization"} provides + a clear picture of your organization's cybersecurity posture and readiness against + modern threats. We've evaluated your current security measures against industry best + practices to identify strengths and opportunities for improvement. + + + + Our assessment follows globally recognized security standards to ensure your + organization meets regulatory requirements and industry benchmarks. This approach helps + protect your business assets, maintain customer trust, and reduce operational risks from + cyber threats. + + + + + Environment Overview + + + + {userStats?.licensedUsers || "0"} + Licensed Users + + + {userStats?.unlicensedUsers || "0"} + Unlicensed Users + + + {userStats?.guests || "0"} + Guest Users + + + {userStats?.globalAdmins || "0"} + Global Admins + + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + {/* STATISTIC PAGE 1 - CHAPTER SPLITTER */} + + + + 83% + + of organizations experienced{"\n"} + more than one cyberattack + {"\n"} + in the past year + + + + Proactive security prevents{"\n"} + repeated attacks + + + + {/* SECURITY CONTROLS - Only show if standards data is available */} + {(() => { + return securityControls && securityControls.length > 0; + })() && ( + + + + Security Standards Assessment + + Detailed evaluation of implemented security standards + + + {brandingSettings?.logo && ( + + )} + + + + + Your security standards have been carefully evaluated against industry best practices + to protect your business from cyber threats while ensuring smooth daily operations. + These standards help maintain business continuity, protect sensitive data, and meet + regulatory requirements that are essential for your industry. + + + + + Security Standards Status + + + + Standard + Description + Tags + + Status + + + + {securityControls.map((control, index) => ( + + + {control.name} + + + {control.description} + + + {control.tags} + + + {control.status} + + + ))} + + + + + Key Recommendations + + + + • + + Immediate Actions: Address + standards marked as "Review" to enhance security posture + + + + • + + Compliance: Ensure all security + standards are properly implemented and maintained + + + + • + + Monitoring: Establish regular + review cycles for all security standards + + + + • + + Training: Implement security + awareness programs to reduce human risk factors + + + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + )} + + {/* STATISTIC PAGE 2 - CHAPTER SPLITTER - Only show if secure score data is available */} + {secureScoreData && secureScoreData?.isSuccess && secureScoreData?.translatedData && ( + + + + 95% + + of successful cyber attacks{"\n"} + could have been prevented with{"\n"} + proactive security measures + + + + Your security resilience is{"\n"} + our primary mission + + + )} + + {/* MICROSOFT SECURE SCORE - DEDICATED PAGE - Only show if secure score data is available */} + {secureScoreData && secureScoreData?.isSuccess && secureScoreData?.translatedData && ( + + + + Microsoft Secure Score + + Comprehensive security posture measurement and benchmarking + + + {brandingSettings?.logo && ( + + )} + + + + + Microsoft Secure Score measures how well your organization is protected against cyber + threats. This score reflects the effectiveness of your current security measures and + helps identify areas where additional protection could strengthen your business + resilience. + + + + + Score Comparison + + + + + {secureScoreData?.translatedData?.currentScore || "N/A"} + + Current Score + + + + {secureScoreData?.translatedData?.maxScore || "N/A"} + + Max Score + + + + {secureScoreData?.translatedData?.percentageVsSimilar || "N/A"}% + + vs Similar Orgs + + + + {secureScoreData?.translatedData?.percentageVsAllTenants || "N/A"}% + + vs All Orgs + + + + + + 7-Day Score Trend + + + Secure Score Progress + {secureScoreData?.secureScore?.data?.Results && + secureScoreData.secureScore.data.Results.length > 0 ? ( + + + {/* Chart Background */} + + + {/* Chart Grid Lines */} + {[0, 1, 2, 3, 4].map((i) => ( + + ))} + + {/* Chart Data Points and Area */} + {(() => { + const data = secureScoreData.secureScore.data.Results.slice().reverse(); + const maxScore = secureScoreData?.translatedData?.maxScore || 100; + const minScore = 0; // Always start from 0 + const scoreRange = maxScore; // Full range from 0 to max + const chartWidth = 320; + const chartHeight = 140; + const pointSpacing = chartWidth / Math.max(data.length - 1, 1); + + // Generate path for area chart + let pathData = `M 40 ${ + 160 - (data[0].currentScore / scoreRange) * chartHeight + }`; + data.forEach((point, index) => { + if (index > 0) { + const x = 40 + index * pointSpacing; + const y = 160 - (point.currentScore / scoreRange) * chartHeight; + pathData += ` L ${x} ${y}`; + } + }); + pathData += ` L ${40 + (data.length - 1) * pointSpacing} 160 L 40 160 Z`; + + // Generate line path (without area fill) + let lineData = `M 40 ${ + 160 - (data[0].currentScore / scoreRange) * chartHeight + }`; + data.forEach((point, index) => { + if (index > 0) { + const x = 40 + index * pointSpacing; + const y = 160 - (point.currentScore / scoreRange) * chartHeight; + lineData += ` L ${x} ${y}`; + } + }); + + return ( + <> + {/* Area Fill */} + + + {/* Line */} + + + {/* Data Points */} + {data.map((point, index) => { + const x = 40 + index * pointSpacing; + const y = 160 - (point.currentScore / scoreRange) * chartHeight; + return ; + })} + + {/* X-axis Labels */} + {data.map((point, index) => { + const x = 40 + index * pointSpacing; + const date = new Date(point.createdDateTime); + const label = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + return ( + + {label} + + ); + })} + + {/* Y-axis Labels */} + {[ + 0, + Math.round(maxScore * 0.25), + Math.round(maxScore * 0.5), + Math.round(maxScore * 0.75), + maxScore, + ].map((score, index) => ( + + {score} + + ))} + + ); + })()} + + + + Current: {secureScoreData?.translatedData?.currentScore || "N/A"} /{" "} + {secureScoreData?.translatedData?.maxScore || "N/A"}( + {secureScoreData?.translatedData?.percentageCurrent || "N/A"}%) + + + ) : ( + + Current Score: {secureScoreData?.translatedData?.currentScore || "N/A"} /{" "} + {secureScoreData?.translatedData?.maxScore || "N/A"} + {"\n"} + Achievement Rate: {secureScoreData?.translatedData?.percentageCurrent || "N/A"}% + {"\n"} + Historical data not available + + )} + + + + + What Your Score Means + + Your current score of {secureScoreData?.translatedData?.currentScore || "N/A"}{" "} + represents {secureScoreData?.translatedData?.percentageCurrent || "N/A"}% of the + maximum protection level available. This indicates how well your organization is + currently defended against common cyber threats and data breaches. + + + + + Why Scores Change + + • Business growth and new employees may temporarily lower scores until security + measures are applied{"\n"}• Changes in software licenses can affect available security + features{"\n"}• New security threats require updated protections, which may impact + scores{"\n"}• Regular security improvements help maintain and increase your protection + level + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + )} + + {/* LICENSING PAGE - Only show if license data is available */} + {licensingData && Array.isArray(licensingData) && licensingData.length > 0 && ( + <> + {/* STATISTIC PAGE 3 - CHAPTER SPLITTER */} + + + + Every + 39 + seconds + + a business falls victim to{"\n"} + ransomware attacks + + + + Proactive defense beats{"\n"} + reactive recovery + + + + + + License Management + + Microsoft 365 license allocation and utilization analysis + + + {brandingSettings?.logo && ( + + )} + + + + + Smart license management helps control costs while ensuring your team has the tools + they need to be productive. This analysis shows how your current licenses are being + used and identifies opportunities to optimize spending without compromising business + operations. + + + + + License Allocation Summary + + + + License Type + Used + + Available + + Total + + + {licensingData.map((license, index) => ( + + + {license.License || license.license || "N/A"} + + + {license.CountUsed || license.countUsed || "0"} + + + {license.CountAvailable || license.countAvailable || "0"} + + + {license.TotalLicenses || license.totalLicenses || "0"} + + + ))} + + + + + License Optimization Recommendations + + + + • + + Usage Monitoring: Track how + licenses are being used to identify cost-saving opportunities + + + + • + + Cost Control: Review unused + licenses to reduce unnecessary spending + + + + • + + Growth Planning: Ensure you have + enough licenses for business expansion without overspending + + + + • + + Regular Reviews: Conduct + quarterly reviews to maintain cost-effective license allocation + + + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + )} + + {/* DEVICES PAGE - Only show if device data is available */} + {deviceData && Array.isArray(deviceData) && deviceData.length > 0 && ( + <> + {/* STATISTIC PAGE 4 - CHAPTER SPLITTER */} + + + + $4.45M + + average cost of a{"\n"} + data breach in 2024 + + + + Investment in security + {"\n"} + saves millions in recovery + + + + + + Device Management + + Device compliance status and management overview + + + {brandingSettings?.logo && ( + + )} + + + + + Managing employee devices is essential for protecting your business data and + maintaining productivity. This analysis shows which devices meet your security + standards and identifies any that may need attention to prevent data breaches or + operational disruptions. + + + + + Device Compliance Overview + + + + {deviceData.length} + Total Devices + + + + { + deviceData.filter( + (device) => + device.complianceState === "Compliant" || + device.ComplianceState === "Compliant" + ).length + } + + Compliant + + + + { + deviceData.filter( + (device) => + device.complianceState !== "Compliant" && + device.ComplianceState !== "Compliant" + ).length + } + + Non-Compliant + + + + {Math.round( + (deviceData.filter( + (device) => + device.complianceState === "Compliant" || + device.ComplianceState === "Compliant" + ).length / + deviceData.length) * + 100 + )} + % + + Compliance Rate + + + + + + Device Management Summary + + + + Device Name + OS + Compliance + Last Sync + + + {deviceData.slice(0, 8).map((device, index) => { + const lastSync = device.lastSyncDateTime + ? new Date(device.lastSyncDateTime).toLocaleDateString() + : "N/A"; + return ( + + + {device.deviceName || "N/A"} + + + {device.operatingSystem || "N/A"} + + + + {device.complianceState || "Unknown"} + + + {lastSync} + + ); + })} + + + + + Device Insights + + + + + {deviceData.filter((device) => device.operatingSystem === "Windows").length} + + Windows Devices + + + + {deviceData.filter((device) => device.operatingSystem === "iOS").length} + + iOS Devices + + + + {deviceData.filter((device) => device.operatingSystem === "Android").length} + + Android Devices + + + + {deviceData.filter((device) => device.isEncrypted === true).length} + + Encrypted + + + + + + Device Management Recommendations + + Keep devices updated and secure to protect business data. Regularly check that all + employee devices meet security standards and address any issues promptly. Consider + automated policies to maintain consistent security across all devices and conduct + regular reviews to identify potential risks. + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + )} + + {/* CONDITIONAL ACCESS POLICIES PAGE - Only show if data is available */} + {conditionalAccessData && Array.isArray(conditionalAccessData) && conditionalAccessData.length > 0 && ( + <> + {/* STATISTIC PAGE 5 - CHAPTER SPLITTER */} + + + + 277 + days + + average time to identify and{"\n"} + contain a data breach + + + + Early detection minimizes{"\n"} + business impact + + + + + + Conditional Access Policies + + Identity and access management security controls + + + {brandingSettings?.logo && ( + + )} + + + + + Access control policies help protect your business by ensuring only the right people + can access sensitive information under appropriate circumstances. These smart + security measures automatically evaluate each access request and apply additional + verification when needed, balancing security with employee productivity. + + + + + How Access Controls Protect Your Business + + These policies work like intelligent security guards, making decisions based on who + is trying to access what, from where, and when. For example, accessing email from + the office might be seamless, but accessing it from an unusual location might + require additional verification. This approach protects your data while minimizing + disruption to daily work. + + + + + Current Policy Configuration + + + + Policy Name + State + Applications + Controls + + + {conditionalAccessData.slice(0, 8).map((policy, index) => { + const getStateStyle = (state) => { + switch (state) { + case "enabled": + return styles.statusCompliant; + case "enabledForReportingButNotEnforced": + return styles.statusPartial; + case "disabled": + return styles.statusReview; + default: + return styles.statusText; + } + }; + + const getStateDisplay = (state) => { + switch (state) { + case "enabled": + return "Enabled"; + case "enabledForReportingButNotEnforced": + return "Report Only"; + case "disabled": + return "Disabled"; + default: + return state || "Unknown"; + } + }; + + const getControlsText = (policy) => { + const controls = []; + if (policy.builtInControls) { + if (policy.builtInControls.includes("mfa")) controls.push("MFA"); + if (policy.builtInControls.includes("block")) controls.push("Block"); + if (policy.builtInControls.includes("compliantDevice")) + controls.push("Compliant Device"); + } + return controls.length > 0 ? controls.join(", ") : "Custom"; + }; + + return ( + + + {policy.displayName || "N/A"} + + + + {getStateDisplay(policy.state)} + + + + {policy.includeApplications || "All"} + + + {getControlsText(policy)} + + + ); + })} + + + + + Policy Overview + + + + {conditionalAccessData.length} + Total Policies + + + + {conditionalAccessData.filter((policy) => policy.state === "enabled").length} + + Enabled + + + + { + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + } + + Report Only + + + + { + conditionalAccessData.filter( + (policy) => policy.builtInControls && policy.builtInControls.includes("mfa") + ).length + } + + MFA Policies + + + + + + Policy Analysis + + + + • + + Policy Coverage:{" "} + {conditionalAccessData.length} conditional access policies configured + + + + • + + Enforcement Status:{" "} + {conditionalAccessData.filter((policy) => policy.state === "enabled").length}{" "} + policies actively enforced + + + + • + + Testing Phase:{" "} + { + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + }{" "} + policies in report-only mode + + + + • + + Security Controls: Multi-factor + authentication and access blocking implemented + + + + + + + Access Control Recommendations + + {conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length > 0 + ? `Consider activating ${ + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + } policies currently in testing mode after ensuring they don't disrupt business operations. ` + : "Your access controls are properly configured. "} + Regularly review how these policies affect employee productivity and adjust as + needed. Consider additional location-based protections for enhanced security without + impacting daily operations. + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + )} + + ); +}; + +export const ExecutiveReportButton = (props) => { + const { tenantName, tenantId, userStats, standardsData, organizationData, ...other } = props; + + const settings = useSettings(); + const brandingSettings = settings.customBranding; + + // Get real secure score data + const secureScore = useSecureScore(); + + // Get real license data + const licenseData = ApiGetCall({ + url: "/api/ListLicenses", + data: { + tenantFilter: settings.currentTenant, + }, + queryKey: `licenses-report-${settings.currentTenant}`, + }); + // Get real device data + const deviceData = ApiGetCall({ + url: "/api/ListDevices", + data: { + tenantFilter: settings.currentTenant, + }, + queryKey: `devices-report-${settings.currentTenant}`, + }); + + // Get real conditional access policy data + const conditionalAccessData = ApiGetCall({ + url: "/api/ListConditionalAccessPolicies", + data: { + tenantFilter: settings.currentTenant, + }, + queryKey: `ca-policies-report-${settings.currentTenant}`, + }); + + // Get real standards data + const standardsCompareData = ApiGetCall({ + url: "/api/ListStandardsCompare", + data: { + tenantFilter: settings.currentTenant, + }, + queryKey: `standards-compare-report-${settings.currentTenant}`, + }); + + // Check if all data is loaded (either successful or failed) + const isDataLoading = + secureScore.isFetching || + licenseData.isFetching || + deviceData.isFetching || + conditionalAccessData.isFetching || + standardsCompareData.isFetching; + + const hasAllDataFinished = + (secureScore.isSuccess || secureScore.isError) && + (licenseData.isSuccess || licenseData.isError) && + (deviceData.isSuccess || deviceData.isError) && + (conditionalAccessData.isSuccess || conditionalAccessData.isError) && + (standardsCompareData.isSuccess || standardsCompareData.isError); + + // Show button when all data is finished loading (regardless of success/failure) + const shouldShowButton = hasAllDataFinished && !isDataLoading; + + const fileName = `Executive_Report_${tenantName?.replace(/[^a-zA-Z0-9]/g, "_") || "Tenant"}_${ + new Date().toISOString().split("T")[0] + }.pdf`; + + // Don't render the button if data is not ready + if (!shouldShowButton) { + return ( + + + + ); + } + + return ( + + } + fileName={fileName} + > + {({ blob, url, loading, error }) => ( + + + + )} + + ); +}; diff --git a/src/components/linearProgressWithLabel.jsx b/src/components/linearProgressWithLabel.jsx index f01031da45ca..1dea47fa4890 100644 --- a/src/components/linearProgressWithLabel.jsx +++ b/src/components/linearProgressWithLabel.jsx @@ -1,4 +1,4 @@ -import { Box, LinearProgress, Typography } from "@mui/material"; +import { Box, LinearProgress } from "@mui/material"; export const LinearProgressWithLabel = (props) => { return ( diff --git a/src/components/pdfExportButton.js b/src/components/pdfExportButton.js index 3d011fd1ebf2..c95c27911bc4 100644 --- a/src/components/pdfExportButton.js +++ b/src/components/pdfExportButton.js @@ -3,10 +3,12 @@ import { PictureAsPdf } from "@mui/icons-material"; import jsPDF from "jspdf"; import autoTable from "jspdf-autotable"; import { getCippFormatting } from "../utils/get-cipp-formatting"; +import { useSettings } from "../hooks/use-settings"; export const PDFExportButton = (props) => { const { rows, columns, reportName, columnVisibility, ...other } = props; - + const brandingSettings = useSettings().customBranding; + //we need to use jspdf here because the react-pdf library gets killed with our amount of data. const handleExportRows = (rows) => { const unit = "pt"; const size = "A3"; // Use A1, A2, A3 or A4 @@ -27,12 +29,98 @@ export const PDFExportButton = (props) => { return formattedRow; }); + // Add custom branding logo if available + let logoHeight = 0; + if (brandingSettings?.logo) { + try { + const logoSize = 60; // Fixed logo height + const logoX = 40; // Left margin + const logoY = 30; // Top margin + + // Add the base64 image to the PDF + doc.addImage(brandingSettings.logo, "PNG", logoX, logoY, logoSize, logoSize); + logoHeight = logoSize + 20; // Logo height plus some spacing + } catch (error) { + console.warn("Failed to add logo to PDF:", error); + } + } + + // Calculate column widths based on content and available space + const pageWidth = doc.internal.pageSize.getWidth(); + const margin = 40; // Consistent margins from edges + const availableWidth = pageWidth - 2 * margin; + const columnCount = exportColumns.length; + + // Calculate dynamic column widths based on content length + const columnWidths = exportColumns.map((col) => { + const headerLength = col.header.length; + const maxContentLength = Math.max( + ...formattedData.map((row) => String(row[col.dataKey] || "").length) + ); + const estimatedWidth = Math.max(headerLength, maxContentLength) * 6; // 6 points per character + return Math.min(estimatedWidth, (availableWidth / columnCount) * 1.5); // Cap at 1.5x average + }); + + // Normalize widths to fit available space + const totalEstimatedWidth = columnWidths.reduce((sum, width) => sum + width, 0); + const normalizedWidths = columnWidths.map( + (width) => (width / totalEstimatedWidth) * availableWidth + ); + + // Convert hex color to RGB if custom branding color is provided + const getHeaderColor = () => { + if (brandingSettings?.colour) { + const hex = brandingSettings.colour.replace("#", ""); + const r = parseInt(hex.substr(0, 2), 16); + const g = parseInt(hex.substr(2, 2), 16); + const b = parseInt(hex.substr(4, 2), 16); + return [r, g, b]; + } + return [247, 127, 0]; // Default orange color + }; + let content = { - startY: 100, - columns: exportColumns, - body: formattedData, + startY: 100 + logoHeight, // Adjust table start position based on logo + head: [exportColumns.map((col) => col.header)], + body: formattedData.map((row) => exportColumns.map((col) => String(row[col.dataKey] || ""))), theme: "striped", - headStyles: { fillColor: [247, 127, 0] }, + headStyles: { + fillColor: getHeaderColor(), + textColor: [255, 255, 255], + fontStyle: "bold", + halign: "center", + valign: "middle", + fontSize: 10, + cellPadding: 8, + }, + bodyStyles: { + fontSize: 9, + cellPadding: 6, + valign: "top", + overflow: "linebreak", + cellWidth: "wrap", + }, + columnStyles: exportColumns.reduce((styles, col, index) => { + styles[index] = { + cellWidth: normalizedWidths[index], + halign: "left", + valign: "top", + }; + return styles; + }, {}), + margin: { + top: margin, + right: margin, + bottom: margin, + left: margin, + }, + tableWidth: "auto", + styles: { + overflow: "linebreak", + cellWidth: "wrap", + fontSize: 9, + cellPadding: 6, + }, }; autoTable(doc, content); diff --git a/src/components/query-field.js b/src/components/query-field.js index f1078830ee60..dfeb6f37e1a4 100644 --- a/src/components/query-field.js +++ b/src/components/query-field.js @@ -29,7 +29,6 @@ export const QueryField = (props) => { inputRef.current.focus(); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps [disabled]); const handleChange = useCallback((event) => { diff --git a/src/components/resource-loading.js b/src/components/resource-loading.js index 743a53baea03..d80a6b09452a 100644 --- a/src/components/resource-loading.js +++ b/src/components/resource-loading.js @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import { CircularProgress, SvgIcon, Typography } from "@mui/material"; +import { CircularProgress, Typography } from "@mui/material"; import { styled } from "@mui/material/styles"; const ResourceLoadingRoot = styled("div")(({ theme }) => ({ diff --git a/src/components/toaster.js b/src/components/toaster.js index aa26b5c462c6..933adf625de1 100644 --- a/src/components/toaster.js +++ b/src/components/toaster.js @@ -1,5 +1,5 @@ import { CloseSharp } from "@mui/icons-material"; -import { Alert, Button, IconButton, Snackbar } from "@mui/material"; +import { Alert, IconButton, Snackbar } from "@mui/material"; import { useSelector } from "react-redux"; import { useDispatch } from "react-redux"; import { closeToast } from "../store/toasts"; diff --git a/src/contexts/settings-context.js b/src/contexts/settings-context.js index a265c9fd09aa..e85cd2574530 100644 --- a/src/contexts/settings-context.js +++ b/src/contexts/settings-context.js @@ -74,6 +74,10 @@ const initialSettings = { pinNav: true, currentTenant: null, showDevtools: false, + customBranding: { + colour: "#F77F00", + logo: null, + }, }; const initialState = { diff --git a/src/data/alerts.json b/src/data/alerts.json index ef62b6d13373..af0270248982 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -55,6 +55,15 @@ "inputName": "SharePointQuota", "recommendedRunInterval": "4h" }, + { + "name": "OneDriveQuota", + "label": "Alert on % OneDrive quota used", + "requiresInput": true, + "inputType": "textField", + "inputLabel": "Enter quota percentage (default: 90)", + "inputName": "OneDriveQuota", + "recommendedRunInterval": "4h" + }, { "name": "ExpiringLicenses", "label": "Alert on licenses expiring in 30 days", @@ -85,6 +94,16 @@ "label": "Alert on new Defender Incidents found", "recommendedRunInterval": "4h" }, + { + "name": "Vulnerabilities", + "label": "Alert on vulnerabilities older than X hours", + "requiresInput": true, + "inputType": "number", + "inputLabel": "Alert on vulnerabilities first seen more than X hours ago (default: 24)", + "inputName": "AgeInHours", + "recommendedRunInterval": "4h", + "description": "Monitors for software vulnerabilities that were first discovered more than the specified number of hours ago. This helps identify lingering vulnerabilities that may have been missed or not yet remediated. Requires Defender for Endpoint/Business." + }, { "name": "UnusedLicenses", "label": "Alert on unused licenses", @@ -168,5 +187,27 @@ "inputName": "TERRLThreshold", "recommendedRunInterval": "1h", "description": "Monitors tenant outbound email volume against Microsoft's TERRL limits. Tenant data is updated every hour." + }, + { + "name": "LowDomainScore", + "label": "Alert on domains with low security score", + "requiresInput": true, + "inputType": "number", + "inputLabel": "Alert when score is below % (default: 70)", + "inputName": "InputValue", + "recommendedRunInterval": "7d", + "description": "Monitors domain security scores from the DomainAnalyser and alerts when scores fall below the specified threshold." + }, + { + "name": "GlobalAdminNoAltEmail", + "label": "Alert on Global Admin accounts without alternate email address", + "recommendedRunInterval": "7d", + "description": "Monitors Global Admin accounts and alerts when they don't have an alternate email address set, which is important for password recovery of key accounts." + }, + { + "name": "NewRiskyUsers", + "label": "Alert on new risky users (P2 License Required)", + "recommendedRunInterval": "30m", + "description": "Monitors for new risky users in the tenant. Risky users are defined as users who have performed actions that are considered risky, such as password resets, MFA failures, or suspicious activity." } ] diff --git a/src/data/standards.json b/src/data/standards.json index d70ddafcbd6c..9a2137930d5c 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -5,6 +5,7 @@ "tag": [], "helpText": "Defines the email address to receive general updates and information related to M365 subscriptions. Leave a contact field blank if you do not want to update the contact information.", "docsDescription": "", + "executiveText": "Establishes designated contact email addresses for receiving important Microsoft 365 subscription updates and notifications. This ensures proper communication channels are maintained for general, security, marketing, and technical matters, improving organizational responsiveness to critical system updates.", "addedComponent": [ { "type": "textField", @@ -44,6 +45,7 @@ "tag": [], "helpText": "Creates a new mail contact in Exchange Online across all selected tenants. The contact will be visible in the Global Address List.", "docsDescription": "This standard creates a new mail contact in Exchange Online. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios.", + "executiveText": "Automatically creates external email contacts in the organization's address book, enabling seamless communication with external partners and vendors. This standardizes contact management across all company locations and improves collaboration efficiency.", "addedComponent": [ { "type": "textField", @@ -75,28 +77,70 @@ "impactColour": "info", "addedDate": "2024-03-19", "powershellEquivalent": "New-MailContact", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] + }, + { + "name": "standards.DeployContactTemplates", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Creates new mail contacts in Exchange Online across all selected tenants based on the selected templates. The contact will be visible in the Global Address List unless hidden.", + "docsDescription": "This standard creates new mail contacts in Exchange Online based on the selected templates. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios.", + "executiveText": "Deploys standardized external contact templates across all company locations, ensuring consistent communication channels with key external partners, vendors, and stakeholders. This streamlines contact management and maintains uniform business relationships.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "label": "Select Mail Contact Templates", + "name": "standards.DeployContactTemplates.templateIds", + "api": { + "url": "/api/ListContactTemplates", + "labelField": "name", + "valueField": "GUID", + "queryKey": "Contact Templates" + } + } + ], + "label": "Deploy Mail Contact Template", + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + }, + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-05-31", + "powershellEquivalent": "New-MailContact", + "recommendedBy": ["CIPP"] }, { "name": "standards.AuditLog", "cat": "Global Standards", - "tag": [ - "CIS", - "mip_search_auditlog" - ], + "tag": ["CIS M365 5.0 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"], "helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.", + "executiveText": "Activates comprehensive activity logging across Microsoft 365 services to track user actions, system changes, and security events. This provides essential audit trails for compliance requirements, security investigations, and regulatory reporting.", "addedComponent": [], "label": "Enable the Unified Audit Log", "impact": "Low Impact", "impactColour": "info", "addedDate": "2021-11-16", "powershellEquivalent": "Enable-OrganizationCustomization", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] + }, + { + "name": "standards.RestrictThirdPartyStorageServices", + "cat": "Global Standards", + "tag": ["CIS M365 5.0 (1.3.7)"], + "helpText": "Restricts third-party storage services in Microsoft 365 on the web by managing the Microsoft 365 on the web service principal. This disables integrations with services like Dropbox, Google Drive, Box, and other third-party storage providers.", + "docsDescription": "Third-party storage can be enabled for users in Microsoft 365, allowing them to store and share documents using services such as Dropbox, alongside OneDrive and team sites. This standard ensures Microsoft 365 on the web third-party storage services are restricted by creating and disabling the Microsoft 365 on the web service principal (appId: c1f33bc0-bdb4-4248-ba9b-096807ddb43e). By using external storage services an organization may increase the risk of data breaches and unauthorized access to confidential information. Additionally, third-party services may not adhere to the same security standards as the organization, making it difficult to maintain data privacy and security. Impact is highly dependent upon current practices - if users do not use other storage providers, then minimal impact is likely. However, if users regularly utilize providers outside of the tenant this will affect their ability to continue to do so.", + "executiveText": "Prevents employees from using external cloud storage services like Dropbox, Google Drive, and Box within Microsoft 365, reducing data security risks and ensuring all company data remains within controlled corporate systems. This helps maintain data governance and prevents potential data leaks to unauthorized platforms.", + "addedComponent": [], + "label": "Restrict third-party storage services in Microsoft 365 on the web", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-06-06", + "powershellEquivalent": "New-MgServicePrincipal and Update-MgServicePrincipal", + "recommendedBy": ["CIS"] }, { "name": "standards.ProfilePhotos", @@ -104,6 +148,7 @@ "tag": [], "helpText": "Controls whether users can set their own profile photos in Microsoft 365.", "docsDescription": "Controls whether users can set their own profile photos in Microsoft 365. When disabled, only User and Global administrators can update profile photos for users.", + "executiveText": "Manages user profile photo permissions within Microsoft 365, allowing organizations to control whether employees can upload their own photos or require administrative approval. This helps maintain professional appearance standards and prevents inappropriate images in corporate directories.", "addedComponent": [ { "type": "autoComplete", @@ -135,6 +180,7 @@ "cat": "Global Standards", "tag": [], "helpText": "Adds branding to the logon page that only appears if the url is not login.microsoftonline.com. This potentially prevents AITM attacks via EvilNginx. This will also automatically generate alerts if a clone of your login page has been found when set to Remediate.", + "executiveText": "Implements advanced phishing protection by adding visual indicators to login pages that help users identify legitimate Microsoft login pages versus fraudulent copies. This security measure protects against sophisticated phishing attacks that attempt to steal employee credentials.", "addedComponent": [], "label": "Enable Phishing Protection system via branding CSS", "impact": "Low Impact", @@ -146,15 +192,14 @@ "remediate": false }, "powershellEquivalent": "Portal only", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.Branding", "cat": "Global Standards", "tag": [], "helpText": "Sets the branding for the tenant. This includes the login page, and the Office 365 portal.", + "executiveText": "Customizes Microsoft 365 login pages and portals with company branding, including logos, colors, and messaging. This creates a consistent corporate identity experience for employees and reinforces brand recognition while maintaining professional appearance across all Microsoft services.", "addedComponent": [ { "type": "textField", @@ -210,27 +255,24 @@ { "name": "standards.EnableCustomerLockbox", "cat": "Global Standards", - "tag": [ - "CIS", - "CustomerLockBoxEnabled" - ], + "tag": ["CIS M365 5.0 (1.3.6)", "CustomerLockBoxEnabled"], "helpText": "Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data", "docsDescription": "Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.", + "executiveText": "Requires explicit organizational approval before Microsoft support staff can access company data for service operations. This provides an additional layer of data protection and ensures the organization maintains control over who can access sensitive business information, even during technical support scenarios.", "addedComponent": [], "label": "Enable Customer Lockbox", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-01-08", "powershellEquivalent": "Set-OrganizationConfig -CustomerLockBoxEnabled $true", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.EnablePronouns", "cat": "Global Standards", "tag": [], "helpText": "Enables the Pronouns feature for the tenant. This allows users to set their pronouns in their profile.", + "executiveText": "Allows employees to display their preferred pronouns in their Microsoft 365 profiles, supporting inclusive workplace practices and helping colleagues communicate respectfully. This feature enhances diversity and inclusion initiatives while fostering a more welcoming work environment.", "addedComponent": [], "label": "Enable Pronouns", "impact": "Low Impact", @@ -239,63 +281,78 @@ "powershellEquivalent": "Update-MgBetaAdminPeoplePronoun -IsEnabledInOrganization:$true", "recommendedBy": [] }, + { + "name": "standards.EnableNamePronunciation", + "cat": "Global Standards", + "tag": [], + "helpText": "Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile.", + "docsDescription": "Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile.", + "executiveText": "Enables employees to add pronunciation guides for their names in Microsoft 365 profiles, improving communication and respect in diverse workplaces. This feature helps colleagues pronounce names correctly, enhancing professional relationships and inclusive culture.", + "addedComponent": [], + "label": "Enable Name Pronunciation", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-06-06", + "recommendedBy": ["CIPP"] + }, { "name": "standards.AnonReportDisable", "cat": "Global Standards", "tag": [], "helpText": "Shows usernames instead of pseudo anonymised names in reports. This standard is required for reporting to work correctly.", "docsDescription": "Microsoft announced some APIs and reports no longer return names, to comply with compliance and legal requirements in specific countries. This proves an issue for a lot of MSPs because those reports are often helpful for engineers. This standard applies a setting that shows usernames in those API calls / reports.", + "executiveText": "Configures Microsoft 365 reports to display actual usernames instead of anonymized identifiers, enabling IT administrators to effectively troubleshoot issues and generate meaningful usage reports. This improves operational efficiency and system management capabilities.", "addedComponent": [], "label": "Enable Usernames instead of pseudo anonymised names in reports", "impact": "Low Impact", "impactColour": "info", "addedDate": "2021-11-16", "powershellEquivalent": "Update-MgBetaAdminReportSetting -BodyParameter @{displayConcealedNames = $true}", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.DisableGuestDirectory", "cat": "Global Standards", - "tag": [], + "tag": [ + "CIS M365 5.0 (5.1.6.2)", + "CISA (MS.AAD.5.1v1)", + "EIDSCA.AP14", + "EIDSCA.ST08", + "EIDSCA.ST09", + "NIST CSF 2.0 (PR.AA-05)" + ], "helpText": "Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory.", "docsDescription": "Sets it so guests can view only their own user profile. Permission to view other users isn't allowed. Also restricts guest users from seeing the membership of groups they're in. See exactly what get locked down in the [Microsoft documentation.](https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions)", + "executiveText": "Restricts external guest users from viewing the company's employee directory and organizational structure, protecting sensitive information about staff and internal groups. This security measure prevents unauthorized access to corporate contact information while still allowing necessary collaboration.", "addedComponent": [], "label": "Restrict guest user access to directory objects", "impact": "Low Impact", "impactColour": "info", "addedDate": "2022-05-04", "powershellEquivalent": "Set-AzureADMSAuthorizationPolicy -GuestUserRoleId '2af84b1e-32c8-42b7-82bc-daa82404023b'", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", - "tag": [], + "tag": ["CIS M365 5.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], "helpText": "Disables SMTP AUTH for the organization and all users. This is the default for new tenants.", "docsDescription": "Disables SMTP basic authentication for the tenant and all users with it explicitly enabled.", + "executiveText": "Disables outdated email authentication methods that are vulnerable to security attacks, forcing applications and devices to use modern, more secure authentication protocols. This reduces the risk of email-based security breaches and credential theft.", "addedComponent": [], "label": "Disable SMTP Basic Authentication", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2021-11-16", "powershellEquivalent": "Set-TransportConfig -SmtpClientAuthenticationDisabled $true", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.ActivityBasedTimeout", "cat": "Global Standards", - "tag": [ - "CIS", - "spo_idle_session_timeout" - ], + "tag": ["CIS M365 5.0 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"], "helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps", + "executiveText": "Automatically logs out inactive users from Microsoft 365 applications after a specified time period to prevent unauthorized access to company data on unattended devices. This security measure protects against data breaches when employees leave workstations unlocked.", "addedComponent": [ { "type": "autoComplete", @@ -332,16 +389,15 @@ "impactColour": "warning", "addedDate": "2022-04-13", "powershellEquivalent": "Portal or Graph API", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.AuthMethodsSettings", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03"], "helpText": "Configures the report suspicious activity settings and system credential preferences in the authentication methods policy.", "docsDescription": "Controls the authentication methods policy settings for reporting suspicious activity and system credential preferences. These settings help enhance the security of authentication in your organization.", + "executiveText": "Configures security settings that allow users to report suspicious login attempts and manages how the system handles authentication credentials. This enhances overall security by enabling early detection of potential security threats and optimizing authentication processes.", "addedComponent": [ { "type": "autoComplete", @@ -401,6 +457,7 @@ "tag": [], "helpText": "Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application.", "docsDescription": "Uses the CIPP functionality that deploys applications across an entire tenant base as a standard.", + "executiveText": "Automatically deploys approved business applications across all company locations and users, ensuring consistent access to essential tools and maintaining standardized software configurations. This streamlines application management and reduces IT deployment overhead.", "addedComponent": [ { "type": "select", @@ -464,40 +521,47 @@ "tag": [], "helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.", "docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.", + "executiveText": "Enables Local Administrator Password Solution (LAPS) capability, which automatically manages and rotates local administrator passwords on company computers. This significantly improves security by preventing the use of shared or static administrator passwords that could be exploited by attackers.", "addedComponent": [], "label": "Enable LAPS on the tenant", "impact": "Low Impact", "impactColour": "info", "addedDate": "2023-04-25", "powershellEquivalent": "Portal or Graph API", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.PWdisplayAppInformationRequiredState", "cat": "Entra (AAD) Standards", "tag": [ - "CIS" + "CIS M365 5.0 (2.3.1)", + "CIS M365 5.0 (5.2.3.2)", + "EIDSCA.AM03", + "EIDSCA.AM04", + "EIDSCA.AM06", + "EIDSCA.AM07", + "EIDSCA.AM09", + "EIDSCA.AM10", + "NIST CSF 2.0 (PR.AA-03)" ], "helpText": "Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name.", "docsDescription": "Allows users to use Passwordless with Number Matching and adds location information from the last request", + "executiveText": "Enhances authentication security by requiring users to match numbers and showing detailed information about login requests, including application names and location data. This helps employees verify legitimate login attempts and prevents unauthorized access through more secure authentication methods.", "addedComponent": [], "label": "Enable Passwordless with Location information and Number Matching", "impact": "Low Impact", "impactColour": "info", "addedDate": "2021-11-16", "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.allowOTPTokens", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["EIDSCA.AM02"], "helpText": "Allows you to use MS authenticator OTP token generator", "docsDescription": "Allows you to use Microsoft Authenticator OTP token generator. Useful for using the NPS extension as MFA on VPN clients.", + "executiveText": "Enables one-time password generation through Microsoft Authenticator app, providing an additional secure authentication method for employees. This is particularly useful for secure VPN access and other systems requiring multi-factor authentication.", "addedComponent": [], "label": "Enable OTP via Authenticator", "impact": "Low Impact", @@ -509,9 +573,10 @@ { "name": "standards.PWcompanionAppAllowedState", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["EIDSCA.AM01"], "helpText": "Sets the state of Authenticator Lite, Authenticator lite is a companion app for passwordless authentication.", "docsDescription": "Sets the Authenticator Lite state to enabled. This allows users to use the Authenticator Lite built into the Outlook app instead of the full Authenticator app.", + "executiveText": "Enables a simplified authentication experience by allowing users to authenticate directly through Outlook without requiring a separate authenticator app. This improves user convenience while maintaining security standards for passwordless authentication.", "addedComponent": [ { "type": "autoComplete", @@ -527,6 +592,10 @@ { "label": "Disabled", "value": "disabled" + }, + { + "label": "Microsoft managed", + "value": "default" } ] } @@ -541,18 +610,25 @@ { "name": "standards.EnableFIDO2", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": [ + "EIDSCA.AF01", + "EIDSCA.AF02", + "EIDSCA.AF03", + "EIDSCA.AF04", + "EIDSCA.AF05", + "EIDSCA.AF06", + "NIST CSF 2.0 (PR.AA-03)" + ], "helpText": "Enables the FIDO2 authenticationMethod for the tenant", "docsDescription": "Enables FIDO2 capabilities for the tenant. This allows users to use FIDO2 keys like a Yubikey for authentication.", + "executiveText": "Enables support for hardware security keys (like YubiKey) that provide the highest level of authentication security. These physical devices prevent phishing attacks and credential theft, offering superior protection for high-value accounts and sensitive business operations.", "addedComponent": [], "label": "Enable FIDO2 capabilities", "impact": "Low Impact", "impactColour": "info", "addedDate": "2022-12-08", "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.EnableHardwareOAuth", @@ -560,6 +636,7 @@ "tag": [], "helpText": "Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes.", "docsDescription": "Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication.", + "executiveText": "Enables physical hardware tokens that generate secure authentication codes, providing an alternative to smartphone-based authentication. This is particularly valuable for employees who cannot use mobile devices or require the highest security standards for accessing sensitive systems.", "addedComponent": [], "label": "Enable Hardware OAuth tokens", "impact": "Low Impact", @@ -571,9 +648,10 @@ { "name": "standards.allowOAuthTokens", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["EIDSCA.AT01", "EIDSCA.AT02"], "helpText": "Allows you to use any software OAuth token generator", "docsDescription": "Enables OTP Software OAuth tokens for the tenant. This allows users to use OTP codes generated via software, like a password manager to be used as an authentication method.", + "executiveText": "Allows employees to use third-party authentication apps and password managers to generate secure login codes, providing flexibility in authentication methods while maintaining security standards. This accommodates diverse user preferences and existing security tools.", "addedComponent": [], "label": "Enable OTP Software OAuth tokens", "impact": "Low Impact", @@ -582,12 +660,28 @@ "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", "recommendedBy": [] }, + { + "name": "standards.FormsPhishingProtection", + "cat": "Global Standards", + "tag": ["CIS M365 5.0 (1.3.5)", "Security", "PhishingProtection"], + "helpText": "Enables internal phishing protection for Microsoft Forms to help prevent malicious forms from being created and shared within the organization. This feature scans forms created by internal users for potential phishing content and suspicious patterns.", + "docsDescription": "Enables internal phishing protection for Microsoft Forms by setting the isInOrgFormsPhishingScanEnabled property to true. This security feature helps protect organizations from internal phishing attacks through Microsoft Forms by automatically scanning forms created by internal users for potential malicious content, suspicious links, and phishing patterns. When enabled, Forms will analyze form content and block or flag potentially dangerous forms before they can be shared within the organization.", + "executiveText": "Automatically scans Microsoft Forms created by employees for malicious content and phishing attempts, preventing the creation and distribution of harmful forms within the organization. This protects against both internal threats and compromised accounts that might be used to distribute malicious content.", + "addedComponent": [], + "label": "Enable internal phishing protection for Forms", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-06-06", + "powershellEquivalent": "Graph API", + "recommendedBy": ["CIS", "CIPP"] + }, { "name": "standards.TAP", "cat": "Entra (AAD) Standards", "tag": [], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", "docsDescription": "Enables Temporary Password generation for the tenant.", + "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", "addedComponent": [ { "type": "autoComplete", @@ -612,35 +706,29 @@ "impactColour": "info", "addedDate": "2022-03-15", "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.PasswordExpireDisabled", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS", - "PWAgePolicyNew" - ], + "tag": ["CIS M365 5.0 (1.3.1)", "PWAgePolicyNew"], "helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.", "docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.", + "executiveText": "Eliminates mandatory password expiration requirements, allowing employees to keep strong passwords indefinitely rather than forcing frequent changes that often lead to weaker passwords. This modern security approach reduces help desk calls and improves overall password security when combined with multi-factor authentication.", "addedComponent": [], "label": "Do not expire passwords", "impact": "Low Impact", "impactColour": "info", "addedDate": "2021-11-16", "powershellEquivalent": "Update-MgDomain", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", "tag": [], "helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.", + "executiveText": "Allows external partners and vendors to use their own organization's multi-factor authentication when accessing company resources, streamlining collaboration while maintaining security standards. This reduces friction for external users while ensuring they still meet authentication requirements.", "addedComponent": [ { "type": "autoComplete", @@ -670,30 +758,35 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (1.2.3)", "CISA (MS.AAD.6.1v1)"], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", + "executiveText": "Prevents regular employees from creating new Microsoft 365 organizations, ensuring all new tenants are properly managed and controlled by IT administrators. This prevents unauthorized shadow IT environments and maintains centralized governance over Microsoft 365 resources.", "addedComponent": [], "label": "Disable M365 Tenant creation by users", "impact": "Low Impact", "impactColour": "info", "addedDate": "2022-11-29", "powershellEquivalent": "Update-MgPolicyAuthorizationPolicy", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.EnableAppConsentRequests", "cat": "Entra (AAD) Standards", "tag": [ - "CIS" + "CIS M365 5.0 (1.5.2)", + "CISA (MS.AAD.9.1v1)", + "EIDSCA.CP04", + "EIDSCA.CR01", + "EIDSCA.CR02", + "EIDSCA.CR03", + "EIDSCA.CR04", + "Essential 8 (1507)", + "NIST CSF 2.0 (PR.AA-05)" ], "helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings", "docsDescription": "Enables the ability for users to request admin consent for applications. Should be used in conjunction with the \"Require admin consent for applications\" standards", + "executiveText": "Establishes a formal approval process where employees can request access to business applications that require administrative review. This balances security with productivity by allowing controlled access to necessary tools while preventing unauthorized application installations.", "addedComponent": [ { "type": "AdminRolesMultiSelect", @@ -706,9 +799,7 @@ "impactColour": "info", "addedDate": "2023-11-27", "powershellEquivalent": "Update-MgPolicyAdminConsentRequestPolicy", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.NudgeMFA", @@ -716,6 +807,7 @@ "tag": [], "helpText": "Sets the state of the registration campaign for the tenant", "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.", + "executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.", "addedComponent": [ { "type": "autoComplete", @@ -751,9 +843,10 @@ { "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CISA (MS.AAD.21.1v1)"], "helpText": "Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "docsDescription": "Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", + "executiveText": "Restricts the creation of Microsoft 365 groups, Teams, and SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces. This ensures proper governance, naming conventions, and resource management while maintaining oversight of all collaborative environments.", "addedComponent": [], "label": "Disable M365 Group creation by users", "impact": "Low Impact", @@ -766,26 +859,29 @@ "name": "standards.DisableAppCreation", "cat": "Entra (AAD) Standards", "tag": [ - "CIS" + "CIS M365 5.0 (1.2.2)", + "CISA (MS.AAD.4.1v1)", + "EIDSCA.AP10", + "Essential 8 (1175)", + "NIST CSF 2.0 (PR.AA-05)" ], "helpText": "Disables the ability for users to create App registrations in the tenant.", "docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.", + "executiveText": "Prevents regular employees from creating application registrations that could be used to maintain unauthorized access to company systems. This security measure ensures that only authorized IT personnel can create applications, reducing the risk of persistent security breaches through malicious applications.", "addedComponent": [], "label": "Disable App creation by users", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-03-20", "powershellEquivalent": "Update-MgPolicyAuthorizationPolicy", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.DisableSecurityGroupUsers", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)"], "helpText": "Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams", + "executiveText": "Restricts the creation of security groups to IT administrators only, preventing employees from creating unauthorized access groups that could bypass security controls. This ensures proper governance of access permissions and maintains centralized control over who can access what resources.", "addedComponent": [], "label": "Disable Security Group creation by users", "impact": "Medium Impact", @@ -799,6 +895,7 @@ "cat": "Entra (AAD) Standards", "tag": [], "helpText": "This standard currently does not function and can be safely disabled", + "executiveText": "This standard is currently non-functional and should be disabled. It was previously designed to remove outdated multi-factor authentication configurations in favor of modern security policies.", "addedComponent": [], "label": "Remove Legacy MFA if SD or CA is active", "impact": "Medium Impact", @@ -811,7 +908,8 @@ "name": "standards.DisableSelfServiceLicenses", "cat": "Entra (AAD) Standards", "tag": [], - "helpText": "This standard disables all self service licenses and enables all exclusions", + "helpText": "Note: requires 'Billing Administrator' GDAP role. This standard disables all self service licenses and enables all exclusions", + "executiveText": "Prevents employees from purchasing Microsoft 365 licenses independently, ensuring all software acquisitions go through proper procurement channels. This maintains budget control, prevents unauthorized spending, and ensures compliance with corporate licensing agreements.", "addedComponent": [ { "type": "textField", @@ -832,25 +930,29 @@ "cat": "Entra (AAD) Standards", "tag": [], "helpText": "Blocks login for guest users that have not logged in for 90 days", + "executiveText": "Automatically disables external guest accounts that haven't been used for 90 days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.", "addedComponent": [], "label": "Disable Guest accounts that have not logged on for 90 days", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2022-10-20", "powershellEquivalent": "Graph API", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.OauthConsent", "cat": "Entra (AAD) Standards", "tag": [ - "CIS" + "CIS M365 5.0 (1.5.1)", + "CISA (MS.AAD.4.2v1)", + "EIDSCA.AP08", + "EIDSCA.AP09", + "Essential 8 (1175)", + "NIST CSF 2.0 (PR.AA-05)" ], "helpText": "Disables users from being able to consent to applications, except for those specified in the field below", "docsDescription": "Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications.", + "executiveText": "Requires administrative approval before employees can grant applications access to company data, preventing unauthorized data sharing and potential security breaches. This protects against malicious applications while allowing approved business tools to function normally.", "addedComponent": [ { "type": "textField", @@ -864,19 +966,15 @@ "impactColour": "warning", "addedDate": "2021-11-16", "powershellEquivalent": "Update-MgPolicyAuthorizationPolicy", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.OauthConsentLowSec", "cat": "Entra (AAD) Standards", - "tag": [ - "IntegratedApps" - ], + "tag": ["IntegratedApps"], "helpText": "Sets the default oauth consent level so users can consent to applications that have low risks.", "docsDescription": "Allows users to consent to applications with low assigned risk.", + "executiveText": "Allows employees to approve low-risk applications without administrative intervention, balancing security with productivity. This provides a middle ground between complete restriction and open access, enabling business agility while maintaining protection against high-risk applications.", "label": "Allow users to consent to applications with low security risk (Prevent OAuth phishing. Lower impact, less secure)", "impact": "Medium Impact", "impactColour": "warning", @@ -887,8 +985,9 @@ { "name": "standards.GuestInvite", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07"], "helpText": "This setting controls who can invite guests to your directory to collaborate on resources secured by your company, such as SharePoint sites or Azure resources.", + "executiveText": "Controls who within the organization can invite external partners and vendors to access company resources, ensuring proper oversight of external access while enabling necessary business collaboration. This helps maintain security while supporting partnership and vendor relationships.", "addedComponent": [ { "type": "autoComplete", @@ -927,11 +1026,10 @@ { "name": "standards.StaleEntraDevices", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS", "Essential 8 (1501)", "NIST CSF 2.0 (ID.AM-08)", "NIST CSF 2.0 (PR.PS-03)"], "helpText": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days.", "docsDescription": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days. First disables and later deletes the devices. More info can be found in the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices)", + "executiveText": "Automatically identifies and removes inactive devices that haven't connected to company systems for a specified period, reducing security risks from abandoned or lost devices. This maintains a clean device inventory and prevents potential unauthorized access through dormant device registrations.", "addedComponent": [ { "type": "number", @@ -956,6 +1054,7 @@ "cat": "Entra (AAD) Standards", "tag": [], "helpText": "Disables App consent and set to Allow user consent for apps", + "executiveText": "Reverses application consent restrictions, allowing employees to approve applications independently without administrative oversight. This increases productivity and user autonomy but reduces security controls over data access permissions.", "addedComponent": [], "label": "Undo App Consent Standard", "impact": "High Impact", @@ -967,9 +1066,10 @@ { "name": "standards.SecurityDefaults", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CISA (MS.AAD.11.1v1)"], "helpText": "Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access.", "docsDescription": "Enables SD for the tenant, which disables all forms of basic authentication and enforces users to configure MFA. Users are only prompted for MFA when a logon is considered 'suspect' by Microsoft.", + "executiveText": "Activates Microsoft's baseline security configuration that requires multi-factor authentication and blocks legacy authentication methods. This provides essential security protection for organizations without complex conditional access policies, significantly improving security posture with minimal configuration.", "addedComponent": [], "label": "Enable Security Defaults", "impact": "High Impact", @@ -981,40 +1081,39 @@ { "name": "standards.DisableSMS", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)"], "helpText": "This blocks users from using SMS as an MFA method. If a user only has SMS as a MFA method, they will be unable to log in.", "docsDescription": "Disables SMS as an MFA method for the tenant. If a user only has SMS as a MFA method, they will be unable to sign in.", + "executiveText": "Disables SMS text messages as a multi-factor authentication method due to security vulnerabilities like SIM swapping attacks. This forces users to adopt more secure authentication methods like authenticator apps or hardware tokens, significantly improving account security.", "addedComponent": [], "label": "Disables SMS as an MFA method", "impact": "High Impact", "impactColour": "danger", "addedDate": "2023-12-18", "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.DisableVoice", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)"], "helpText": "This blocks users from using Voice call as an MFA method. If a user only has Voice as a MFA method, they will be unable to log in.", "docsDescription": "Disables Voice call as an MFA method for the tenant. If a user only has Voice call as a MFA method, they will be unable to sign in.", + "executiveText": "Disables voice call authentication due to security vulnerabilities and social engineering risks. This forces users to adopt more secure authentication methods like authenticator apps, improving overall account security by eliminating phone-based attack vectors.", "addedComponent": [], "label": "Disables Voice call as an MFA method", "impact": "High Impact", "impactColour": "danger", "addedDate": "2023-12-18", "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.DisableEmail", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CIS M365 5.0 (2.3.5)", "NIST CSF 2.0 (PR.AA-03)"], "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.", + "executiveText": "Disables email-based authentication codes due to security concerns with email interception and account compromise. This forces users to adopt more secure authentication methods, particularly affecting guest users who must use stronger verification methods.", "addedComponent": [], "label": "Disables Email as an MFA method", "impact": "High Impact", @@ -1029,6 +1128,7 @@ "tag": [], "helpText": "This blocks users from using Certificates as an MFA method.", "docsDescription": "", + "executiveText": "Disables certificate-based authentication as a multi-factor authentication method, typically used when organizations want to standardize on other authentication methods or when certificate management becomes too complex for the security benefit provided.", "addedComponent": [], "label": "Disables Certificates as an MFA method", "impact": "High Impact", @@ -1043,6 +1143,7 @@ "tag": [], "helpText": "This blocks users from using QR Code Pin as an MFA method. If a user only has QR Code Pin as a MFA method, they will be unable to log in.", "docsDescription": "Disables QR Code Pin as an MFA method for the tenant. If a user only has QR Code Pin as a MFA method, they will be unable to sign in.", + "executiveText": "Disables QR Code Pin authentication method due to security concerns, forcing users to adopt more secure authentication alternatives. This helps standardize authentication methods and reduces potential security vulnerabilities while ensuring employees use more robust multi-factor authentication options.", "addedComponent": [], "label": "Disables QR Code Pin as an MFA method", "impact": "High Impact", @@ -1054,8 +1155,19 @@ { "name": "standards.PerUserMFA", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": [ + "CIS M365 5.0 (1.2.1)", + "CIS M365 5.0 (1.1.1)", + "CIS M365 5.0 (1.1.2)", + "CISA (MS.AAD.1.1v1)", + "CISA (MS.AAD.1.2v1)", + "Essential 8 (1504)", + "Essential 8 (1173)", + "Essential 8 (1401)", + "NIST CSF 2.0 (PR.AA-03)" + ], "helpText": "Enables per user MFA for all users.", + "executiveText": "Requires all employees to use multi-factor authentication for enhanced account security, significantly reducing the risk of unauthorized access from compromised passwords. This fundamental security measure protects against the majority of account-based attacks and is essential for maintaining strong cybersecurity posture.", "addedComponent": [], "label": "Enables per user MFA for all users.", "impact": "High Impact", @@ -1079,7 +1191,7 @@ "label": "Preferred Language", "api": { "url": "/languageList.json", - "labelField": "language", + "labelField": "tag", "valueField": "tag" } } @@ -1094,9 +1206,7 @@ { "name": "standards.OutBoundSpamAlert", "cat": "Exchange Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (2.1.6)"], "helpText": "Set the Outbound Spam Alert e-mail address", "docsDescription": "Sets the e-mail address to which outbound spam alerts are sent.", "addedComponent": [ @@ -1111,9 +1221,7 @@ "impactColour": "info", "addedDate": "2023-05-03", "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.MessageExpiration", @@ -1170,15 +1278,14 @@ "tag": [], "helpText": "Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF.", "docsDescription": "Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF. Cannot be overridden by the user. For more information, see [Microsoft's documentation.](https://learn.microsoft.com/en-us/exchange/mail-flow/content-conversion/tnef-conversion?view=exchserver-2019)", + "executiveText": "Prevents the creation of winmail.dat attachments that can cause compatibility issues when sending emails to external recipients using non-Outlook email clients. This improves email compatibility and reduces support issues with external partners and customers.", "addedComponent": [], "label": "Disable TNEF/winmail.dat", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-04-26", "powershellEquivalent": "Set-RemoteDomain -Identity 'Default' -TNEFEnabled $false", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.FocusedInbox", @@ -1186,6 +1293,7 @@ "tag": [], "helpText": "Sets the default Focused Inbox state for the tenant. This can be overridden by the user.", "docsDescription": "Sets the default Focused Inbox state for the tenant. This can be overridden by the user in their Outlook settings. For more information, see [Microsoft's documentation.](https://support.microsoft.com/en-us/office/focused-inbox-for-outlook-f445ad7f-02f4-4294-a82e-71d8964e3978)", + "executiveText": "Configures the default setting for Outlook's Focused Inbox feature, which automatically sorts important emails into a focused view while placing less important emails in a separate section. This can improve employee productivity by reducing email clutter, though users can adjust this setting individually.", "addedComponent": [ { "type": "autoComplete", @@ -1217,6 +1325,7 @@ "tag": [], "helpText": "Sets the Cloud Message Recall state for the tenant. This allows users to recall messages from the cloud.", "docsDescription": "Sets the default state for Cloud Message Recall for the tenant. By default this is enabled. You can read more about the feature [here.](https://techcommunity.microsoft.com/t5/exchange-team-blog/cloud-based-message-recall-in-exchange-online/ba-p/3744714)", + "executiveText": "Enables employees to recall or retract emails they've sent, helping prevent embarrassing mistakes or accidental data sharing. This feature can reduce the impact of human errors in email communication and provides a safety net for sensitive information accidentally sent to wrong recipients.", "addedComponent": [ { "type": "autoComplete", @@ -1248,6 +1357,7 @@ "tag": [], "helpText": "Enables auto-expanding archives for the tenant", "docsDescription": "Enables auto-expanding archives for the tenant. Does not enable archives for users.", + "executiveText": "Enables automatic expansion of email archive storage when users approach their archive limits, ensuring continuous email retention without manual intervention. This prevents email storage issues and maintains compliance with data retention policies without requiring ongoing administrative management.", "addedComponent": [], "label": "Enable Auto-expanding archives", "impact": "Low Impact", @@ -1257,10 +1367,44 @@ "recommendedBy": [] }, { - "name": "standards.EnableOnlineArchiving", + "name": "standards.TwoClickEmailProtection", "cat": "Exchange Standards", "tag": [], + "helpText": "Configures the two-click confirmation requirement for viewing encrypted/protected emails in OWA and new Outlook. When enabled, users must click \"View message\" before accessing protected content, providing an additional layer of privacy protection.", + "docsDescription": "Configures the TwoClickMailPreviewEnabled setting in Exchange Online organization configuration. This security feature requires users to click \"View message\" before accessing encrypted or protected emails in Outlook on the web (OWA) and new Outlook for Windows. This provides additional privacy protection by preventing protected content from automatically displaying, giving users time to ensure their screen is not visible to others before viewing sensitive content. The feature helps protect against shoulder surfing and accidental disclosure of confidential information.", + "executiveText": "Requires employees to click twice before viewing encrypted or sensitive emails, preventing accidental exposure of confidential information when screens might be visible to others. This privacy protection helps prevent shoulder surfing and ensures employees are intentional about viewing sensitive content.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Select value", + "name": "standards.TwoClickEmailProtection.state", + "options": [ + { + "label": "Enabled", + "value": "enabled" + }, + { + "label": "Disabled", + "value": "disabled" + } + ] + } + ], + "label": "Set two-click confirmation for encrypted emails in New Outlook", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-06-13", + "powershellEquivalent": "Set-OrganizationConfig -TwoClickMailPreviewEnabled $true | $false", + "recommendedBy": [] + }, + { + "name": "standards.EnableOnlineArchiving", + "cat": "Exchange Standards", + "tag": ["Essential 8 (1511)", "NIST CSF 2.0 (PR.DS-11)"], "helpText": "Enables the In-Place Online Archive for all UserMailboxes with a valid license.", + "executiveText": "Automatically enables online email archiving for all licensed employees, providing additional storage for older emails while maintaining easy access. This helps manage mailbox sizes, improves email performance, and supports compliance with data retention requirements.", "addedComponent": [], "label": "Enable Online Archive for all users", "impact": "Low Impact", @@ -1274,6 +1418,7 @@ "cat": "Exchange Standards", "tag": [], "helpText": "Enables litigation hold for all UserMailboxes with a valid license.", + "executiveText": "Preserves all email content for legal and compliance purposes by preventing permanent deletion of emails, even when users attempt to delete them. This is essential for organizations subject to legal discovery requirements or regulatory compliance mandates.", "addedComponent": [ { "type": "textField", @@ -1292,11 +1437,10 @@ { "name": "standards.SpoofWarn", "cat": "Exchange Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (6.2.3)"], "helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA", "docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)", + "executiveText": "Displays visual warnings in Outlook when emails come from external senders, helping employees identify potentially suspicious messages and reducing the risk of phishing attacks. This security feature makes it easier for staff to distinguish between internal and external communications.", "addedComponent": [ { "type": "autoComplete", @@ -1328,19 +1472,14 @@ "impactColour": "info", "addedDate": "2021-11-16", "powershellEquivalent": "Set-ExternalInOutlook \u2013Enabled $true or $false", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.EnableMailTips", "cat": "Exchange Standards", - "tag": [ - "CIS", - "exo_mailtipsenabled" - ], + "tag": ["CIS M365 5.0 (6.5.2)", "exo_mailtipsenabled"], "helpText": "Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements", + "executiveText": "Enables helpful notifications in Outlook that warn users about potential email issues, such as sending to large groups, external recipients, or invalid addresses. This reduces email mistakes and improves communication efficiency by providing real-time guidance to employees.", "addedComponent": [ { "type": "number", @@ -1355,16 +1494,14 @@ "impactColour": "info", "addedDate": "2024-01-14", "powershellEquivalent": "Set-OrganizationConfig", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.TeamsMeetingsByDefault", "cat": "Exchange Standards", "tag": [], "helpText": "Sets the default state for automatically turning meetings into Teams meetings for the tenant. This can be overridden by the user in Outlook.", + "executiveText": "Automatically adds Microsoft Teams meeting links to calendar invitations by default, streamlining the process of creating virtual meetings. This improves collaboration efficiency and ensures consistent meeting experiences across the organization, though users can override this setting when needed.", "addedComponent": [ { "type": "autoComplete", @@ -1397,6 +1534,7 @@ "tag": [], "helpText": "Disables the daily viva reports for all users. This standard requires the CIPP-SAM application to have the Company Administrator (Global Admin) role in the tenant. Enable this using CIPP > Advanced > Super Admin > SAM App Roles. Activate the roles with a CPV refresh.", "docsDescription": "", + "executiveText": "Disables daily Microsoft Viva Insights reports that are automatically sent to employees, reducing email volume and allowing organizations to control when and how productivity insights are shared. This can help prevent information overload while maintaining the ability to access insights when needed.", "addedComponent": [], "label": "Disable daily Insight/Viva reports", "impact": "Low Impact", @@ -1408,64 +1546,91 @@ { "name": "standards.RotateDKIM", "cat": "Exchange Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (2.1.9)"], "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", + "executiveText": "Upgrades email security by replacing older 1024-bit encryption keys with stronger 2048-bit keys for email authentication. This improves the organization's email security posture and helps prevent email spoofing and tampering, maintaining trust with email recipients.", "addedComponent": [], "label": "Rotate DKIM keys that are 1024 bit to 2048 bit", "impact": "Low Impact", "impactColour": "info", "addedDate": "2023-03-14", "powershellEquivalent": "Rotate-DkimSigningConfig", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.AddDKIM", "cat": "Exchange Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (2.1.9)"], "helpText": "Enables DKIM for all domains that currently support it", + "executiveText": "Enables email authentication technology that digitally signs outgoing emails to verify they actually came from your organization. This prevents email spoofing, improves email deliverability, and protects the company's reputation by ensuring recipients can trust emails from your domains.", "addedComponent": [], "label": "Enables DKIM for all domains that currently support it", "impact": "Low Impact", "impactColour": "info", "addedDate": "2023-03-14", "powershellEquivalent": "New-DkimSigningConfig and Set-DkimSigningConfig", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] + }, + { + "name": "standards.AddDMARCToMOERA", + "cat": "Global Standards", + "tag": ["CIS M365 5.0 (2.1.10)", "Security", "PhishingProtection"], + "helpText": "Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", + "docsDescription": "Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", + "executiveText": "Implements advanced email security for Microsoft's default domain names (onmicrosoft.com) to prevent criminals from impersonating your organization. This blocks fraudulent emails that could damage your company's reputation and protects partners and customers from phishing attacks using your domain names.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": true, + "required": false, + "placeholder": "v=DMARC1; p=reject; (recommended)", + "label": "Value", + "name": "standards.AddDMARCToMOERA.RecordValue", + "options": [ + { + "label": "v=DMARC1; p=reject; (recommended)", + "value": "v=DMARC1; p=reject;" + } + ] + } + ], + "label": "Enables DMARC on MOERA (onmicrosoft.com) domains", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-06-16", + "powershellEquivalent": "Portal only", + "recommendedBy": ["CIS", "Microsoft"] }, { "name": "standards.EnableMailboxAuditing", "cat": "Exchange Standards", "tag": [ - "CIS", - "exo_mailboxaudit" + "CIS M365 5.0 (6.1.1)", + "CIS M365 5.0 (6.1.2)", + "CIS M365 5.0 (6.1.3)", + "exo_mailboxaudit", + "Essential 8 (1509)", + "Essential 8 (1683)", + "NIST CSF 2.0 (DE.CM-09)" ], "helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.", + "executiveText": "Enables comprehensive logging of all email access and modifications across all employee mailboxes, providing detailed audit trails for security investigations and compliance requirements. This helps detect unauthorized access, data breaches, and supports regulatory compliance efforts.", "addedComponent": [], "label": "Enable Mailbox auditing", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-01-08", "powershellEquivalent": "Set-OrganizationConfig -AuditDisabled $false", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.SendReceiveLimitTenant", "cat": "Exchange Standards", "tag": [], "helpText": "Sets the Send and Receive limits for new users. Valid values are 1MB to 150MB", + "executiveText": "Establishes standard email attachment size limits for all new employees, balancing functionality with system performance and security. This prevents email system overload from large attachments while ensuring employees can share necessary files through appropriate channels.", "addedComponent": [ { "type": "number", @@ -1493,6 +1658,7 @@ "tag": [], "helpText": "Sets the default sharing level for the default calendar, for all users", "docsDescription": "Sets the default sharing level for the default calendar for all users in the tenant. You can read about the different sharing levels [here.](https://learn.microsoft.com/en-us/powershell/module/exchange/set-mailboxfolderpermission?view=exchange-ps#-accessrights)", + "executiveText": "Configures how much calendar information employees share by default with colleagues, balancing collaboration needs with privacy. This setting determines whether others can see meeting details, free/busy times, or just availability, helping optimize scheduling while protecting sensitive meeting information.", "disabledFeatures": { "report": true, "warn": true, @@ -1562,11 +1728,10 @@ { "name": "standards.EXOOutboundSpamLimits", "cat": "Exchange Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (2.1.6)"], "helpText": "Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. ", "docsDescription": "Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one.", + "executiveText": "Sets limits on how many emails employees can send per hour and per day to prevent spam and protect the organization's email reputation. When limits are exceeded, the system can alert administrators or temporarily block the user, helping detect compromised accounts or prevent abuse.", "addedComponent": [ { "type": "number", @@ -1613,38 +1778,30 @@ "impactColour": "info", "addedDate": "2025-05-13", "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy", - "recommendedBy": [ - "CIPP", - "CIS" - ] + "recommendedBy": ["CIPP", "CIS"] }, { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", - "tag": [ - "CIS", - "exo_individualsharing" - ], + "tag": ["CIS M365 5.0 (1.3.3)", "exo_individualsharing"], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", + "executiveText": "Prevents employees from sharing their calendars with external parties, protecting sensitive meeting information and internal schedules from unauthorized access. This security measure helps maintain confidentiality of business activities while still allowing internal collaboration.", "addedComponent": [], "label": "Disable external calendar sharing", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-01-08", "powershellEquivalent": "Get-SharingPolicy | Set-SharingPolicy -Enabled $False", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.AutoAddProxy", "cat": "Exchange Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS"], "helpText": "Automatically adds all available domains as a proxy address.", "docsDescription": "Automatically finds all available domain names in the tenant, and tries to add proxy addresses based on the user's UPN to each of these.", + "executiveText": "Automatically creates email addresses for employees across all company domains, ensuring they can receive emails sent to any of the organization's domain names. This improves email delivery reliability and maintains consistent communication channels across different business units or brands.", "addedComponent": [], "label": "Automatically deploy proxy addresses", "impact": "Medium Impact", @@ -1661,21 +1818,17 @@ { "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", - "tag": [ - "CIS", - "exo_storageproviderrestricted" - ], + "tag": ["CIS M365 5.0 (6.5.3)", "exo_storageproviderrestricted"], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", + "executiveText": "Prevents employees from accessing personal cloud storage services like Dropbox or Google Drive through Outlook on the web, reducing data security risks and ensuring company information stays within approved corporate systems. This helps maintain data governance and prevents accidental data leaks.", "addedComponent": [], "label": "Disable additional storage providers in OWA", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-01-17", "powershellEquivalent": "Get-OwaMailboxPolicy | Set-OwaMailboxPolicy -AdditionalStorageProvidersEnabled $False", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.AntiSpamSafeList", @@ -1683,6 +1836,7 @@ "tag": [], "helpText": "Sets the anti-spam connection filter policy option 'safe list' in Defender.", "docsDescription": "Sets [Microsoft's built-in 'safe list'](https://learn.microsoft.com/en-us/powershell/module/exchange/set-hostedconnectionfilterpolicy?view=exchange-ps#-enablesafelist) in the anti-spam connection filter policy, rather than setting a custom safe/block list of IPs.", + "executiveText": "Enables Microsoft's pre-approved list of trusted email servers to improve email delivery from legitimate sources while maintaining spam protection. This reduces false positives where legitimate emails might be blocked while still protecting against spam and malicious emails.", "addedComponent": [ { "type": "switch", @@ -1693,6 +1847,7 @@ "label": "Set Anti-Spam Connection Filter Safe List", "impact": "Medium Impact", "impactColour": "info", + "tag": ["CIS M365 5.0 (2.1.13)"], "addedDate": "2025-02-15", "powershellEquivalent": "Set-HostedConnectionFilterPolicy \"Default\" -EnableSafeList $true", "recommendedBy": [] @@ -1702,6 +1857,7 @@ "cat": "Exchange Standards", "tag": [], "helpText": "Sets the shorten meetings settings on a tenant level. This will shorten meetings by the selected amount of minutes. Valid values are 0 to 29. Short meetings are under 60 minutes, long meetings are over 60 minutes.", + "executiveText": "Automatically shortens calendar meetings by a specified number of minutes to provide buffer time between meetings, reducing back-to-back scheduling stress and allowing employees time to transition between meetings. This improves work-life balance and meeting effectiveness.", "addedComponent": [ { "type": "autoComplete", @@ -1749,6 +1905,7 @@ "tag": [], "helpText": "Sets the state of Bookings on the tenant. Bookings is a scheduling tool that allows users to book appointments with others both internal and external.", "docsDescription": "", + "executiveText": "Controls whether employees can use Microsoft Bookings to create online appointment scheduling pages for internal and external clients. This feature can improve customer service and streamline appointment management, but may need to be controlled for security or business process reasons.", "addedComponent": [ { "type": "autoComplete", @@ -1780,6 +1937,7 @@ "tag": [], "helpText": "Sets the state of Direct Send in Exchange Online. Direct Send allows applications to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication.", "docsDescription": "Controls whether applications can use Direct Send to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication. A detailed explanation from Microsoft can be found [here.](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365)", + "executiveText": "Controls whether business applications and devices (like printers or scanners) can send emails through the company's email system without authentication. While this enables convenient features like scan-to-email, it may pose security risks and should be carefully managed.", "addedComponent": [ { "type": "autoComplete", @@ -1810,26 +1968,28 @@ "name": "standards.DisableOutlookAddins", "cat": "Exchange Standards", "tag": [ - "CIS", - "exo_outlookaddins" + "CIS M365 5.0 (6.3.1)", + "exo_outlookaddins", + "NIST CSF 2.0 (PR.AA-05)", + "NIST CSF 2.0 (PR.PS-05)" ], "helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.", "docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.", + "executiveText": "Prevents employees from installing third-party add-ins in Outlook without administrative approval, reducing security risks from potentially malicious extensions. This ensures only vetted and approved tools can access company email data while maintaining centralized control over email functionality.", "addedComponent": [], "label": "Disable users from installing add-ins in Outlook", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2024-02-05", "powershellEquivalent": "Get-ManagementRoleAssignment | Remove-ManagementRoleAssignment", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.SafeSendersDisable", "cat": "Exchange Standards", "tag": [], "helpText": "Loops through all users and removes the Safe Senders list. This is to prevent SPF bypass attacks, as the Safe Senders list is not checked by SPF.", + "executiveText": "Removes user-defined safe sender lists to prevent security bypasses where malicious emails could avoid spam filtering. This ensures all emails go through proper security screening, even if users have previously marked senders as 'safe', improving overall email security.", "addedComponent": [], "disabledFeatures": { "report": true, @@ -1841,9 +2001,7 @@ "impactColour": "warning", "addedDate": "2023-10-26", "powershellEquivalent": "Set-MailboxJunkEmailConfiguration", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.DelegateSentItems", @@ -1851,6 +2009,7 @@ "tag": [], "helpText": "Sets emails sent as and on behalf of shared mailboxes to also be stored in the shared mailbox sent items folder", "docsDescription": "This makes sure that e-mails sent from shared mailboxes or delegate mailboxes, end up in the mailbox of the shared/delegate mailbox instead of the sender, allowing you to keep replies in the same mailbox as the original e-mail.", + "executiveText": "Ensures emails sent from shared mailboxes (like info@company.com) are stored in the shared mailbox rather than the individual sender's mailbox. This maintains complete email threads in one location, improving collaboration and ensuring all team members can see the full conversation history.", "addedComponent": [ { "type": "switch", @@ -1871,15 +2030,14 @@ "tag": [], "helpText": "Enables the ability for users to send from their alias addresses.", "docsDescription": "Allows users to change the 'from' address to any set in their Azure AD Profile.", + "executiveText": "Allows employees to send emails from their alternative email addresses (aliases) rather than just their primary address. This is useful for employees who manage multiple roles or departments, enabling them to send emails from the most appropriate address for the context.", "addedComponent": [], "label": "Allow users to send from their alias addresses", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2022-05-25", "powershellEquivalent": "Set-Mailbox", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.UserSubmissions", @@ -1887,6 +2045,7 @@ "tag": [], "helpText": "Set the state of the spam submission button in Outlook", "docsDescription": "Set the state of the built-in Report button in Outlook. This gives the users the ability to report emails as spam or phish.", + "executiveText": "Enables employees to easily report suspicious emails directly from Outlook, helping improve the organization's spam and phishing detection systems. This crowdsourced approach to security allows users to contribute to threat detection while providing valuable feedback to enhance email security filters.", "addedComponent": [ { "type": "autoComplete", @@ -1921,49 +2080,61 @@ { "name": "standards.DisableSharedMailbox", "cat": "Exchange Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (1.2.2)", "CISA (MS.AAD.10.1v1)", "NIST CSF 2.0 (PR.AA-01)"], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", + "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.", "addedComponent": [], - "label": "Disable Shared Mailbox AAD accounts", + "label": "Disable Shared Mailbox Entra accounts", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2021-11-16", "powershellEquivalent": "Get-Mailbox & Update-MgUser", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] + }, + { + "name": "standards.DisableResourceMailbox", + "cat": "Exchange Standards", + "tag": ["CIS", "NIST CSF 2.0 (PR.AA-01)"], + "helpText": "Blocks login for all accounts that are marked as a resource mailbox and does not have a license assigned. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.", + "docsDescription": "Resource mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for resource mailboxes. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.", + "executiveText": "Prevents direct login to resource mailbox accounts (like conference rooms or equipment), ensuring they can only be managed through proper administrative channels. This security measure eliminates potential unauthorized access to resource scheduling systems while maintaining proper booking functionality.", + "addedComponent": [], + "label": "Disable Unlicensed Resource Mailbox Entra accounts", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-06-01", + "powershellEquivalent": "Get-Mailbox & Update-MgUser", + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.EXODisableAutoForwarding", "cat": "Exchange Standards", "tag": [ - "CIS", + "CIS M365 5.0 (6.2.1)", "mdo_autoforwardingmode", - "mdo_blockmailforward" + "mdo_blockmailforward", + "CISA (MS.EXO.4.1v1)", + "NIST CSF 2.0 (PR.DS-02)" ], "helpText": "Disables the ability for users to automatically forward e-mails to external recipients.", "docsDescription": "Disables the ability for users to automatically forward e-mails to external recipients. This is to prevent data exfiltration. Please check if there are any legitimate use cases for this feature before implementing, like forwarding invoices and such.", + "executiveText": "Prevents employees from automatically forwarding company emails to external addresses, protecting against data leaks and unauthorized information sharing. This security measure helps maintain control over sensitive business communications while preventing both accidental and intentional data exfiltration.", "addedComponent": [], "label": "Disable automatic forwarding to external recipients", "impact": "High Impact", "impactColour": "danger", "addedDate": "2024-07-26", "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy -AutoForwardingMode 'Off'", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.RetentionPolicyTag", "cat": "Exchange Standards", - "tag": [], + "tag": ["CIS M365 5.0 (6.4.1)"], "helpText": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", "docsDescription": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", + "executiveText": "Automatically and permanently removes deleted emails after a specified number of days, helping manage storage costs and ensuring compliance with data retention policies. This prevents accumulation of unnecessary deleted items while maintaining a reasonable recovery window for accidentally deleted emails.", "addedComponent": [ { "type": "number", @@ -1985,6 +2156,7 @@ "tag": [], "helpText": "Sets a e-mail address to alert when a User requests to release a quarantined message.", "docsDescription": "Sets a e-mail address to alert when a User requests to release a quarantined message. This is useful for monitoring and ensuring that the correct messages are released.", + "executiveText": "Notifies IT administrators when employees request to release emails that were quarantined for security reasons, enabling oversight of potentially dangerous messages. This helps ensure that legitimate emails are released while maintaining security controls over suspicious content.", "addedComponent": [ { "type": "textField", @@ -2005,6 +2177,7 @@ "tag": [], "helpText": "Sets a e-mail address to alert when a User deletes more than 20 SharePoint files within 60 minutes. NB: Requires a Office 365 E5 subscription, Office 365 E3 with Threat Intelligence or Office 365 EquivioAnalytics add-on.", "docsDescription": "Sets a e-mail address to alert when a User deletes more than 20 SharePoint files within 60 minutes. This is useful for monitoring and ensuring that the correct SharePoint files are deleted. NB: Requires a Office 365 E5 subscription, Office 365 E3 with Threat Intelligence or Office 365 EquivioAnalytics add-on.", + "executiveText": "Alerts administrators when employees delete large numbers of SharePoint files in a short time period, helping detect potential data destruction attacks, ransomware, or accidental mass deletions. This early warning system enables rapid response to protect critical business documents and data.", "addedComponent": [ { "type": "number", @@ -2034,13 +2207,44 @@ "powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert", "recommendedBy": [] }, + { + "name": "standards.SafeLinksTemplatePolicy", + "label": "SafeLinks Policy Template", + "cat": "Templates", + "multiple": false, + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + }, + "impact": "Medium Impact", + "addedDate": "2025-04-29", + "helpText": "Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.", + "executiveText": "Deploys standardized URL protection policies that automatically scan and verify links in emails and documents before users click them. This template-based approach ensures consistent protection against malicious websites and phishing attacks across the organization.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "name": "standards.SafeLinksTemplatePolicy.TemplateIds", + "label": "Select SafeLinks Policy Templates", + "api": { + "url": "/api/ListSafeLinksPolicyTemplates", + "labelField": "TemplateName", + "valueField": "GUID", + "queryKey": "ListSafeLinksPolicyTemplates" + } + } + ] + }, { "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", "tag": [ - "CIS", + "CIS M365 5.0 (2.1.1)", "mdo_safelinksforemail", - "mdo_safelinksforOfficeApps" + "mdo_safelinksforOfficeApps", + "NIST CSF 2.0 (DE.CM-09)" ], "helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", "addedComponent": [ @@ -2073,9 +2277,7 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-SafeLinksPolicy or New-SafeLinksPolicy", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.AntiPhishPolicy", @@ -2088,7 +2290,9 @@ "mdo_phisspamacation", "mdo_spam_notifications_only_for_admins", "mdo_antiphishingpolicies", - "mdo_phishthresholdlevel" + "mdo_phishthresholdlevel", + "CIS M365 5.0 (2.1.7)", + "NIST CSF 2.0 (DE.CM-09)" ], "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mail tips.", "addedComponent": [ @@ -2288,18 +2492,17 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-AntiPhishPolicy or New-AntiPhishPolicy", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.SafeAttachmentPolicy", "cat": "Defender Standards", "tag": [ - "CIS", + "CIS M365 5.0 (2.1.4)", "mdo_safedocuments", "mdo_commonattachmentsfilter", - "mdo_safeattachmentpolicy" + "mdo_safeattachmentpolicy", + "NIST CSF 2.0 (DE.CM-09)" ], "helpText": "This creates a Safe Attachment policy", "addedComponent": [ @@ -2366,16 +2569,12 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-SafeAttachmentPolicy or New-SafeAttachmentPolicy", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.AtpPolicyForO365", "cat": "Defender Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (2.1.5)", "NIST CSF 2.0 (DE.CM-09)"], "helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.", "addedComponent": [ { @@ -2391,9 +2590,7 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-AtpPolicyForO365", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.PhishingSimulations", @@ -2444,10 +2641,12 @@ "name": "standards.MalwareFilterPolicy", "cat": "Defender Standards", "tag": [ - "CIS", + "CIS M365 5.0 (2.1.2)", + "CIS M365 5.0 (2.1.3)", "mdo_zapspam", "mdo_zapphish", - "mdo_zapmalware" + "mdo_zapmalware", + "NIST CSF 2.0 (DE.CM-09)" ], "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", "addedComponent": [ @@ -2534,9 +2733,7 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-MalwareFilterPolicy or New-MalwareFilterPolicy", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.PhishSimSpoofIntelligence", @@ -2873,6 +3070,7 @@ }, "tag": [], "helpText": "This standard creates a Custom Quarantine Policies that can be used in Anti-Spam and all MDO365 policies. Quarantine Policies can be used to specify recipients permissions, enable end-user spam notifications, and specify the release action preference", + "executiveText": "Creates standardized quarantine policies that define how employees can interact with quarantined emails, including permissions to release, delete, or preview suspicious messages. This ensures consistent security handling across the organization while providing appropriate user access to manage quarantined content.", "addedComponent": [ { "type": "autoComplete", @@ -2954,6 +3152,7 @@ "cat": "Intune Standards", "tag": [], "helpText": "A value between 0 and 270 is supported. A value of 0 disables retirement, retired devices are removed from Intune after the specified number of days.", + "executiveText": "Automatically removes inactive devices from management after a specified period, helping maintain a clean device inventory and reducing security risks from abandoned or lost devices. This policy ensures that only actively used corporate devices remain in the management system.", "addedComponent": [ { "type": "number", @@ -2966,15 +3165,14 @@ "impactColour": "info", "addedDate": "2023-05-19", "powershellEquivalent": "Graph API", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] }, { "name": "standards.intuneBrandingProfile", "cat": "Intune Standards", "tag": [], "helpText": "Sets the branding profile for the Intune Company Portal app. This is a tenant wide setting and overrules any settings set on the app level.", + "executiveText": "Customizes the Intune Company Portal app with company branding, contact information, and support details, providing employees with a consistent corporate experience when managing their devices. This improves user experience and ensures employees know how to get IT support when needed.", "addedComponent": [ { "type": "textField", @@ -3048,6 +3246,7 @@ "cat": "Intune Standards", "tag": [], "helpText": "Sets the mark devices with no compliance policy assigned as compliance/non compliant and Compliance status validity period.", + "executiveText": "Configures how the system treats devices that don't have specific compliance policies and sets how often devices must check in to maintain their compliance status. This ensures proper security oversight of all corporate devices and maintains current compliance information.", "addedComponent": [ { "type": "autoComplete", @@ -3086,6 +3285,7 @@ "tag": [], "helpText": "Configures the MDM user scope. This also sets the terms of use, discovery and compliance URL to default URLs.", "docsDescription": "Configures the MDM user scope. This also sets the terms of use URL, discovery URL and compliance URL to default values.", + "executiveText": "Defines which users can enroll their devices in mobile device management, controlling access to corporate resources and applications. This setting determines the scope of device management coverage and ensures appropriate users have access to necessary business tools.", "addedComponent": [ { "name": "appliesTo", @@ -3123,8 +3323,9 @@ { "name": "standards.DefaultPlatformRestrictions", "cat": "Intune Standards", - "tag": [], + "tag": ["CISA (MS.AAD.19.1v1)"], "helpText": "Sets the default platform restrictions for enrolling devices into Intune. Note: Do not block personally owned if platform is blocked.", + "executiveText": "Controls which types of devices (iOS, Android, Windows, macOS) and ownership models (corporate vs. personal) can be enrolled in the company's device management system. This helps maintain security standards while supporting necessary business device types and usage scenarios.", "addedComponent": [ { "type": "switch", @@ -3197,8 +3398,9 @@ { "name": "standards.intuneDeviceReg", "cat": "Intune Standards", - "tag": [], + "tag": ["CISA (MS.AAD.17.1v1)"], "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users", + "executiveText": "Limits how many devices each employee can register for corporate access, preventing excessive device proliferation while accommodating legitimate business needs. This helps maintain security oversight and prevents potential abuse of device registration privileges.", "addedComponent": [ { "type": "number", @@ -3219,6 +3421,7 @@ "cat": "Intune Standards", "tag": [], "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.", + "executiveText": "Requires employees to use multi-factor authentication when registering devices for corporate access, adding an extra security layer to prevent unauthorized device enrollment. This helps ensure only legitimate users can connect their devices to company systems.", "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra", "impact": "Medium Impact", "impactColour": "warning", @@ -3232,6 +3435,7 @@ "tag": [], "helpText": "Sets the retention period for deleted users OneDrive to the specified period of time. The default is 30 days.", "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected amount of time that data can be retrieved from it.", + "executiveText": "Preserves departed employees' OneDrive files for a specified period, allowing time to recover important business documents before permanent deletion. This helps prevent data loss while managing storage costs and maintaining compliance with data retention policies.", "addedComponent": [ { "type": "autoComplete", @@ -3302,6 +3506,7 @@ "cat": "SharePoint Standards", "tag": [], "helpText": "Sets the default timezone for the tenant. This will be used for all new users and sites.", + "executiveText": "Standardizes the timezone setting across all SharePoint sites and new user accounts, ensuring consistent scheduling and time-based operations throughout the organization. This improves collaboration efficiency and reduces confusion in global or multi-timezone organizations.", "addedComponent": [ { "type": "TimezoneSelect", @@ -3319,43 +3524,37 @@ { "name": "standards.SPAzureB2B", "cat": "SharePoint Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (7.2.2)"], "helpText": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled", + "executiveText": "Enables secure collaboration with external partners through SharePoint and OneDrive by integrating with Azure B2B guest access. This allows controlled sharing with external organizations while maintaining security oversight and proper access management.", "addedComponent": [], "label": "Enable SharePoint and OneDrive integration with Azure AD B2B", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -EnableAzureADB2BIntegration $true", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", + "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.", "addedComponent": [], "label": "Disallow downloading infected files from SharePoint", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -DisallowInfectedFileDownload $true", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.SPDisableLegacyWorkflows", "cat": "SharePoint Standards", "tag": [], "helpText": "Disables the creation of new SharePoint 2010 and 2013 classic workflows and removes the 'Return to classic SharePoint' link on modern SharePoint list and library pages.", + "executiveText": "Disables outdated SharePoint workflow features and classic interface options, encouraging use of modern, more secure and efficient collaboration tools. This helps maintain security standards while guiding users toward current, supported functionality.", "addedComponent": [], "label": "Disable Legacy Workflows", "impact": "Low Impact", @@ -3367,28 +3566,23 @@ { "name": "standards.SPDirectSharing", "cat": "SharePoint Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (7.2.7)", "CISA (MS.SPO.1.4v1)"], "helpText": "Ensure default link sharing is set to Direct in SharePoint and OneDrive", + "executiveText": "Configures SharePoint and OneDrive to share files directly with specific people rather than creating anonymous links, improving security by ensuring only intended recipients can access shared documents. This reduces the risk of accidental data exposure through link sharing.", "addedComponent": [], "label": "Default sharing to Direct users", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -DefaultSharingLinkType Direct", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (7.2.9)", "CISA (MS.SPO.1.5v1)"], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", + "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.", "addedComponent": [ { "type": "number", @@ -3401,17 +3595,14 @@ "impactColour": "warning", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -ExternalUserExpireInDays 30 -ExternalUserExpirationRequired $True", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (7.2.10)", "CISA (MS.SPO.1.6v1)"], "helpText": "Ensure re-authentication with verification code is restricted", + "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.", "addedComponent": [ { "type": "number", @@ -3424,16 +3615,14 @@ "impactColour": "warning", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -EmailAttestationRequired $true -EmailAttestationReAuthDays 15", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.DisableAddShortcutsToOneDrive", "cat": "SharePoint Standards", "tag": [], "helpText": "If disabled, the button Add shortcut to OneDrive will be removed and users in the tenant will no longer be able to add new shortcuts to their OneDrive. Existing shortcuts will remain functional", + "executiveText": "Controls whether employees can create shortcuts to SharePoint libraries in their OneDrive, managing how users organize and access shared content. This setting helps maintain organized file structures and can prevent confusion from excessive shortcuts while preserving existing workflows.", "addedComponent": [ { "type": "autoComplete", @@ -3465,6 +3654,7 @@ "cat": "SharePoint Standards", "tag": [], "helpText": "If disabled, users in the tenant will no longer be able to use the Sync button to sync SharePoint content on all sites. However, existing synced content will remain functional on the user's computer.", + "executiveText": "Controls whether employees can synchronize SharePoint files to their local devices, balancing productivity benefits with data security concerns. This setting helps manage data distribution while maintaining access to cloud-based collaboration when sync is disabled.", "addedComponent": [ { "type": "autoComplete", @@ -3495,29 +3685,29 @@ "name": "standards.DisableSharePointLegacyAuth", "cat": "SharePoint Standards", "tag": [ - "CIS", - "spo_legacy_auth" + "CIS M365 5.0 (6.5.1)", + "CIS M365 5.0 (7.2.1)", + "spo_legacy_auth", + "CISA (MS.AAD.3.1v1)", + "NIST CSF 2.0 (PR.IR-01)" ], "helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.", "docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.", + "executiveText": "Disables outdated authentication methods for SharePoint access, forcing applications and users to use modern, more secure authentication protocols. This significantly improves security by eliminating vulnerable authentication pathways while requiring updates to older applications.", "addedComponent": [], "label": "Disable legacy basic authentication for SharePoint", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2024-02-05", "powershellEquivalent": "Set-SPOTenant -LegacyAuthProtocolsEnabled $false", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.sharingCapability", "cat": "SharePoint Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.AAD.14.1v1)", "CISA (MS.SPO.1.1v1)"], "helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level", + "executiveText": "Defines the organization's default policy for sharing files and folders in SharePoint and OneDrive, balancing collaboration needs with security requirements. This fundamental setting determines whether employees can share with external users, anonymous links, or only internal colleagues.", "addedComponent": [ { "type": "autoComplete", @@ -3549,29 +3739,22 @@ "impactColour": "danger", "addedDate": "2022-06-15", "powershellEquivalent": "Update-MgBetaAdminSharePointSetting", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.DisableReshare", "cat": "SharePoint Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"], "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", + "executiveText": "Prevents external users from sharing company documents with additional people, maintaining control over document distribution and preventing unauthorized access expansion. This security measure ensures that external sharing remains within intended boundaries set by internal employees.", "addedComponent": [], "label": "Disable Re-sharing by External Users", "impact": "High Impact", "impactColour": "danger", "addedDate": "2022-06-15", "powershellEquivalent": "Update-MgBetaAdminSharePointSetting", - "recommendedBy": [ - "CIS", - "CIPP" - ] + "recommendedBy": ["CIS", "CIPP"] }, { "name": "standards.DisableUserSiteCreate", @@ -3579,6 +3762,7 @@ "tag": [], "helpText": "Disables users from creating new SharePoint sites", "docsDescription": "Disables standard users from creating SharePoint sites, also disables the ability to fully create teams", + "executiveText": "Restricts the creation of new SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces and ensuring proper governance. This maintains organized information architecture while requiring approval for new collaborative environments.", "addedComponent": [], "label": "Disable site creation by standard users", "impact": "High Impact", @@ -3592,6 +3776,7 @@ "cat": "SharePoint Standards", "tag": [], "helpText": "Sets the file extensions that are excluded from syncing with OneDrive. These files will be blocked from upload. '*.' is automatically added to the extension and can be omitted.", + "executiveText": "Blocks specific file types from being uploaded or synchronized to OneDrive, helping prevent security risks from potentially dangerous file formats. This security measure protects against malware distribution while allowing legitimate business file types to be shared safely.", "addedComponent": [ { "type": "textField", @@ -3611,6 +3796,7 @@ "cat": "SharePoint Standards", "tag": [], "helpText": "Disables the ability for Mac devices to sync with OneDrive.", + "executiveText": "Prevents Mac computers from synchronizing files with OneDrive, typically implemented for security or compliance reasons in Windows-centric environments. This restriction helps maintain standardized device management while potentially limiting collaboration for Mac users.", "addedComponent": [], "label": "Do not allow Mac devices to sync using OneDrive", "impact": "High Impact", @@ -3623,22 +3809,43 @@ "name": "standards.unmanagedSync", "cat": "SharePoint Standards", "tag": [], - "helpText": "The unmanaged Sync standard has been temporarily disabled and does nothing.", - "addedComponent": [], - "label": "Only allow users to sync OneDrive from AAD joined devices", + "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.", + "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)", + "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "name": "standards.unmanagedSync.state", + "label": "State", + "options": [ + { + "label": "Allow limited, web-only access", + "value": "1" + }, + { + "label": "Block access", + "value": "2" + } + ], + "required": false + } + ], + "label": "Restrict access to SharePoint and OneDrive from unmanaged devices", "impact": "High Impact", "impactColour": "danger", - "addedDate": "2022-06-15", - "powershellEquivalent": "Update-MgAdminSharePointSetting", - "recommendedBy": [] + "addedDate": "2025-06-13", + "powershellEquivalent": "Set-SPOTenant -ConditionalAccessPolicy AllowFullAccess | AllowLimitedAccess | BlockAccess", + "recommendedBy": ["CIS"], + "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"] }, { "name": "standards.sharingDomainRestriction", "cat": "SharePoint Standards", - "tag": [ - "CIS" - ], + "tag": ["CIS M365 5.0 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"], "helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.", + "executiveText": "Controls which external domains employees can share files with, enabling secure collaboration with trusted partners while blocking sharing with unauthorized organizations. This targeted approach maintains necessary business relationships while preventing data exposure to unknown entities.", "addedComponent": [ { "type": "autoComplete", @@ -3677,8 +3884,16 @@ { "name": "standards.TeamsGlobalMeetingPolicy", "cat": "Teams Standards", - "tag": [], + "tag": [ + "CIS M365 5.0 (8.5.1)", + "CIS M365 5.0 (8.5.2)", + "CIS M365 5.0 (8.5.3)", + "CIS M365 5.0 (8.5.4)", + "CIS M365 5.0 (8.5.5)", + "CIS M365 5.0 (8.5.6)" + ], "helpText": "Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl", + "executiveText": "Establishes security-focused default settings for Teams meetings, controlling who can join meetings, present content, and participate in chats. These policies balance collaboration needs with security requirements, ensuring meetings remain productive while protecting against unauthorized access and disruption.", "addedComponent": [ { "type": "autoComplete", @@ -3744,16 +3959,14 @@ "impactColour": "info", "addedDate": "2024-11-12", "powershellEquivalent": "Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting $false -AllowAnonymousUsersToStartMeeting $false -AutoAdmittedUsers EveryoneInCompanyExcludingGuests -AllowPSTNUsersToBypassLobby $false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode $DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl $false", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.TeamsEmailIntegration", "cat": "Teams Standards", - "tag": [], "helpText": "Should users be allowed to send emails directly to a channel email addresses?", "docsDescription": "Teams channel email addresses are an optional feature that allows users to email the Teams channel directly.", + "executiveText": "Controls whether Teams channels can receive emails directly, enabling integration between email and team collaboration. This feature can improve workflow efficiency by allowing external communications to flow into team discussions, though it may need management for security or organizational reasons.", "addedComponent": [ { "type": "switch", @@ -3766,9 +3979,8 @@ "impactColour": "info", "addedDate": "2024-07-30", "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"], + "tag": ["CIS M365 5.0 (8.1.2)"] }, { "name": "standards.TeamsGuestAccess", @@ -3776,6 +3988,7 @@ "tag": [], "helpText": "Allow guest users access to teams.", "docsDescription": "Allow guest users access to teams. Guest users are users who are not part of your organization but have been invited to collaborate with your organization in Teams. This setting allows you to control whether guest users can access Teams.", + "executiveText": "Determines whether external partners, vendors, and collaborators can be invited to participate in Teams conversations and meetings. This fundamental setting enables external collaboration while requiring careful management to balance openness with security and information protection.", "addedComponent": [ { "type": "switch", @@ -3791,10 +4004,44 @@ "recommendedBy": [] }, { - "name": "standards.TeamsExternalFileSharing", + "name": "standards.TeamsMeetingVerification", "cat": "Teams Standards", "tag": [], + "helpText": "Configures CAPTCHA verification for external users joining Teams meetings. This helps prevent unauthorized AI notetakers and bots from joining meetings.", + "docsDescription": "Configures CAPTCHA verification for external users joining Teams meetings. This security feature requires external participants to complete a CAPTCHA challenge before joining, which helps prevent unauthorized AI notetakers, bots, and other automated systems from accessing meetings.", + "executiveText": "Requires external meeting participants to complete verification challenges before joining Teams meetings, preventing automated bots and unauthorized AI systems from accessing confidential discussions. This security measure protects against meeting infiltration while maintaining legitimate external collaboration.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "CAPTCHA Verification Setting", + "name": "standards.TeamsMeetingVerification.CaptchaVerificationForMeetingJoin", + "options": [ + { + "label": "Not Required", + "value": "NotRequired" + }, + { + "label": "Anonymous Users and Untrusted Organizations", + "value": "AnonymousUsersAndUntrustedOrganizations" + } + ] + } + ], + "label": "Teams Meeting Verification (CAPTCHA)", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-06-14", + "powershellEquivalent": "Set-CsTeamsMeetingPolicy -CaptchaVerificationForMeetingJoin", + "recommendedBy": ["CIPP"] + }, + { + "name": "standards.TeamsExternalFileSharing", + "cat": "Teams Standards", + "tag": ["CIS M365 5.0 (8.4.1)"], "helpText": "Ensure external file sharing in Teams is enabled for only approved cloud storage services.", + "executiveText": "Controls which external cloud storage services (like Google Drive, Dropbox, Box) employees can access through Teams, ensuring file sharing occurs only through approved and secure platforms. This helps maintain data governance while supporting necessary business integrations.", "addedComponent": [ { "type": "switch", @@ -3827,9 +4074,7 @@ "impactColour": "info", "addedDate": "2024-07-28", "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowGoogleDrive $false -AllowShareFile $false -AllowBox $false -AllowDropBox $false -AllowEgnyte $false", - "recommendedBy": [ - "CIS" - ] + "recommendedBy": ["CIS"] }, { "name": "standards.TeamsEnrollUser", @@ -3837,6 +4082,7 @@ "tag": [], "helpText": "Controls whether users with this policy can set the voice profile capture and enrollment through the Recognition tab in their Teams client settings.", "docsDescription": "Controls whether users with this policy can set the voice profile capture and enrollment through the Recognition tab in their Teams client settings.", + "executiveText": "Determines whether employees can enroll their voice and face profiles for recognition features in Teams, enabling personalized experiences like voice identification. This setting balances convenience features with privacy considerations and organizational policies regarding biometric data collection.", "addedComponent": [ { "type": "autoComplete", @@ -3870,6 +4116,7 @@ "tag": [], "helpText": "Sets the properties of the Global external access policy.", "docsDescription": "Sets the properties of the Global external access policy. External access policies determine whether or not your users can: 1) communicate with users who have Session Initiation Protocol (SIP) accounts with a federated organization; 2) communicate with users who are using custom applications built with Azure Communication Services; 3) access Skype for Business Server over the Internet, without having to log on to your internal network; 4) communicate with users who have SIP accounts with a public instant messaging (IM) provider such as Skype; and, 5) communicate with people who are using Teams with an account that's not managed by an organization.", + "executiveText": "Defines the organization's policy for communicating with external users through Teams, including other organizations, Skype users, and unmanaged accounts. This fundamental setting determines the scope of external collaboration while maintaining security boundaries for business communications.", "addedComponent": [ { "type": "switch", @@ -3895,6 +4142,7 @@ "tag": [], "helpText": "Sets the properties of the Global federation configuration.", "docsDescription": "Sets the properties of the Global federation configuration. Federation configuration settings determine whether or not your users can communicate with users who have SIP accounts with a federated organization.", + "executiveText": "Configures how the organization federates with external organizations for Teams communication, controlling whether employees can communicate with specific external domains or all external organizations. This setting enables secure inter-organizational collaboration while maintaining control over external communications.", "addedComponent": [ { "type": "switch", @@ -3947,6 +4195,7 @@ "tag": [], "helpText": "Sets the default number of days after which Teams meeting recordings automatically expire. Valid values are -1 (Never Expire) or between 1 and 99999. The default value is 120 days.", "docsDescription": "Allows administrators to configure a default expiration period (in days) for Teams meeting recordings. Recordings older than this period will be automatically moved to the recycle bin. This setting helps manage storage consumption and enforce data retention policies.", + "executiveText": "Automatically removes old Teams meeting recordings after a specified period to manage storage costs and comply with data retention policies. This helps organizations balance the need to preserve important meeting content with storage efficiency and regulatory compliance requirements.", "addedComponent": [ { "type": "number", @@ -3968,6 +4217,7 @@ "tag": [], "helpText": "Sets the properties of the Global messaging policy.", "docsDescription": "Sets the properties of the Global messaging policy. Messaging policies control which chat and channel messaging features are available to users in Teams.", + "executiveText": "Defines what messaging capabilities employees have in Teams, including the ability to edit or delete messages, create custom emojis, and report inappropriate content. These policies help maintain professional communication standards while enabling necessary collaboration features.", "addedComponent": [ { "type": "switch", @@ -4058,6 +4308,7 @@ }, "helpText": "Deploy the Autopilot Status Page, which shows progress during device setup through Autopilot.", "docsDescription": "This standard allows configuration of the Autopilot Status Page, providing users with a visual representation of the progress during device setup. It includes options like timeout, logging, and retry settings.", + "executiveText": "Provides employees with a visual progress indicator during automated device setup, improving the user experience when receiving new computers. This reduces IT support calls and helps ensure successful device deployment by guiding users through the setup process.", "addedComponent": [ { "type": "number", @@ -4145,7 +4396,8 @@ { "type": "textField", "name": "standards.AutopilotProfile.DeviceNameTemplate", - "label": "Unique Device Name Template" + "label": "Unique Device Name Template", + "required": false }, { "type": "autoComplete", @@ -4234,6 +4486,7 @@ "impact": "High Impact", "addedDate": "2023-12-30", "helpText": "Deploy and manage Intune templates across devices.", + "executiveText": "Deploys standardized device management configurations across all corporate devices, ensuring consistent security policies, application settings, and compliance requirements. This template-based approach streamlines device management while maintaining uniform security standards across the organization.", "addedComponent": [ { "type": "autoComplete", @@ -4302,6 +4555,7 @@ "impact": "Medium Impact", "addedDate": "2023-12-30", "helpText": "Deploy transport rules to manage email flow.", + "executiveText": "Deploys standardized email flow rules that automatically manage how emails are processed, filtered, and routed within the organization. These templates ensure consistent email security policies, compliance requirements, and business rules are applied across all email communications.", "addedComponent": [ { "type": "autoComplete", @@ -4329,6 +4583,7 @@ "impact": "High Impact", "addedDate": "2023-12-30", "helpText": "Manage conditional access policies for better security.", + "executiveText": "Deploys standardized conditional access policies that automatically enforce security requirements based on user location, device compliance, and risk factors. These templates ensure consistent security controls across the organization while enabling secure access to business resources.", "addedComponent": [ { "type": "autoComplete", @@ -4379,6 +4634,7 @@ "impact": "Medium Impact", "addedDate": "2023-12-30", "helpText": "Deploy and manage Exchange connectors.", + "executiveText": "Configures standardized Exchange connectors that control how email flows between your organization and external systems. These templates ensure secure and reliable email delivery while maintaining proper routing and security policies for business communications.", "addedComponent": [ { "type": "autoComplete", @@ -4406,6 +4662,7 @@ "impact": "Medium Impact", "addedDate": "2023-12-30", "helpText": "Deploy and manage group templates.", + "executiveText": "Creates standardized groups with predefined settings, permissions, and membership rules. These templates ensure consistent group configurations across the organization, streamlining collaboration and access management while maintaining security standards.", "addedComponent": [ { "type": "autoComplete", @@ -4427,6 +4684,7 @@ "tag": [], "helpText": "Sets the maximum number of recipients that can be specified in the To, Cc, and Bcc fields of a message for all mailboxes in the tenant.", "docsDescription": "This standard configures the recipient limits for all mailboxes in the tenant. The recipient limit determines the maximum number of recipients that can be specified in the To, Cc, and Bcc fields of a message. This helps prevent spam and manage email flow.", + "executiveText": "Controls how many recipients employees can include in a single email, helping prevent spam distribution and managing email server load. This security measure protects against both accidental mass mailings and potential abuse while ensuring legitimate business communications can still reach necessary recipients.", "addedComponent": [ { "type": "number", @@ -4440,8 +4698,20 @@ "impactColour": "info", "addedDate": "2025-05-28", "powershellEquivalent": "Set-Mailbox -RecipientLimits", - "recommendedBy": [ - "CIPP" - ] + "recommendedBy": ["CIPP"] + }, + { + "name": "standards.DisableExchangeOnlinePowerShell", + "cat": "Exchange Standards", + "tag": ["CIS M365 5.0 (6.1.1)", "Security", "NIST CSF 2.0 (PR.AA-05)"], + "helpText": "Disables Exchange Online PowerShell access for non-admin users by setting the RemotePowerShellEnabled property to false for each user. This helps prevent attackers from using PowerShell to run malicious commands, access file systems, registry, and distribute ransomware throughout networks. Users with admin roles are automatically excluded.", + "docsDescription": "Disables Exchange Online PowerShell access for non-admin users by setting the RemotePowerShellEnabled property to false for each user. This security measure follows a least privileged access approach, preventing potential attackers from using PowerShell to execute malicious commands, access sensitive systems, or distribute malware. Users with management roles containing 'Admin' are automatically excluded to ensure administrators retain PowerShell access to perform necessary management tasks.", + "executiveText": "Restricts PowerShell access to Exchange Online for regular employees while maintaining access for administrators, significantly reducing security risks from compromised accounts. This prevents attackers from using PowerShell to execute malicious commands or distribute ransomware while preserving necessary administrative capabilities.", + "label": "Disable Exchange Online PowerShell for non-admin users", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-06-19", + "powershellEquivalent": "Set-User -Identity $user -RemotePowerShellEnabled $false", + "recommendedBy": ["CIS", "CIPP"] } -] \ No newline at end of file +] diff --git a/src/data/timezoneList.json b/src/data/timezoneList.json index 57238fe7eedc..d2501fca5bca 100644 --- a/src/data/timezoneList.json +++ b/src/data/timezoneList.json @@ -1,335 +1,446 @@ [ { - "timezone": "(UTC-12:00) International Date Line West" + "timezone": "(UTC-12:00) International Date Line West", + "standardTime": "Dateline Standard Time" }, { - "timezone": "(UTC-11:00) Coordinated Universal Time-11" + "timezone": "(UTC-11:00) Coordinated Universal Time-11", + "standardTime": "UTC-11" }, { - "timezone": "(UTC-10:00) Hawaii" + "timezone": "(UTC-10:00) Hawaii", + "standardTime": "Hawaiian Standard Time" }, { - "timezone": "(UTC-09:00) Alaska" + "timezone": "(UTC-09:00) Alaska", + "standardTime": "Alaskan Standard Time" }, { - "timezone": "(UTC-08:00) Baja California" + "timezone": "(UTC-08:00) Baja California", + "standardTime": "Pacific Standard Time (Mexico)" }, { - "timezone": "(UTC-08:00) Pacific Time (US and Canada)" + "timezone": "(UTC-08:00) Pacific Time (US and Canada)", + "standardTime": "Pacific Standard Time" }, { - "timezone": "(UTC-07:00) Arizona" + "timezone": "(UTC-07:00) Arizona", + "standardTime": "US Mountain Standard Time" }, { - "timezone": "(UTC-07:00) Chihuahua, La Paz, Mazatlan" + "timezone": "(UTC-07:00) Chihuahua, La Paz, Mazatlan", + "standardTime": "Mountain Standard Time (Mexico)" }, { - "timezone": "(UTC-07:00) Mountain Time (US and Canada)" + "timezone": "(UTC-07:00) Mountain Time (US and Canada)", + "standardTime": "Mountain Standard Time" }, { - "timezone": "(UTC-06:00) Central America" + "timezone": "(UTC-06:00) Central America", + "standardTime": "Central America Standard Time" }, { - "timezone": "(UTC-06:00) Central Time (US and Canada)" + "timezone": "(UTC-06:00) Central Time (US and Canada)", + "standardTime": "Central Standard Time" }, { - "timezone": "(UTC-06:00) Guadalajara, Mexico City, Monterrey" + "timezone": "(UTC-06:00) Guadalajara, Mexico City, Monterrey", + "standardTime": "Central Standard Time (Mexico)" }, { - "timezone": "(UTC-06:00) Saskatchewan" + "timezone": "(UTC-06:00) Saskatchewan", + "standardTime": "Canada Central Standard Time" }, { - "timezone": "(UTC-05:00) Bogota, Lima, Quito" + "timezone": "(UTC-05:00) Bogota, Lima, Quito", + "standardTime": "SA Pacific Standard Time" }, { - "timezone": "(UTC-05:00) Eastern Time (US and Canada)" + "timezone": "(UTC-05:00) Eastern Time (US and Canada)", + "standardTime": "Eastern Standard Time" }, { - "timezone": "(UTC-05:00) Indiana (East)" + "timezone": "(UTC-05:00) Indiana (East)", + "standardTime": "US Eastern Standard Time" }, { - "timezone": "(UTC-04:30) Caracas" + "timezone": "(UTC-04:30) Caracas", + "standardTime": "Venezuela Standard Time" }, { - "timezone": "(UTC-04:00) Asuncion" + "timezone": "(UTC-04:00) Asuncion", + "standardTime": "Paraguay Standard Time" }, { - "timezone": "(UTC-04:00) Atlantic Time (Canada)" + "timezone": "(UTC-04:00) Atlantic Time (Canada)", + "standardTime": "Atlantic Standard Time" }, { - "timezone": "(UTC-04:00) Cuiaba" + "timezone": "(UTC-04:00) Cuiaba", + "standardTime": "Central Brazilian Standard Time" }, { - "timezone": "(UTC-04:00) Georgetown, La Paz, Manaus, San Juan" + "timezone": "(UTC-04:00) Georgetown, La Paz, Manaus, San Juan", + "standardTime": "SA Western Standard Time" }, { - "timezone": "(UTC-04:00) Santiago" + "timezone": "(UTC-04:00) Santiago", + "standardTime": "Pacific SA Standard Time" }, { - "timezone": "(UTC-03:30) Newfoundland" + "timezone": "(UTC-03:30) Newfoundland", + "standardTime": "Newfoundland Standard Time" }, { - "timezone": "(UTC-03:00) Brasilia" + "timezone": "(UTC-03:00) Brasilia", + "standardTime": "E. South America Standard Time" }, { - "timezone": "(UTC-03:00) Buenos Aires" + "timezone": "(UTC-03:00) Buenos Aires", + "standardTime": "Argentina Standard Time" }, { - "timezone": "(UTC-03:00) Cayenne, Fortaleza" + "timezone": "(UTC-03:00) Cayenne, Fortaleza", + "standardTime": "SA Eastern Standard Time" }, { - "timezone": "(UTC-03:00) Greenland" + "timezone": "(UTC-03:00) Greenland", + "standardTime": "Greenland Standard Time" }, { - "timezone": "(UTC-03:00) Montevideo" + "timezone": "(UTC-03:00) Montevideo", + "standardTime": "Montevideo Standard Time" }, { - "timezone": "(UTC-03:00) Salvador" + "timezone": "(UTC-03:00) Salvador", + "standardTime": "Bahia Standard Time" }, { - "timezone": "(UTC-02:00) Coordinated Universal Time-02" + "timezone": "(UTC-02:00) Coordinated Universal Time-02", + "standardTime": "UTC-02" }, { - "timezone": "(UTC-02:00) Mid-Atlantic" + "timezone": "(UTC-02:00) Mid-Atlantic", + "standardTime": "Mid-Atlantic Standard Time" }, { - "timezone": "(UTC-01:00) Azores" + "timezone": "(UTC-01:00) Azores", + "standardTime": "Azores Standard Time" }, { - "timezone": "(UTC-01:00) Cabo Verde" + "timezone": "(UTC-01:00) Cabo Verde", + "standardTime": "Cape Verde Standard Time" }, { - "timezone": "(UTC) Casablanca" + "timezone": "(UTC) Casablanca", + "standardTime": "Morocco Standard Time" }, { - "timezone": "(UTC) Coordinated Universal Time" + "timezone": "(UTC) Coordinated Universal Time", + "standardTime": "UTC" }, { - "timezone": "(UTC) Dublin, Edinburgh, Lisbon, London" + "timezone": "(UTC) Dublin, Edinburgh, Lisbon, London", + "standardTime": "GMT Standard Time" }, { - "timezone": "(UTC) Monrovia, Reykjavik" + "timezone": "(UTC) Monrovia, Reykjavik", + "standardTime": "Greenwich Standard Time" }, { - "timezone": "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna" + "timezone": "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna", + "standardTime": "W. Europe Standard Time" }, { - "timezone": "(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague" + "timezone": "(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague", + "standardTime": "Central Europe Standard Time" }, { - "timezone": "(UTC+01:00) Brussels, Copenhagen, Madrid, Paris" + "timezone": "(UTC+01:00) Brussels, Copenhagen, Madrid, Paris", + "standardTime": "Romance Standard Time" }, { - "timezone": "(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb" + "timezone": "(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb", + "standardTime": "Central European Standard Time" }, { - "timezone": "(UTC+01:00) West Central Africa" + "timezone": "(UTC+01:00) West Central Africa", + "standardTime": "W. Central Africa Standard Time" }, { - "timezone": "(UTC+01:00) Windhoek" + "timezone": "(UTC+01:00) Windhoek", + "standardTime": "Namibia Standard Time" }, { - "timezone": "(UTC+02:00) Amman" + "timezone": "(UTC+02:00) Amman", + "standardTime": "Jordan Standard Time" }, { - "timezone": "(UTC+02:00) Athens, Bucharest" + "timezone": "(UTC+02:00) Athens, Bucharest", + "standardTime": "GTB Standard Time" }, { - "timezone": "(UTC+02:00) Beirut" + "timezone": "(UTC+02:00) Beirut", + "standardTime": "Middle East Standard Time" }, { - "timezone": "(UTC+02:00) Cairo" + "timezone": "(UTC+02:00) Cairo", + "standardTime": "Egypt Standard Time" }, { - "timezone": "(UTC+02:00) Damascus" + "timezone": "(UTC+02:00) Damascus", + "standardTime": "Syria Standard Time" }, { - "timezone": "(UTC+02:00) Harare, Pretoria" + "timezone": "(UTC+02:00) Harare, Pretoria", + "standardTime": "South Africa Standard Time" }, { - "timezone": "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius" + "timezone": "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius", + "standardTime": "FLE Standard Time" }, { - "timezone": "(UTC+02:00) Jerusalem" + "timezone": "(UTC+02:00) Jerusalem", + "standardTime": "Israel Standard Time" }, { - "timezone": "(UTC+02:00) Minsk (old)" + "timezone": "(UTC+02:00) Minsk (old)", + "standardTime": "Belarus Standard Time" }, { - "timezone": "(UTC+02:00) E. Europe" + "timezone": "(UTC+02:00) E. Europe", + "standardTime": "E. Europe Standard Time" }, { - "timezone": "(UTC+02:00) Kaliningrad" + "timezone": "(UTC+02:00) Kaliningrad", + "standardTime": "Kaliningrad Standard Time" }, { - "timezone": "(UTC+03:00) Baghdad" + "timezone": "(UTC+03:00) Baghdad", + "standardTime": "Arabic Standard Time" }, { - "timezone": "(UTC+03:00) Istanbul" + "timezone": "(UTC+03:00) Istanbul", + "standardTime": "Turkey Standard Time" }, { - "timezone": "(UTC+03:00) Kuwait, Riyadh" + "timezone": "(UTC+03:00) Kuwait, Riyadh", + "standardTime": "Arab Standard Time" }, { - "timezone": "(UTC+03:00) Minsk" + "timezone": "(UTC+03:00) Minsk", + "standardTime": "Belarus Standard Time" }, { - "timezone": "(UTC+03:00) Moscow, St. Petersburg, Volgograd" + "timezone": "(UTC+03:00) Moscow, St. Petersburg, Volgograd", + "standardTime": "Russian Standard Time" }, { - "timezone": "(UTC+03:00) Nairobi" + "timezone": "(UTC+03:00) Nairobi", + "standardTime": "E. Africa Standard Time" }, { - "timezone": "(UTC+03:30) Tehran" + "timezone": "(UTC+03:30) Tehran", + "standardTime": "Iran Standard Time" }, { - "timezone": "(UTC+04:00) Abu Dhabi, Muscat" + "timezone": "(UTC+04:00) Abu Dhabi, Muscat", + "standardTime": "Arabian Standard Time" }, { - "timezone": "(UTC+04:00) Astrakhan, Ulyanovsk" + "timezone": "(UTC+04:00) Astrakhan, Ulyanovsk", + "standardTime": "Astrakhan Standard Time" }, { - "timezone": "(UTC+04:00) Baku" + "timezone": "(UTC+04:00) Baku", + "standardTime": "Azerbaijan Standard Time" }, { - "timezone": "(UTC+04:00) Izhevsk, Samara" + "timezone": "(UTC+04:00) Izhevsk, Samara", + "standardTime": "Russia Time Zone 3" }, { - "timezone": "(UTC+04:00) Port Louis" + "timezone": "(UTC+04:00) Port Louis", + "standardTime": "Mauritius Standard Time" }, { - "timezone": "(UTC+04:00) Tbilisi" + "timezone": "(UTC+04:00) Tbilisi", + "standardTime": "Georgian Standard Time" }, { - "timezone": "(UTC+04:00) Yerevan" + "timezone": "(UTC+04:00) Yerevan", + "standardTime": "Caucasus Standard Time" }, { - "timezone": "(UTC+04:30) Kabul" + "timezone": "(UTC+04:30) Kabul", + "standardTime": "Afghanistan Standard Time" }, { - "timezone": "(UTC+05:00) Ekaterinburg" + "timezone": "(UTC+05:00) Ekaterinburg", + "standardTime": "Ekaterinburg Standard Time" }, { - "timezone": "(UTC+05:00) Islamabad, Karachi" + "timezone": "(UTC+05:00) Islamabad, Karachi", + "standardTime": "Pakistan Standard Time" }, { - "timezone": "(UTC+05:00) Tashkent" + "timezone": "(UTC+05:00) Tashkent", + "standardTime": "West Asia Standard Time" }, { - "timezone": "(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi" + "timezone": "(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi", + "standardTime": "India Standard Time" }, { - "timezone": "(UTC+05:30) Sri Jayawardenepura" + "timezone": "(UTC+05:30) Sri Jayawardenepura", + "standardTime": "Sri Lanka Standard Time" }, { - "timezone": "(UTC+05:45) Kathmandu" + "timezone": "(UTC+05:45) Kathmandu", + "standardTime": "Nepal Standard Time" }, { - "timezone": "(UTC+06:00) Astana" + "timezone": "(UTC+06:00) Astana", + "standardTime": "Qyzylorda Standard Time" }, { - "timezone": "(UTC+06:00) Dhaka" + "timezone": "(UTC+06:00) Dhaka", + "standardTime": "Bangladesh Standard Time" }, { - "timezone": "(UTC+06:00) Omsk" + "timezone": "(UTC+06:00) Omsk", + "standardTime": "Omsk Standard Time" }, { - "timezone": "(UTC+06:30) Yangon (Rangoon)" + "timezone": "(UTC+06:30) Yangon (Rangoon)", + "standardTime": "Myanmar Standard Time" }, { - "timezone": "(UTC+07:00) Bangkok, Hanoi, Jakarta" + "timezone": "(UTC+07:00) Bangkok, Hanoi, Jakarta", + "standardTime": "SE Asia Standard Time" }, { - "timezone": "(UTC+07:00) Barnaul, Gorno-Altaysk" + "timezone": "(UTC+07:00) Barnaul, Gorno-Altaysk", + "standardTime": "Altai Standard Time" }, { - "timezone": "(UTC+07:00) Krasnoyarsk" + "timezone": "(UTC+07:00) Krasnoyarsk", + "standardTime": "North Asia Standard Time" }, { - "timezone": "(UTC+07:00) Novosibirsk" + "timezone": "(UTC+07:00) Novosibirsk", + "standardTime": "N. Central Asia Standard Time" }, { - "timezone": "(UTC+07:00) Tomsk" + "timezone": "(UTC+07:00) Tomsk", + "standardTime": "Tomsk Standard Time" }, { - "timezone": "(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi" + "timezone": "(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi", + "standardTime": "China Standard Time" }, { - "timezone": "(UTC+08:00) Irkutsk" + "timezone": "(UTC+08:00) Irkutsk", + "standardTime": "North Asia East Standard Time" }, { - "timezone": "(UTC+08:00) Kuala Lumpur, Singapore" + "timezone": "(UTC+08:00) Kuala Lumpur, Singapore", + "standardTime": "Singapore Standard Time" }, { - "timezone": "(UTC+08:00) Perth" + "timezone": "(UTC+08:00) Perth", + "standardTime": "W. Australia Standard Time" }, { - "timezone": "(UTC+08:00) Taipei" + "timezone": "(UTC+08:00) Taipei", + "standardTime": "Taipei Standard Time" }, { - "timezone": "(UTC+08:00) Ulaanbaatar" + "timezone": "(UTC+08:00) Ulaanbaatar", + "standardTime": "Ulaanbaatar Standard Time" }, { - "timezone": "(UTC+09:00) Osaka, Sapporo, Tokyo" + "timezone": "(UTC+09:00) Osaka, Sapporo, Tokyo", + "standardTime": "Tokyo Standard Time" }, { - "timezone": "(UTC+09:00) Seoul" + "timezone": "(UTC+09:00) Seoul", + "standardTime": "Korea Standard Time" }, { - "timezone": "(UTC+09:00) Yakutsk" + "timezone": "(UTC+09:00) Yakutsk", + "standardTime": "Yakutsk Standard Time" }, { - "timezone": "(UTC+09:30) Adelaide" + "timezone": "(UTC+09:30) Adelaide", + "standardTime": "Cen. Australia Standard Time" }, { - "timezone": "(UTC+09:30) Darwin" + "timezone": "(UTC+09:30) Darwin", + "standardTime": "AUS Central Standard Time" }, { - "timezone": "(UTC+10:00) Brisbane" + "timezone": "(UTC+10:00) Brisbane", + "standardTime": "E. Australia Standard Time" }, { - "timezone": "(UTC+10:00) Canberra, Melbourne, Sydney" + "timezone": "(UTC+10:00) Canberra, Melbourne, Sydney", + "standardTime": "AUS Eastern Standard Time" }, { - "timezone": "(UTC+10:00) Guam, Port Moresby" + "timezone": "(UTC+10:00) Guam, Port Moresby", + "standardTime": "West Pacific Standard Time" }, { - "timezone": "(UTC+10:00) Hobart" + "timezone": "(UTC+10:00) Hobart", + "standardTime": "Tasmania Standard Time" }, { - "timezone": "(UTC+10:00) Magadan" + "timezone": "(UTC+10:00) Magadan", + "standardTime": "Magadan Standard Time" }, { - "timezone": "(UTC+10:00) Vladivostok" + "timezone": "(UTC+10:00) Vladivostok", + "standardTime": "Vladivostok Standard Time" }, { - "timezone": "(UTC+11:00) Chokurdakh" + "timezone": "(UTC+11:00) Chokurdakh", + "standardTime": "Russia Time Zone 10" }, { - "timezone": "(UTC+11:00) Sakhalin" + "timezone": "(UTC+11:00) Sakhalin", + "standardTime": "Sakhalin Standard Time" }, { - "timezone": "(UTC+11:00) Solomon Is., New Caledonia" + "timezone": "(UTC+11:00) Solomon Is., New Caledonia", + "standardTime": "Central Pacific Standard Time" }, { - "timezone": "(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky" + "timezone": "(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky", + "standardTime": "Russia Time Zone 11" }, { - "timezone": "(UTC+12:00) Auckland, Wellington" + "timezone": "(UTC+12:00) Auckland, Wellington", + "standardTime": "New Zealand Standard Time" }, { - "timezone": "(UTC+12:00) Coordinated Universal Time+12" + "timezone": "(UTC+12:00) Coordinated Universal Time+12", + "standardTime": "UTC+12" }, { - "timezone": "(UTC+12:00) Fiji" + "timezone": "(UTC+12:00) Fiji", + "standardTime": "Fiji Standard Time" }, { - "timezone": "(UTC+12:00) Petropavlovsk-Kamchatsky - Old" + "timezone": "(UTC+12:00) Petropavlovsk-Kamchatsky - Old", + "standardTime": "Kamchatka Standard Time" }, { - "timezone": "(UTC+13:00) Nuku'alofa" + "timezone": "(UTC+13:00) Nuku'alofa", + "standardTime": "Tonga Standard Time" }, { - "timezone": "(UTC+13:00) Samoa" + "timezone": "(UTC+13:00) Samoa", + "standardTime": "Samoa Standard Time" } ] diff --git a/src/hooks/use-page-view.js b/src/hooks/use-page-view.js index c4365337c92a..fe5923c47679 100644 --- a/src/hooks/use-page-view.js +++ b/src/hooks/use-page-view.js @@ -1,3 +1 @@ -import { useEffect } from "react"; - export const usePageView = () => {}; diff --git a/src/hooks/use-securescore.js b/src/hooks/use-securescore.js index bea4554f4506..37fc9224aec3 100644 --- a/src/hooks/use-securescore.js +++ b/src/hooks/use-securescore.js @@ -89,12 +89,10 @@ export function useSecureScore() { (secureScoreData.currentScore / secureScoreData.maxScore) * 100 ), percentageVsAllTenants: Math.round( - (secureScoreData.averageComparativeScores?.[0]?.averageScore / secureScoreData.maxScore) * - 100 + secureScoreData.averageComparativeScores?.[0]?.averageScore ), percentageVsSimilar: Math.round( - (secureScoreData.averageComparativeScores?.[1]?.averageScore / secureScoreData.maxScore) * - 100 + secureScoreData.averageComparativeScores?.[1]?.averageScore ), controlScores: updatedControlScores, }); diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index 1f2b77713237..9594443127bc 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -1,5 +1,5 @@ import { usePathname, useRouter } from "next/navigation"; -import { Box, Container, Divider, Stack, Tab, Tabs, Typography } from "@mui/material"; +import { Box, Divider, Stack, Tab, Tabs } from "@mui/material"; export const TabbedLayout = (props) => { const { tabOptions, children } = props; diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js index eac39587c104..cc0da2049060 100644 --- a/src/layouts/account-popover.js +++ b/src/layouts/account-popover.js @@ -16,7 +16,6 @@ import { ListItemIcon, ListItemText, Popover, - Skeleton, Stack, SvgIcon, Typography, diff --git a/src/layouts/config.js b/src/layouts/config.js index c853befc8a08..e51850cee974 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -1,12 +1,9 @@ import { BuildingOfficeIcon, HomeIcon, UsersIcon, WrenchIcon } from "@heroicons/react/24/outline"; import { - Cloud, CloudOutlined, - DeviceHub, HomeRepairService, Laptop, MailOutline, - Shield, ShieldOutlined, } from "@mui/icons-material"; import { SvgIcon } from "@mui/material"; @@ -210,6 +207,14 @@ export const nativeMenuItems = [ }, ], }, + { + title: "Safe Links", + path: "/security/safelinks", + items: [ + { title: "Safe Links Policies", path: "/security/safelinks/safelinks" }, + { title: "Safe Links Templates", path: "/security/safelinks/safelinks-template" }, + ], + }, ], }, { @@ -308,6 +313,7 @@ export const nativeMenuItems = [ { title: "Deleted Mailboxes", path: "/email/administration/deleted-mailboxes" }, { title: "Mailbox Rules", path: "/email/administration/mailbox-rules" }, { title: "Contacts", path: "/email/administration/contacts" }, + { title: "Contact Templates", path: "/email/administration/contacts-template" }, { title: "Quarantine", path: "/email/administration/quarantine" }, { title: "Tenant Allow/Block Lists", @@ -352,6 +358,7 @@ export const nativeMenuItems = [ title: "Resource Management", path: "/email/resources/management", items: [ + { title: "Equipment", path: "/email/resources/management/equipment" }, { title: "Rooms", path: "/email/resources/management/list-rooms" }, { title: "Room Lists", path: "/email/resources/management/room-lists" }, ], diff --git a/src/layouts/footer.js b/src/layouts/footer.js index 07115518e388..d6bd01193b56 100644 --- a/src/layouts/footer.js +++ b/src/layouts/footer.js @@ -1,4 +1,4 @@ -import { Box, Container, Divider, Typography } from "@mui/material"; +import { Container } from "@mui/material"; export const Footer = () => { diff --git a/src/layouts/index.js b/src/layouts/index.js index 0b2f011034b1..b99f427af9b5 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -15,7 +15,6 @@ import { CippImageCard } from "../components/CippCards/CippImageCard"; import Page from "../pages/onboardingv2"; import { useDialog } from "../hooks/use-dialog"; import { nativeMenuItems } from "/src/layouts/config"; -import { keepPreviousData } from "@tanstack/react-query"; const SIDE_NAV_WIDTH = 270; const SIDE_NAV_PINNED_WIDTH = 50; @@ -31,13 +30,9 @@ const useMobileNav = () => { } }, [open]); - useEffect( - () => { - handlePathnameChange(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [pathname] - ); + useEffect(() => { + handlePathnameChange(); + }, [pathname]); const handleOpen = useCallback(() => { setOpen(true); @@ -144,13 +139,11 @@ export const Layout = (props) => { const userSettingsAPI = ApiGetCall({ url: "/api/ListUserSettings", queryKey: "userSettings", - refetchOnMount: false, - refetchOnReconnect: false, - keepPreviousData: true, }); useEffect(() => { if (userSettingsAPI.isSuccess && !userSettingsAPI.isFetching && !userSettingsComplete) { + console.log("User Settings API Data:", userSettingsAPI.data); //if userSettingsAPI.data contains offboardingDefaults.user, delete that specific key. if (userSettingsAPI.data.offboardingDefaults?.user) { delete userSettingsAPI.data.offboardingDefaults.user; @@ -275,7 +268,7 @@ export const Layout = (props) => { - + { priority: 1, }, { - link: "https://rightofboom.com", - imagesrc: theme === "light" ? "/sponsors/RoB-light.svg" : "/sponsors/RoB.png", + link: "https://www.domotz.com/cipp-community-free-domotz-beta.php?utm_source=Community_CIPP&utm_medium=Community_CIPP&utm_campaign=Community_CIPP", + imagesrc: theme === "light" ? "/sponsors/domotz-light.png" : "/sponsors/domotz-dark.png", priority: 1, }, { @@ -226,14 +226,20 @@ export const SideNav = (props) => { sx={{ display: "flex", justifyContent: "center", + alignItems: "center", + height: "55px", // Fixed height for the container }} > sponsor window.open(randomimg.link)} - width={"100px"} /> diff --git a/src/pages/401.js b/src/pages/401.js index 0787504e2319..913107b2a202 100644 --- a/src/pages/401.js +++ b/src/pages/401.js @@ -26,7 +26,7 @@ const Page = () => ( alignItems="center" // Center vertically sx={{ height: "100%" }} // Ensure the container takes full height > - + ( alignItems="center" // Center vertically sx={{ height: "100%" }} // Ensure the container takes full height > - + { alignItems="center" sx={{ height: "100%" }} > - + { @@ -96,7 +95,7 @@ const ApiOfflinePage = () => { alignItems="center" // Center vertically sx={{ height: "100%" }} // Ensure the container takes full height > - + ( alignItems="center" // Center vertically sx={{ height: "100%" }} // Ensure the container takes full height > - + { {/* Tenant Filter */} - + {/* Compliance Filter */} - + { /> {/* AsApp Filter */} - + { /> {/* Submit Button */} - + diff --git a/src/pages/cipp/advanced/table-maintenance.js b/src/pages/cipp/advanced/table-maintenance.js index d6cb9caf1a2b..d7bc058697ee 100644 --- a/src/pages/cipp/advanced/table-maintenance.js +++ b/src/pages/cipp/advanced/table-maintenance.js @@ -283,7 +283,7 @@ const Page = () => { that should only be used when directed by CyberDrain support. - + { } /> - + {selectedTable && ( diff --git a/src/pages/cipp/custom-data/directory-extensions/index.js b/src/pages/cipp/custom-data/directory-extensions/index.js index 42317151ffec..891342e97ce1 100644 --- a/src/pages/cipp/custom-data/directory-extensions/index.js +++ b/src/pages/cipp/custom-data/directory-extensions/index.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Alert, Button, Link, SvgIcon, Typography } from "@mui/material"; +import { Alert, Button, Link, SvgIcon } from "@mui/material"; import { Add } from "@mui/icons-material"; import tabOptions from "../tabOptions"; import NextLink from "next/link"; diff --git a/src/pages/cipp/custom-data/mappings/edit.js b/src/pages/cipp/custom-data/mappings/edit.js index a8a3cfcbee85..10bce76f56f2 100644 --- a/src/pages/cipp/custom-data/mappings/edit.js +++ b/src/pages/cipp/custom-data/mappings/edit.js @@ -4,12 +4,9 @@ import { useRouter } from "next/router"; import { useEffect } from "react"; import { ApiPostCall, ApiGetCall } from "/src/api/ApiCall"; import { - Box, Button, Stack, CardContent, - Typography, - Divider, CardActions, Skeleton, } from "@mui/material"; diff --git a/src/pages/cipp/custom-data/mappings/index.js b/src/pages/cipp/custom-data/mappings/index.js index 8992accda6f0..7579685d1f62 100644 --- a/src/pages/cipp/custom-data/mappings/index.js +++ b/src/pages/cipp/custom-data/mappings/index.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Alert, Button, Link, SvgIcon, Typography } from "@mui/material"; +import { Alert, Button, SvgIcon, Typography } from "@mui/material"; import { Add } from "@mui/icons-material"; import tabOptions from "../tabOptions"; import NextLink from "next/link"; diff --git a/src/pages/cipp/custom-data/schema-extensions/index.js b/src/pages/cipp/custom-data/schema-extensions/index.js index 07b185e5ce21..f7b5729187ba 100644 --- a/src/pages/cipp/custom-data/schema-extensions/index.js +++ b/src/pages/cipp/custom-data/schema-extensions/index.js @@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Alert, Button, Link, SvgIcon, Typography } from "@mui/material"; -import { Add, Block, CheckCircleOutline, Sync } from "@mui/icons-material"; +import { Add, Block, CheckCircleOutline } from "@mui/icons-material"; import tabOptions from "../tabOptions"; import { TrashIcon } from "@heroicons/react/24/outline"; import NextLink from "next/link"; diff --git a/src/pages/cipp/integrations/configure.js b/src/pages/cipp/integrations/configure.js index 54ed857b3443..3702ae108926 100644 --- a/src/pages/cipp/integrations/configure.js +++ b/src/pages/cipp/integrations/configure.js @@ -193,13 +193,12 @@ const Page = () => { {extension?.links && ( <> {extension.links.map((link, index) => ( - + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/email/administration/contacts/add.jsx b/src/pages/email/administration/contacts/add.jsx index 2feea32e286e..3ce17ef0d9d0 100644 --- a/src/pages/email/administration/contacts/add.jsx +++ b/src/pages/email/administration/contacts/add.jsx @@ -1,11 +1,6 @@ -import React from "react"; -import { Divider } from "@mui/material"; -import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; -import { useSelector } from "react-redux"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "../../../../hooks/use-settings"; const AddContact = () => { @@ -19,6 +14,17 @@ const AddContact = () => { lastName: "", email: "", hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", }, }); @@ -31,20 +37,30 @@ const AddContact = () => { postUrl="/api/AddContact" resetForm={true} customDataformatter={(values) => { - // Add tenantDomain to the payload return { tenantID: tenantDomain, - firstName: values.firstName, - lastName: values.lastName, - displayName: values.displayName, - email: values.email, + DisplayName: values.displayName, hidefromGAL: values.hidefromGAL, + email: values.email, + FirstName: values.firstName, + LastName: values.lastName, + Title: values.jobTitle, + StreetAddress: values.streetAddress, + PostalCode: values.postalCode, + City: values.city, + State: values.state, + CountryOrRegion: values.country?.value || values.country, + Company: values.companyName, + mobilePhone: values.mobilePhone, + phone: values.businessPhone, + website: values.website, + mailTip: values.mailTip, }; }} > {/* Display Name */} - + { {/* First Name and Last Name */} - + { formControl={formControl} /> - + { {/* Email */} - + { {/* Hide from GAL */} - + [country.Name, country.Code]) +); + const EditContact = () => { const tenantDomain = useSettings().currentTenant; const router = useRouter(); const { id } = router.query; - + const contactInfo = ApiGetCall({ url: `/api/ListContacts?tenantFilter=${tenantDomain}&id=${id}`, queryKey: `ListContacts-${id}`, - waiting: false, + waiting: !!id, }); - useEffect(() => { - if (id) { - contactInfo.refetch(); - } - }, [router.query, id, tenantDomain]); + const defaultFormValues = useMemo(() => ({ + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }), []); const formControl = useForm({ mode: "onChange", - defaultValues: { - displayName: "", - firstName: "", - lastName: "", - email: "", - hidefromGAL: false, - streetAddress: "", - postalCode: "", - city: "", - country: "", - companyName: "", - mobilePhone: "", - businessPhone: "", - jobTitle: "", - }, + defaultValues: defaultFormValues, }); - useEffect(() => { - if (contactInfo.isSuccess && contactInfo.data?.[0]) { - const contact = contactInfo.data[0]; - // Get the address info from the first address entry - const address = contact.addresses?.[0] || {}; + // Memoize processed contact data + const processedContactData = useMemo(() => { + if (!contactInfo.isSuccess || !contactInfo.data) { + return null; + } + + const contact = contactInfo.data; + const address = contact.addresses?.[0] || {}; + const phones = contact.phones || []; + + // Use Map for O(1) phone lookup + const phoneMap = new Map(phones.map(p => [p.type, p.number])); - // Find phone numbers by type - const phones = contact.phones || []; - const mobilePhone = phones.find((p) => p.type === "mobile")?.number; - const businessPhone = phones.find((p) => p.type === "business")?.number; + return { + displayName: contact.displayName || "", + firstName: contact.givenName || "", + lastName: contact.surname || "", + email: contact.mail || "", + hidefromGAL: contact.hidefromGAL || false, + streetAddress: address.street || "", + postalCode: address.postalCode || "", + city: address.city || "", + state: address.state || "", + country: address.countryOrRegion + ? countryLookup.get(address.countryOrRegion) || "" + : "", + companyName: contact.companyName || "", + mobilePhone: phoneMap.get("mobile") || "", + businessPhone: phoneMap.get("business") || "", + jobTitle: contact.jobTitle || "", + website: contact.website || "", + mailTip: contact.mailTip || "", + }; + }, [contactInfo.isSuccess, contactInfo.data]); - formControl.reset({ - displayName: contact.displayName || "", - firstName: contact.givenName || "", - lastName: contact.surname || "", - email: contact.mail || "", - hidefromGAL: contact.hidefromGAL || false, - streetAddress: address.street || "", - postalCode: address.postalCode || "", - city: address.city || "", - country: address.countryOrRegion - ? countryList.find((c) => c.Name === address.countryOrRegion)?.Code || "" - : "", - companyName: contact.companyName || "", - mobilePhone: mobilePhone || "", - businessPhone: businessPhone || "", - jobTitle: contact.jobTitle || "", - }); + // Use callback to prevent unnecessary re-renders + const resetForm = useCallback(() => { + if (processedContactData) { + formControl.reset(processedContactData); } - }, [contactInfo.isSuccess, contactInfo.data, contactInfo.isFetching]); + }, [processedContactData, formControl]); + + useEffect(() => { + resetForm(); + }, [resetForm]); - if (contactInfo.isLoading) { - return
Loading...
; - } + // Memoize custom data formatter + const customDataFormatter = useCallback((values) => { + const contact = Array.isArray(contactInfo.data) ? contactInfo.data[0] : contactInfo.data; + return { + tenantID: tenantDomain, + ContactID: contact?.id, + DisplayName: values.displayName, + hidefromGAL: values.hidefromGAL, + email: values.email, + FirstName: values.firstName, + LastName: values.lastName, + Title: values.jobTitle, + StreetAddress: values.streetAddress, + PostalCode: values.postalCode, + City: values.city, + State: values.state, + CountryOrRegion: values.country?.value || values.country, + Company: values.companyName, + mobilePhone: values.mobilePhone, + phone: values.businessPhone, + website: values.website, + mailTip: values.mailTip, + }; + }, [tenantDomain, contactInfo.data]); + + const contact = Array.isArray(contactInfo.data) ? contactInfo.data[0] : contactInfo.data; return ( { - return { - tenantID: tenantDomain, - ContactID: contactInfo.data?.[0]?.id, - DisplayName: values.displayName, - hidefromGAL: values.hidefromGAL, - email: values.email, - FirstName: values.firstName, - LastName: values.lastName, - Title: values.jobTitle, - StreetAddress: values.streetAddress, - PostalCode: values.postalCode, - City: values.city, - CountryOrRegion: values.country?.value || values.country, - Company: values.companyName, - mobilePhone: values.mobilePhone, - phone: values.businessPhone, - }; - }} + data={contact} + customDataformatter={customDataFormatter} > {/* Display Name */} - + { {/* First Name and Last Name */} - + { formControl={formControl} /> - + { {/* Email */} - + { {/* Hide from GAL */} - + { {/* Company Information */} - + { formControl={formControl} /> - + { {/* Address Information */} - + { formControl={formControl} /> - + - + { formControl={formControl} /> - + { {/* Phone Numbers */} - + { formControl={formControl} /> - + { const pageTitle = "Contacts"; - - const actions = [ + const actions = useMemo(() => [ { label: "Edit Contact", - link: "/email/administration/contacts/edit?id=[id]", + link: "/email/administration/contacts/edit?id=[Guid]", multiPost: false, postEntireRow: true, icon: , color: "warning", - condition: (row) => !row.onPremisesSyncEnabled, + condition: (row) => !row.IsDirSynced, }, { label: "Remove Contact", type: "POST", url: "/api/RemoveContact", data: { - GUID: "id", - mail: "mail", + GUID: "Guid", + mail: "WindowsEmailAddress", }, confirmText: "Are you sure you want to delete this contact? Remember this will not work if the contact is AD Synced.", color: "danger", icon: , - condition: (row) => !row.onPremisesSyncEnabled, + condition: (row) => !row.IsDirSynced, }, - ]; + ], []); + + const simpleColumns = useMemo(() => [ + "DisplayName", + "WindowsEmailAddress", + "Company", + "IsDirSynced" + ], []); - const simpleColumns = ["displayName", "mail", "companyName", "onPremisesSyncEnabled"]; + const cardButton = useMemo(() => ( + + ), []); return ( { apiUrl="/api/ListContacts" actions={actions} simpleColumns={simpleColumns} - cardButton={ - <> - - - } + cardButton={cardButton} /> ); }; diff --git a/src/pages/email/administration/mailbox-rules/index.js b/src/pages/email/administration/mailbox-rules/index.js index ba6a59439ea4..956d9df8aa38 100644 --- a/src/pages/email/administration/mailbox-rules/index.js +++ b/src/pages/email/administration/mailbox-rules/index.js @@ -1,6 +1,5 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { TrashIcon } from "@heroicons/react/24/outline"; import { getCippTranslation } from "../../../../utils/get-cipp-translation"; import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; diff --git a/src/pages/email/administration/mailboxes/addshared.jsx b/src/pages/email/administration/mailboxes/addshared.jsx index b14e962e435d..7c1e00885b84 100644 --- a/src/pages/email/administration/mailboxes/addshared.jsx +++ b/src/pages/email/administration/mailboxes/addshared.jsx @@ -1,8 +1,6 @@ -import React from "react"; import { Divider } from "@mui/material"; import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; -import { useSelector } from "react-redux"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -30,6 +28,7 @@ const AddContact = () => { title="Shared Mailbox" backButtonTitle="Mailbox Overview" postUrl="/api/AddSharedMailbox" + resetForm={true} customDataformatter={(values) => { return { tenantID: tenantDomain, @@ -40,7 +39,7 @@ const AddContact = () => { }} > - + { {/* Email */} - + { formControl={formControl} /> - + diff --git a/src/pages/email/administration/quarantine/index.js b/src/pages/email/administration/quarantine/index.js index e8eb9cf6603c..3009973001a0 100644 --- a/src/pages/email/administration/quarantine/index.js +++ b/src/pages/email/administration/quarantine/index.js @@ -10,7 +10,7 @@ import { Typography, CircularProgress, } from "@mui/material"; -import { Block, Close, Done, DoneAll, Subject } from "@mui/icons-material"; +import { Block, Close, Done, DoneAll } from "@mui/icons-material"; import { CippMessageViewer } from "/src/components/CippComponents/CippMessageViewer.jsx"; import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; import { useSettings } from "/src/hooks/use-settings"; diff --git a/src/pages/email/administration/tenant-allow-block-lists/add.jsx b/src/pages/email/administration/tenant-allow-block-lists/add.jsx index 1fecb155bd9f..ad8fda8cd4f6 100644 --- a/src/pages/email/administration/tenant-allow-block-lists/add.jsx +++ b/src/pages/email/administration/tenant-allow-block-lists/add.jsx @@ -180,7 +180,7 @@ const AddTenantAllowBlockList = () => { > {/* Entries */} - + { /> {/* Notes & List Type */} - + { formControl={formControl} /> - + { {/* List Method */} - + { {/* No Expiration */} - + { {/* Remove After */} - + { - const pageTitle = "List of Safe Link Filters"; - const apiUrl = "/api/ListSafeLinksFilters"; - - const actions = [ - { - label: "Enable Rule", - type: "POST", - icon: , - url: "/api/EditSafeLinksFilter", - data: { - State: "Enable", - RuleName: "RuleName", - }, - confirmText: "Are you sure you want to enable this rule?", - color: "info", - condition: (row) => row.State === "Disabled", - }, - { - label: "Disable Rule", - type: "POST", - icon: , - url: "/api/EditSafeLinksFilter", - data: { - State: "Disable", - RuleName: "RuleName", - }, - confirmText: "Are you sure you want to disable this rule?", - color: "info", - condition: (row) => row.State === "Enabled", - }, - /* TODO: implement Delete Rule action - { - label: "Delete Rule", - type: "GET", - url: "/api/EditSafeLinksFilter", - data: { - RuleName: "RuleName", - }, - confirmText: "Are you sure you want to delete this rule?", - color: "danger", - }, - */ - ]; - - const offCanvas = { - extendedInfoFields: ["RuleName", "Name", "State", "WhenCreated", "WhenChanged"], - actions: actions, // Attaching actions to offCanvas per original design - }; - - const simpleColumns = [ - "RuleName", - "Name", - "State", - "Priority", - "RecipientDomainIs", - "EnableSafeLinksForEmail", - "EnableSafeLinksForTeams", - "EnableSafeLinksForOffice", - "TrackClicks", - "ScanUrls", - "EnableForInternalSenders", - "DeliverMessageAfterScan", - "AllowClickThrough", - "DisableUrlRewrite", - "EnableOrganizationBranding", - "WhenCreated", - "WhenChanged", - ]; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; -export default Page; diff --git a/src/pages/email/resources/management/equipment/add.jsx b/src/pages/email/resources/management/equipment/add.jsx new file mode 100644 index 000000000000..a186a19e0ba5 --- /dev/null +++ b/src/pages/email/resources/management/equipment/add.jsx @@ -0,0 +1,83 @@ +import React from "react"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormDomainSelector"; +import { useSettings } from "/src/hooks/use-settings"; + +const AddEquipmentMailbox = () => { + const tenantDomain = useSettings().currentTenant; + const formControl = useForm({ + mode: "onChange", + defaultValues: { + displayName: "", + username: "", + domain: null, + location: "", + department: "", + company: "", + }, + }); + + return ( + { + const shippedValues = { + tenantID: tenantDomain, + domain: values.domain?.value, + displayName: values.displayName.trim(), + username: values.username.trim(), + userPrincipalName: values.username.trim() + "@" + (values.domain?.value || "").trim(), + }; + + return shippedValues; + }} + > + + {/* Display Name */} + + + + + + + {/* Username and Domain */} + + + + + + + + + ); +}; + +AddEquipmentMailbox.getLayout = (page) => {page}; + +export default AddEquipmentMailbox; diff --git a/src/pages/email/resources/management/equipment/edit.jsx b/src/pages/email/resources/management/equipment/edit.jsx new file mode 100644 index 000000000000..046aaa04cb07 --- /dev/null +++ b/src/pages/email/resources/management/equipment/edit.jsx @@ -0,0 +1,436 @@ +import React, { useEffect } from "react"; +import { Divider, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "/src/api/ApiCall"; +import countryList from "/src/data/countryList.json"; +import timezoneList from "/src/data/timezoneList.json"; + +// Work days options +const workDaysOptions = [ + { value: "Sunday", label: "Sunday" }, + { value: "Monday", label: "Monday" }, + { value: "Tuesday", label: "Tuesday" }, + { value: "Wednesday", label: "Wednesday" }, + { value: "Thursday", label: "Thursday" }, + { value: "Friday", label: "Friday" }, + { value: "Saturday", label: "Saturday" }, + { value: "WeekDay", label: "Weekdays (Monday-Friday)" }, + { value: "WeekendDay", label: "Weekend (Saturday-Sunday)" }, + { value: "AllDays", label: "All Days" }, +]; + +// Automation Processing Options +const automateProcessingOptions = [ + { value: "None", label: "None - No processing" }, + { value: "AutoUpdate", label: "AutoUpdate - Accept/Decline but not delete" }, + { value: "AutoAccept", label: "AutoAccept - Accept and delete" }, +]; + +const EditEquipmentMailbox = () => { + const router = useRouter(); + const { equipmentId } = router.query; + const tenantDomain = useSettings().currentTenant; + const formControl = useForm({ + mode: "onChange", + }); + + const equipmentInfo = ApiGetCall({ + url: `/api/ListEquipment?EquipmentId=${equipmentId}&tenantFilter=${tenantDomain}`, + queryKey: `Equipment-${equipmentId}`, + waiting: false, + }); + + useEffect(() => { + if (equipmentInfo.isSuccess && equipmentInfo.data?.[0]) { + const equipment = equipmentInfo.data[0]; + formControl.reset({ + // Core Properties + displayName: equipment.displayName, + hiddenFromAddressListsEnabled: equipment.hiddenFromAddressListsEnabled, + + // Equipment Details + department: equipment.department, + company: equipment.company, + + // Location Information + streetAddress: equipment.streetAddress, + city: equipment.city, + stateOrProvince: equipment.stateOrProvince, + postalCode: equipment.postalCode, + countryOrRegion: equipment.countryOrRegion + ? countryList.find((c) => c.Name === equipment.countryOrRegion)?.Code || "" + : "", + phone: equipment.phone, + tags: equipment.tags?.map((tag) => ({ label: tag, value: tag })) || [], + + // Booking Information + allowConflicts: equipment.allowConflicts, + allowRecurringMeetings: equipment.allowRecurringMeetings, + bookingWindowInDays: equipment.bookingWindowInDays, + maximumDurationInMinutes: equipment.maximumDurationInMinutes, + processExternalMeetingMessages: equipment.processExternalMeetingMessages, + forwardRequestsToDelegates: equipment.forwardRequestsToDelegates, + scheduleOnlyDuringWorkHours: equipment.scheduleOnlyDuringWorkHours, + automateProcessing: equipment.automateProcessing, + + // Calendar Configuration + workDays: + equipment.workDays?.split(",")?.map((day) => ({ + label: day.trim(), + value: day.trim(), + })) || [], + workHoursStartTime: equipment.workHoursStartTime, + workHoursEndTime: equipment.workHoursEndTime, + workingHoursTimeZone: equipment.workingHoursTimeZone + ? { + value: equipment.workingHoursTimeZone, + label: timezoneList.find((tz) => tz.standardTime === equipment.workingHoursTimeZone) + ? `${equipment.workingHoursTimeZone} - ${ + timezoneList.find((tz) => tz.standardTime === equipment.workingHoursTimeZone) + ?.timezone + }` + : equipment.workingHoursTimeZone, + } + : null, + }); + } + }, [equipmentInfo.isSuccess, equipmentInfo.data]); + + useEffect(() => { + if (equipmentId) { + equipmentInfo.refetch(); + } + }, [router.query, equipmentId, tenantDomain]); + + return ( + ({ + tenantID: tenantDomain, + equipmentId: equipmentId, + displayName: values.displayName?.trim(), + hiddenFromAddressListsEnabled: values.hiddenFromAddressListsEnabled, + + // Equipment Details + department: values.department?.trim(), + company: values.company?.trim(), + + // Location Information + streetAddress: values.streetAddress?.trim(), + city: values.city?.trim(), + stateOrProvince: values.stateOrProvince?.trim(), + postalCode: values.postalCode?.trim(), + countryOrRegion: values.countryOrRegion?.value || values.countryOrRegion || null, + phone: values.phone?.trim(), + tags: values.tags?.map((tag) => tag.value), + + // Booking Information + allowConflicts: values.allowConflicts, + allowRecurringMeetings: values.allowRecurringMeetings, + bookingWindowInDays: values.bookingWindowInDays, + maximumDurationInMinutes: values.maximumDurationInMinutes, + processExternalMeetingMessages: values.processExternalMeetingMessages, + forwardRequestsToDelegates: values.forwardRequestsToDelegates, + scheduleOnlyDuringWorkHours: values.scheduleOnlyDuringWorkHours, + automateProcessing: values.automateProcessing?.value || values.automateProcessing, + + // Calendar Configuration + workDays: values.workDays?.map((day) => day.value).join(","), + workHoursStartTime: values.workHoursStartTime, + workHoursEndTime: values.workHoursEndTime, + workingHoursTimeZone: values.workingHoursTimeZone?.value || values.workingHoursTimeZone, + })} + > + + {/* Basic Information */} + + + Basic Information + + + + + + + + + + + + + + {/* Booking Information */} + + + Booking Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Working Hours */} + + + Working Hours + + + + + + + + + + + + + ({ + value: tz.standardTime, + label: `${tz.standardTime} - ${tz.timezone}`, + }))} + multiple={false} + creatable={false} + formControl={formControl} + /> + + + + + + + + + + + + + {/* Equipment & Location Details */} + + + Equipment & Location Details + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + + + + + + + + + + ); +}; + +EditEquipmentMailbox.getLayout = (page) => {page}; + +export default EditEquipmentMailbox; diff --git a/src/pages/email/resources/management/equipment/index.js b/src/pages/email/resources/management/equipment/index.js new file mode 100644 index 000000000000..9a3f97e29cc5 --- /dev/null +++ b/src/pages/email/resources/management/equipment/index.js @@ -0,0 +1,83 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Button } from "@mui/material"; +import Link from "next/link"; +import { AddBusiness, Edit, Block, LockOpen, Key } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; + +const Page = () => { + const pageTitle = "Equipment"; + + const actions = [ + { + label: "Edit Equipment", + link: `/email/resources/management/equipment/edit?equipmentId=[ExternalDirectoryObjectId]`, + icon: , + color: "info", + condition: (row) => !row.isDirSynced, + }, + { + label: "Edit permissions", + link: "/identity/administration/users/user/exchange?userId=[ExternalDirectoryObjectId]", + color: "info", + icon: , + }, + { + label: "Block Sign In", + type: "POST", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "ExternalDirectoryObjectId" }, + confirmText: "Are you sure you want to block the sign-in for this equipment mailbox?", + multiPost: false, + condition: (row) => !row.isDirSynced, + }, + { + label: "Unblock Sign In", + type: "POST", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "ExternalDirectoryObjectId", Enable: true }, + confirmText: "Are you sure you want to unblock sign-in for this equipment mailbox?", + multiPost: false, + condition: (row) => !row.isDirSynced, + }, + { + label: "Delete Equipment", + type: "POST", + icon: , + url: "/api/RemoveUser", + data: { ID: "ExternalDirectoryObjectId" }, + confirmText: "Are you sure you want to delete this equipment mailbox?", + multiPost: false, + condition: (row) => !row.isDirSynced, + }, + ]; + + return ( + } + > + Add Equipment + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/resources/management/list-rooms/add.jsx b/src/pages/email/resources/management/list-rooms/add.jsx index 21aea3ae91d6..b4ef65fd7317 100644 --- a/src/pages/email/resources/management/list-rooms/add.jsx +++ b/src/pages/email/resources/management/list-rooms/add.jsx @@ -45,7 +45,7 @@ const AddRoomMailbox = () => { > {/* Display Name */} - + { {/* Username and Domain */} - + { validators={{ required: "Username is required" }} /> - + { {/* Resource Capacity (Optional) */} - + { - const timezones = `Azores Standard Time (UTC-01:00) Azores -Cape Verde Standard Time (UTC-01:00) Cabo Verde Is. -UTC-02 (UTC-02:00) Co-ordinated Universal Time-02 -Greenland Standard Time (UTC-02:00) Greenland -Mid-Atlantic Standard Time (UTC-02:00) Mid-Atlantic - Old -Tocantins Standard Time (UTC-03:00) Araguaina -Paraguay Standard Time (UTC-03:00) Asuncion -E. South America Standard Time (UTC-03:00) Brasilia -SA Eastern Standard Time (UTC-03:00) Cayenne, Fortaleza -Argentina Standard Time (UTC-03:00) City of Buenos Aires -Montevideo Standard Time (UTC-03:00) Montevideo -Magallanes Standard Time (UTC-03:00) Punta Arenas -Saint Pierre Standard Time (UTC-03:00) Saint Pierre and Miquelon -Bahia Standard Time (UTC-03:00) Salvador -Newfoundland Standard Time (UTC-03:30) Newfoundland -Atlantic Standard Time (UTC-04:00) Atlantic Time (Canada) -Venezuela Standard Time (UTC-04:00) Caracas -Central Brazilian Standard Time (UTC-04:00) Cuiaba -SA Western Standard Time (UTC-04:00) Georgetown, La Paz, Manaus, San Juan -Pacific SA Standard Time (UTC-04:00) Santiago -SA Pacific Standard Time (UTC-05:00) Bogota, Lima, Quito, Rio Branco -Eastern Standard Time (Mexico) (UTC-05:00) Chetumal -Eastern Standard Time (UTC-05:00) Eastern Time (US & Canada) -Haiti Standard Time (UTC-05:00) Haiti -Cuba Standard Time (UTC-05:00) Havana -US Eastern Standard Time (UTC-05:00) Indiana (East) -Turks And Caicos Standard Time (UTC-05:00) Turks and Caicos -Central America Standard Time (UTC-06:00) Central America -Central Standard Time (UTC-06:00) Central Time (US & Canada) -Easter Island Standard Time (UTC-06:00) Easter Island -Central Standard Time (Mexico) (UTC-06:00) Guadalajara, Mexico City, Monterrey -Canada Central Standard Time (UTC-06:00) Saskatchewan -US Mountain Standard Time (UTC-07:00) Arizona -Mountain Standard Time (Mexico) (UTC-07:00) La Paz, Mazatlan -Mountain Standard Time (UTC-07:00) Mountain Time (US & Canada) -Yukon Standard Time (UTC-07:00) Yukon -Pacific Standard Time (Mexico) (UTC-08:00) Baja California -UTC-08 (UTC-08:00) Co-ordinated Universal Time-08 -Pacific Standard Time (UTC-08:00) Pacific Time (US & Canada) -Alaskan Standard Time (UTC-09:00) Alaska -UTC-09 (UTC-09:00) Co-ordinated Universal Time-09 -Marquesas Standard Time (UTC-09:30) Marquesas Islands -Aleutian Standard Time (UTC-10:00) Aleutian Islands -Hawaiian Standard Time (UTC-10:00) Hawaii -UTC-11 (UTC-11:00) Co-ordinated Universal Time-11 -Dateline Standard Time (UTC-12:00) International Date Line West -UTC (UTC) Co-ordinated Universal Time -GMT Standard Time (UTC+00:00) Dublin, Edinburgh, Lisbon, London -Greenwich Standard Time (UTC+00:00) Monrovia, Reykjavik -Sao Tome Standard Time (UTC+00:00) Sao Tome -W. Europe Standard Time (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna -Central Europe Standard Time (UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague -Romance Standard Time (UTC+01:00) Brussels, Copenhagen, Madrid, Paris -Morocco Standard Time (UTC+01:00) Casablanca -Central European Standard Time (UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb -W. Central Africa Standard Time (UTC+01:00) West Central Africa -GTB Standard Time (UTC+02:00) Athens, Bucharest -Middle East Standard Time (UTC+02:00) Beirut -Egypt Standard Time (UTC+02:00) Cairo -E. Europe Standard Time (UTC+02:00) Chisinau -West Bank Standard Time (UTC+02:00) Gaza, Hebron -South Africa Standard Time (UTC+02:00) Harare, Pretoria -FLE Standard Time (UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius -Israel Standard Time (UTC+02:00) Jerusalem -South Sudan Standard Time (UTC+02:00) Juba -Kaliningrad Standard Time (UTC+02:00) Kaliningrad -Sudan Standard Time (UTC+02:00) Khartoum -Libya Standard Time (UTC+02:00) Tripoli -Namibia Standard Time (UTC+02:00) Windhoek -Jordan Standard Time (UTC+03:00) Amman -Arabic Standard Time (UTC+03:00) Baghdad -Syria Standard Time (UTC+03:00) Damascus -Turkey Standard Time (UTC+03:00) Istanbul -Arab Standard Time (UTC+03:00) Kuwait, Riyadh -Belarus Standard Time (UTC+03:00) Minsk -Russian Standard Time (UTC+03:00) Moscow, St Petersburg -E. Africa Standard Time (UTC+03:00) Nairobi -Volgograd Standard Time (UTC+03:00) Volgograd -Iran Standard Time (UTC+03:30) Tehran -Arabian Standard Time (UTC+04:00) Abu Dhabi, Muscat -Astrakhan Standard Time (UTC+04:00) Astrakhan, Ulyanovsk -Azerbaijan Standard Time (UTC+04:00) Baku -Russia Time Zone 3 (UTC+04:00) Izhevsk, Samara -Mauritius Standard Time (UTC+04:00) Port Louis -Saratov Standard Time (UTC+04:00) Saratov -Georgian Standard Time (UTC+04:00) Tbilisi -Caucasus Standard Time (UTC+04:00) Yerevan -Afghanistan Standard Time (UTC+04:30) Kabul -West Asia Standard Time (UTC+05:00) Ashgabat, Tashkent -Qyzylorda Standard Time (UTC+05:00) Astana -Ekaterinburg Standard Time (UTC+05:00) Ekaterinburg -Pakistan Standard Time (UTC+05:00) Islamabad, Karachi -India Standard Time (UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi -Sri Lanka Standard Time (UTC+05:30) Sri Jayawardenepura -Nepal Standard Time (UTC+05:45) Kathmandu -Central Asia Standard Time (UTC+06:00) Bishkek -Bangladesh Standard Time (UTC+06:00) Dhaka -Omsk Standard Time (UTC+06:00) Omsk -Myanmar Standard Time (UTC+06:30) Yangon (Rangoon) -SE Asia Standard Time (UTC+07:00) Bangkok, Hanoi, Jakarta -Altai Standard Time (UTC+07:00) Barnaul, Gorno-Altaysk -W. Mongolia Standard Time (UTC+07:00) Hovd -North Asia Standard Time (UTC+07:00) Krasnoyarsk -N. Central Asia Standard Time (UTC+07:00) Novosibirsk -Tomsk Standard Time (UTC+07:00) Tomsk -China Standard Time (UTC+08:00) Beijing, Chongqing, Hong Kong SAR, Urumqi -North Asia East Standard Time (UTC+08:00) Irkutsk -Singapore Standard Time (UTC+08:00) Kuala Lumpur, Singapore -W. Australia Standard Time (UTC+08:00) Perth -Taipei Standard Time (UTC+08:00) Taipei -Ulaanbaatar Standard Time (UTC+08:00) Ulaanbaatar -Aus Central W. Standard Time (UTC+08:45) Eucla -Transbaikal Standard Time (UTC+09:00) Chita -Tokyo Standard Time (UTC+09:00) Osaka, Sapporo, Tokyo -North Korea Standard Time (UTC+09:00) Pyongyang -Korea Standard Time (UTC+09:00) Seoul -Yakutsk Standard Time (UTC+09:00) Yakutsk -Cen. Australia Standard Time (UTC+09:30) Adelaide -AUS Central Standard Time (UTC+09:30) Darwin -E. Australia Standard Time (UTC+10:00) Brisbane -AUS Eastern Standard Time (UTC+10:00) Canberra, Melbourne, Sydney -West Pacific Standard Time (UTC+10:00) Guam, Port Moresby -Tasmania Standard Time (UTC+10:00) Hobart -Vladivostok Standard Time (UTC+10:00) Vladivostok -Lord Howe Standard Time (UTC+10:30) Lord Howe Island -Bougainville Standard Time (UTC+11:00) Bougainville Island -Russia Time Zone 10 (UTC+11:00) Chokurdakh -Magadan Standard Time (UTC+11:00) Magadan -Norfolk Standard Time (UTC+11:00) Norfolk Island -Sakhalin Standard Time (UTC+11:00) Sakhalin -Central Pacific Standard Time (UTC+11:00) Solomon Is., New Caledonia -Russia Time Zone 11 (UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky -New Zealand Standard Time (UTC+12:00) Auckland, Wellington -UTC+12 (UTC+12:00) Co-ordinated Universal Time+12 -Fiji Standard Time (UTC+12:00) Fiji -Kamchatka Standard Time (UTC+12:00) Petropavlovsk-Kamchatsky - Old -Chatham Islands Standard Time (UTC+12:45) Chatham Islands -UTC+13 (UTC+13:00) Co-ordinated Universal Time+13 -Tonga Standard Time (UTC+13:00) Nuku'alofa -Samoa Standard Time (UTC+13:00) Samoa -Line Islands Standard Time (UTC+14:00) Kiritimati Island`; - - return timezones.split('\n').map(line => { - const parts = line.trim().split(/\s{2,}/); - if (parts.length >= 2) { - return { - value: parts[0].trim(), - label: parts[1].trim(), - }; - } - return null; - }).filter(Boolean); -}; +import timezoneList from "/src/data/timezoneList.json"; // Work days options const workDaysOptions = [ @@ -177,14 +22,14 @@ const workDaysOptions = [ { value: "Saturday", label: "Saturday" }, { value: "WeekDay", label: "Weekdays (Monday-Friday)" }, { value: "WeekendDay", label: "Weekend (Saturday-Sunday)" }, - { value: "AllDays", label: "All Days" } + { value: "AllDays", label: "All Days" }, ]; // Automation Processing Options const automateProcessingOptions = [ { value: "None", label: "None - No processing" }, { value: "AutoUpdate", label: "AutoUpdate - Accept/Decline but not delete" }, - { value: "AutoAccept", label: "AutoAccept - Accept and delete" } + { value: "AutoAccept", label: "AutoAccept - Accept and delete" }, ]; const EditRoomMailbox = () => { @@ -208,10 +53,10 @@ const EditRoomMailbox = () => { // Core Properties displayName: room.displayName, hiddenFromAddressListsEnabled: room.hiddenFromAddressListsEnabled, - + // Room Booking Settings capacity: room.capacity, - + // Location Information building: room.building, floor: room.floor, @@ -223,16 +68,16 @@ const EditRoomMailbox = () => { countryOrRegion: room.countryOrRegion ? countryList.find((c) => c.Name === room.countryOrRegion)?.Code || "" : "", - + // Room Equipment audioDeviceName: room.audioDeviceName, videoDeviceName: room.videoDeviceName, displayDeviceName: room.displayDeviceName, - + // Room Features isWheelChairAccessible: room.isWheelChairAccessible, phone: room.phone, - tags: room.tags?.map(tag => ({ label: tag, value: tag })) || [], + tags: room.tags?.map((tag) => ({ label: tag, value: tag })) || [], // Calendar Properties AllowConflicts: room.AllowConflicts, @@ -246,16 +91,24 @@ const EditRoomMailbox = () => { AutomateProcessing: room.AutomateProcessing, // Calendar Configuration - WorkDays: room.WorkDays?.split(',')?.map(day => ({ - label: day.trim(), - value: day.trim() - })) || [], + WorkDays: + room.WorkDays?.split(",")?.map((day) => ({ + label: day.trim(), + value: day.trim(), + })) || [], WorkHoursStartTime: room.WorkHoursStartTime, WorkHoursEndTime: room.WorkHoursEndTime, - WorkingHoursTimeZone: room.WorkingHoursTimeZone ? { - value: room.WorkingHoursTimeZone, - label: createTimezoneOptions().find(tz => tz.value === room.WorkingHoursTimeZone)?.label || room.WorkingHoursTimeZone - } : null + WorkingHoursTimeZone: room.WorkingHoursTimeZone + ? { + value: room.WorkingHoursTimeZone, + label: timezoneList.find((tz) => tz.standardTime === room.WorkingHoursTimeZone) + ? `${room.WorkingHoursTimeZone} - ${ + timezoneList.find((tz) => tz.standardTime === room.WorkingHoursTimeZone) + ?.timezone + }` + : room.WorkingHoursTimeZone, + } + : null, }); } }, [roomInfo.isSuccess, roomInfo.data]); @@ -273,16 +126,15 @@ const EditRoomMailbox = () => { title="Edit Room Mailbox" backButtonTitle="Room Mailboxes Overview" postUrl="/api/EditRoomMailbox" - customDataformatter={(values) => ({ tenantID: tenantDomain, roomId: roomId, displayName: values.displayName?.trim(), hiddenFromAddressListsEnabled: values.hiddenFromAddressListsEnabled, - + // Room Booking Settings capacity: values.capacity, - + // Location Information building: values.building?.trim(), floor: values.floor, @@ -291,17 +143,17 @@ const EditRoomMailbox = () => { city: values.city?.trim(), state: values.state?.trim(), postalCode: values.postalCode?.trim(), - countryOrRegion: values.countryOrRegion?.value || values.countryOrRegion, - + countryOrRegion: values.countryOrRegion?.value || values.countryOrRegion || null, + // Room Equipment audioDeviceName: values.audioDeviceName?.trim(), videoDeviceName: values.videoDeviceName?.trim(), displayDeviceName: values.displayDeviceName?.trim(), - + // Room Features isWheelChairAccessible: values.isWheelChairAccessible, phone: values.phone?.trim(), - tags: values.tags?.map(tag => tag.value), + tags: values.tags?.map((tag) => tag.value), // Calendar Properties AllowConflicts: values.AllowConflicts, @@ -315,7 +167,7 @@ const EditRoomMailbox = () => { AutomateProcessing: values.AutomateProcessing?.value || values.AutomateProcessing, // Calendar Configuration - WorkDays: values.WorkDays?.map(day => day.value).join(','), + WorkDays: values.WorkDays?.map((day) => day.value).join(","), WorkHoursStartTime: values.WorkHoursStartTime, WorkHoursEndTime: values.WorkHoursEndTime, WorkingHoursTimeZone: values.WorkingHoursTimeZone?.value || values.WorkingHoursTimeZone, @@ -323,11 +175,11 @@ const EditRoomMailbox = () => { > {/* Basic Information */} - + Basic Information - + { validators={{ required: "Display Name is required" }} /> - - + { formControl={formControl} /> - - {/* Booking Settings */} - + Booking Settings - + - - + - - + - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - {/* Working Hours */} - + Working Hours - + { formControl={formControl} /> - - + { /> - + ({ + value: tz.standardTime, + label: `${tz.standardTime} - ${tz.timezone}`, + }))} multiple={false} creatable={false} formControl={formControl} /> - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - {/* Room Facilities */} - + Room Facilities & Equipment - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { creatable={true} /> - - {/* Location Information */} - + Location Information - + { /> - + { /> - + { formControl={formControl} /> - - + { /> - + { /> - + { formControl={formControl} /> - - + { formControl={formControl} /> - - + { EditRoomMailbox.getLayout = (page) => {page}; -export default EditRoomMailbox; \ No newline at end of file +export default EditRoomMailbox; diff --git a/src/pages/email/spamfilter/list-connectionfilter/add.jsx b/src/pages/email/spamfilter/list-connectionfilter/add.jsx index 8937226563a9..2fad50d45b26 100644 --- a/src/pages/email/spamfilter/list-connectionfilter/add.jsx +++ b/src/pages/email/spamfilter/list-connectionfilter/add.jsx @@ -34,7 +34,7 @@ const AddPolicy = () => { postUrl="/api/AddConnectionFilter" > - + { {/* TemplateList */} - + { - + { postUrl="/api/AddQuarantinePolicy" > - + { {/* */} {/* TemplateList, can be added later. But did not seem necessary with so few settings */} - {/* + {/* { */} - + { /> - + { formControl={formControl} /> - + { postUrl="/api/AddSpamFilter" > - + { {/* TemplateList */} - + { - + { Restore Settings - + { /> {/* Target Mailbox */} - + { validators={{ validate: (value) => (value ? true : "Please select a target mailbox.") }} /> - + { Optional Settings - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { ]} /> - + { options={wellKnownFolders} /> - + { options={wellKnownFolders} /> - + { /> - + { ]} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { ]} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { accordionExpanded={true} > - + { /> {formControl.watch("dateFilter") === "relative" && ( - + { )} {formControl.watch("dateFilter") === "startEnd" && ( <> - + { disabled={isMessageIdSet} /> - + { disabled={isMessageIdSet} /> - + { formControl={formControl} /> - + { disabled={isMessageIdSet} /> - + { disabled={isMessageIdSet} /> - + { {/* Submit and Clear Buttons */} - + diff --git a/src/pages/email/transport/list-connectors/add.jsx b/src/pages/email/transport/list-connectors/add.jsx index 5bb832e17d3a..233101bf3e09 100644 --- a/src/pages/email/transport/list-connectors/add.jsx +++ b/src/pages/email/transport/list-connectors/add.jsx @@ -34,7 +34,7 @@ const AddPolicy = () => { postUrl="/api/AddExConnector" > - + { {/* TemplateList */} - + { - + { postUrl="/api/AddTransportRule" > - + { {/* TemplateList */} - + { - + { title={pageTitle} apiUrl="/api/ListDevices" actions={actions} + queryKey={`MEMDevices-${tenantFilter}`} offCanvas={offCanvas} simpleColumns={[ "deviceName", diff --git a/src/pages/endpoint/MEM/list-scripts/index.jsx b/src/pages/endpoint/MEM/list-scripts/index.jsx index ee7ed45f4f84..a5fecd62a878 100644 --- a/src/pages/endpoint/MEM/list-scripts/index.jsx +++ b/src/pages/endpoint/MEM/list-scripts/index.jsx @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage"; -import { Code, TrashIcon, PencilIcon } from "@heroicons/react/24/outline"; +import { TrashIcon, PencilIcon } from "@heroicons/react/24/outline"; import { showToast } from "/src/store/toasts"; import { Button, @@ -14,7 +14,7 @@ import { import { CippCodeBlock } from "/src/components/CippComponents/CippCodeBlock"; import { useState, useEffect } from "react"; import { useDispatch } from "react-redux"; -import { Search, Close, Save } from "@mui/icons-material"; +import { Close, Save } from "@mui/icons-material"; import { useSettings } from "../../../../hooks/use-settings"; import { Stack } from "@mui/system"; import { useQuery, useQueryClient } from "@tanstack/react-query"; diff --git a/src/pages/endpoint/applications/list/add.jsx b/src/pages/endpoint/applications/list/add.jsx index 8bad92e4b715..8a8880251b84 100644 --- a/src/pages/endpoint/applications/list/add.jsx +++ b/src/pages/endpoint/applications/list/add.jsx @@ -93,7 +93,7 @@ const ApplicationDeploymentForm = () => { }} > - + { {/* Tenant Selector */} - + { compareType="is" compareValue="mspApp" > - + { validators={{ required: "Please select an MSP Tool" }} /> - + { compareType="is" compareValue="datto" > - + { /> {selectedTenants?.map((tenant, index) => ( - + { compareValue="syncro" > {selectedTenants?.map((tenant, index) => ( - + { compareType="is" compareValue="huntress" > - + { /> {selectedTenants?.map((tenant, index) => ( - + { compareType="is" compareValue="automate" > - + { /> {selectedTenants?.map((tenant, index) => ( - + { ))} {selectedTenants?.map((tenant, index) => ( - + { compareValue="cwcommand" > {selectedTenants?.map((tenant, index) => ( - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { compareType="is" compareValue="StoreApp" > - + { formControl={formControl} /> - + - + { isFetching={winGetSearchResults.isLoading} /> - + { validators={{ required: "Package Identifier is required" }} /> - + { validators={{ required: "Application Name is required" }} /> - + { {/* Install Options */} - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { compareType="is" compareValue="chocolateyApp" > - + { formControl={formControl} /> - + - + { /> - + { validators={{ required: "Package Name is required" }} /> - + { validators={{ required: "Application Name is required" }} /> - + { formControl={formControl} /> - + { {/* Install Options */} - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { > {/* Office App Fields */} - + { formControl={formControl} /> - + { validators={{ required: "Please select an update channel" }} /> - + ({ value: tag, - label: language, + label: `${language} (${tag})`, }))} multiple={true} formControl={formControl} validators={{ required: "Please select at least one language" }} /> - + { formControl={formControl} /> - + { defaultValue={true} /> - + { defaultValue={true} /> - + { {/* Assign To Options */} - + { component: CippWizardCSVImport, componentProps: { name: "autopilotData", - manualFields: true, - fields: ["SerialNumber", "oemManufacturerName", "modelName", "productKey", "hardwareHash"], - nameToCSVMapping: { - SerialNumber: "Device serial number", - oemManufacturerName: "Manufacturer name", - modelName: "Device Model", - productKey: "Windows product ID", - hardwareHash: "Hardware hash", - }, + fields: [ + { + friendlyName: "Serialnumber", + propertyName: "SerialNumber", + alternativePropertyNames: ["Device Serial Number"] + }, + { + friendlyName: "Manufacturer", + propertyName: "oemManufacturerName", + alternativePropertyNames: ["Manufacturer name"] + }, + { + friendlyName: "Model", + propertyName: "modelName", + alternativePropertyNames: ["Device model"] + }, + { + friendlyName: "Product ID", + propertyName: "productKey", + alternativePropertyNames: ["Windows Product ID"] + }, + { + friendlyName: "Hardware hash", + propertyName: "hardwareHash", + alternativePropertyNames: ["Hardware Hash"] + } + ], + fileName: "autopilot-template" }, }, { diff --git a/src/pages/endpoint/autopilot/add-status-page/index.js b/src/pages/endpoint/autopilot/add-status-page/index.js index 460c7f68f3de..4d109115ce94 100644 --- a/src/pages/endpoint/autopilot/add-status-page/index.js +++ b/src/pages/endpoint/autopilot/add-status-page/index.js @@ -1,7 +1,6 @@ -import React from "react"; import { Divider } from "@mui/material"; import { Grid } from "@mui/system"; -import { useForm, useWatch } from "react-hook-form"; +import { useForm} from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -34,7 +33,7 @@ const Page = () => { > {/* Tenant Selector */} - + { {/* Form Fields */} - + { /> - + { {/* Switches */} - + { const pageTitle = "Autopilot Devices"; diff --git a/src/pages/endpoint/autopilot/list-profiles/add.jsx b/src/pages/endpoint/autopilot/list-profiles/add.jsx index 26fd85204b24..1d6b4cd68a38 100644 --- a/src/pages/endpoint/autopilot/list-profiles/add.jsx +++ b/src/pages/endpoint/autopilot/list-profiles/add.jsx @@ -1,7 +1,6 @@ -import React from "react"; import { Divider } from "@mui/material"; import { Grid } from "@mui/system"; -import { useForm, useWatch } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -39,7 +38,7 @@ const AutopilotProfileForm = () => { > {/* Tenant Selector */} - + { {/* Form Fields */} - + { /> - + { /> - + { /> - + { {/* Switches */} - + { - + Loading... #Todo: Make pretty, make it obey user settings for theme. diff --git a/src/pages/identity/administration/devices/index.js b/src/pages/identity/administration/devices/index.js index c547a0d800b1..9d0f56c18643 100644 --- a/src/pages/identity/administration/devices/index.js +++ b/src/pages/identity/administration/devices/index.js @@ -78,6 +78,7 @@ const Page = () => { $count: true, }} apiDataKey="Results" + queryKey={`EntraDevices-${tenantFilter}`} actions={actions} simpleColumns={[ "displayName", diff --git a/src/pages/identity/administration/group-templates/edit.jsx b/src/pages/identity/administration/group-templates/edit.jsx index 6f7b74c11cdf..96c7572ab709 100644 --- a/src/pages/identity/administration/group-templates/edit.jsx +++ b/src/pages/identity/administration/group-templates/edit.jsx @@ -1,106 +1,106 @@ -import { Box, CircularProgress } from "@mui/material"; -import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm } from "react-hook-form"; -import { useSettings } from "../../../../hooks/use-settings"; -import CippAddGroupTemplateForm from "../../../../components/CippFormPages/CippAddGroupTemplateForm"; -import { useRouter } from "next/router"; -import { ApiGetCall } from "../../../../api/ApiCall"; -import { useEffect } from "react"; - -const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { id } = router.query; - - const formControl = useForm({ - mode: "onChange", - defaultValues: { - tenantFilter: userSettingsDefaults.currentTenant, - }, - }); - - // Fetch template data - const { data: template, isFetching } = ApiGetCall({ - url: `/api/ListGroupTemplates?id=${id}`, - queryKey: `GroupTemplate-${id}`, - waiting: !!id, - }); - - // Map groupType values to valid radio options - const mapGroupType = (type) => { - // Map of group types to the corresponding option value - const groupTypeMap = { - // Standard mappings - azurerole: "azurerole", - generic: "generic", - m365: "m365", - dynamic: "dynamic", - dynamicdistribution: "dynamicdistribution", - distribution: "distribution", - security: "security", - - // Additional mappings from possible backend values - Unified: "m365", - Security: "generic", - Distribution: "distribution", - "Mail-enabled security": "security", - "Mail Enabled Security": "security", - "Azure Role Group": "azurerole", - "Azure Active Directory Role Group": "azurerole", - "Security Group": "generic", - "Microsoft 365 Group": "m365", - "Microsoft 365 (Unified)": "m365", - "Dynamic Group": "dynamic", - DynamicMembership: "dynamic", - "Dynamic Distribution Group": "dynamicdistribution", - DynamicDistribution: "dynamicdistribution", - "Distribution List": "distribution", - }; - - // Return just the value for the radio group, not the label/value pair - return groupTypeMap[type] || "generic"; // Default to generic if no mapping exists - }; - - // Set form values when template data is loaded - useEffect(() => { - if (template) { - const templateData = template[0]; - - // Make sure we have the necessary data before proceeding - if (templateData) { - formControl.reset({ - ...templateData, - groupType: mapGroupType(templateData.groupType), - tenantFilter: userSettingsDefaults.currentTenant, - }); - } - } - }, [template, formControl, userSettingsDefaults.currentTenant]); - - return ( - <> - - {/* Add debugging output to check what values are set */} -
{JSON.stringify(formControl.watch(), null, 2)}
- - - - -
- - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { Box, CircularProgress } from "@mui/material"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { useSettings } from "../../../../hooks/use-settings"; +import CippAddGroupTemplateForm from "../../../../components/CippFormPages/CippAddGroupTemplateForm"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useEffect } from "react"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { id } = router.query; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Fetch template data + const { data: template, isFetching } = ApiGetCall({ + url: `/api/ListGroupTemplates?id=${id}`, + queryKey: `GroupTemplate-${id}`, + waiting: !!id, + }); + + // Map groupType values to valid radio options + const mapGroupType = (type) => { + // Map of group types to the corresponding option value + const groupTypeMap = { + // Standard mappings + azurerole: "azurerole", + generic: "generic", + m365: "m365", + dynamic: "dynamic", + dynamicdistribution: "dynamicdistribution", + distribution: "distribution", + security: "security", + + // Additional mappings from possible backend values + Unified: "m365", + Security: "generic", + Distribution: "distribution", + "Mail-enabled security": "security", + "Mail Enabled Security": "security", + "Azure Role Group": "azurerole", + "Azure Active Directory Role Group": "azurerole", + "Security Group": "generic", + "Microsoft 365 Group": "m365", + "Microsoft 365 (Unified)": "m365", + "Dynamic Group": "dynamic", + DynamicMembership: "dynamic", + "Dynamic Distribution Group": "dynamicdistribution", + DynamicDistribution: "dynamicdistribution", + "Distribution List": "distribution", + }; + + // Return just the value for the radio group, not the label/value pair + return groupTypeMap[type] || "generic"; // Default to generic if no mapping exists + }; + + // Set form values when template data is loaded + useEffect(() => { + if (template) { + const templateData = template[0]; + + // Make sure we have the necessary data before proceeding + if (templateData) { + formControl.reset({ + ...templateData, + groupType: mapGroupType(templateData.groupType), + tenantFilter: userSettingsDefaults.currentTenant, + }); + } + } + }, [template, formControl, userSettingsDefaults.currentTenant]); + + return ( + <> + + {/* Add debugging output to check what values are set */} +
{JSON.stringify(formControl.watch(), null, 2)}
+ + + + +
+ + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/group-templates/index.js b/src/pages/identity/administration/group-templates/index.js index 6fe4acd3230a..b7dc1b5aa2a3 100644 --- a/src/pages/identity/administration/group-templates/index.js +++ b/src/pages/identity/administration/group-templates/index.js @@ -3,7 +3,6 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { AddBox, RocketLaunch, Delete, GitHub, Edit } from "@mui/icons-material"; import Link from "next/link"; -import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; import { ApiGetCall } from "/src/api/ApiCall"; import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; import { getCippTranslation } from "../../../../utils/get-cipp-translation"; @@ -110,7 +109,7 @@ const Page = () => { { title="Groups" backButtonTitle="Group Overview" postUrl="/api/AddGroup" + resetForm={true} > diff --git a/src/pages/identity/administration/groups/edit.jsx b/src/pages/identity/administration/groups/edit.jsx index 870a213c88f9..f9a6a42a9aa2 100644 --- a/src/pages/identity/administration/groups/edit.jsx +++ b/src/pages/identity/administration/groups/edit.jsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from "react"; -import { Divider, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { Box, Button, Divider, Typography } from "@mui/material"; import { Grid } from "@mui/system"; -import { useForm, useWatch } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -16,6 +16,8 @@ const EditGroup = () => { const router = useRouter(); const { groupId, groupType } = router.query; const [groupIdReady, setGroupIdReady] = useState(false); + const [showMembershipTable, setShowMembershipTable] = useState(false); + const [combinedData, setCombinedData] = useState([]); const tenantFilter = useSettings().currentTenant; const groupInfo = ApiGetCall({ @@ -23,7 +25,6 @@ const EditGroup = () => { queryKey: `ListGroups-${groupId}`, waiting: groupIdReady, }); - const [combinedData, setCombinedData] = useState([]); useEffect(() => { if (groupId) { @@ -36,6 +37,12 @@ const EditGroup = () => { mode: "onChange", defaultValues: { tenantFilter: tenantFilter, + AddMember: [], + RemoveMember: [], + AddOwner: [], + RemoveOwner: [], + AddContact: [], + RemoveContact: [], }, }); @@ -43,6 +50,7 @@ const EditGroup = () => { if (groupInfo.isSuccess) { const group = groupInfo.data?.groupInfo; if (group) { + // Create combined data for the table const combinedData = [ ...(groupInfo.data?.owners?.map((o) => ({ type: "Owner", @@ -57,12 +65,16 @@ const EditGroup = () => { ]; setCombinedData(combinedData); + // Reset the form with all values formControl.reset({ tenantFilter: tenantFilter, mail: group.mail, + mailNickname: group.mailNickname || "", allowExternal: groupInfo?.data?.allowExternal, sendCopies: groupInfo?.data?.sendCopies, - groupName: group.displayName, + displayName: group.displayName, + description: group.description || "", + membershipRules: group.membershipRule || "", groupId: group.id, groupType: (() => { if (group.groupTypes?.includes("Unified")) { @@ -84,176 +96,246 @@ const EditGroup = () => { } return null; })(), + // Initialize empty arrays for add/remove actions + AddMember: [], + RemoveMember: [], + AddOwner: [], + RemoveOwner: [], + AddContact: [], + RemoveContact: [], }); } } }, [groupInfo.isSuccess, router.query, groupInfo.isFetching]); return ( - - - - Add - - - m.userPrincipalName)} - /> - + <> + + + + } + > + {showMembershipTable ? ( + + + + ) : ( + + + + Group Properties + + + + + + + + + + - {/* AddOwners */} - - `${option.displayName} (${option.userPrincipalName})`} - valueField="userPrincipalName" - addedField={{ - id: "id", - displayName: "displayName", - }} - removeOptions={groupInfo.data?.owners?.map((o) => o.userPrincipalName)} - /> - - - m?.["@odata.type"] === "#microsoft.graph.orgContact") - .map((m) => m.mail)} - /> - - - - Remove + {groupInfo.data?.groupInfo?.groupTypes?.includes("DynamicMembership") && ( + + + + )} - m?.["@odata.type"] !== "#microsoft.graph.orgContact") - ?.map((m) => ({ - label: `${m.displayName} (${m.userPrincipalName})`, - value: m.userPrincipalName, - addedFields: { - id: m.id, - displayName: m.displayName, - }, - }))} - name="RemoveMember" - label="Remove Member" - multiple={true} - /> - + + + Add Members + - {/* RemoveOwners */} - - ({ - label: `${o.displayName} (${o.userPrincipalName})`, - value: o.userPrincipalName, - addedFields: { - id: o.id, - displayName: o.displayName, - }, - }))} - formControl={formControl} - name="RemoveOwner" - label="Remove Owner" - multiple={true} - /> - - - m?.["@odata.type"] === "#microsoft.graph.orgContact") - .map((m) => ({ - label: `${m.displayName} (${m.mail})`, - value: m.mail, - addedFields: { - id: m.id, - displayName: m.displayName, - mail: m.mail, - }, - }))} - formControl={formControl} - name="RemoveContact" - label="Remove Contact" - multiple={true} - /> - + + + + + + + + + + + - - {(groupType === "Microsoft 365" || groupType === "Distribution List") && ( - + + + Remove Members + + + m?.["@odata.type"] !== "#microsoft.graph.orgContact") + ?.map((m) => ({ + label: `${m.displayName} (${m.userPrincipalName})`, + value: m.userPrincipalName, + addedFields: { id: m.id }, + })) || [] + } /> - )} - {groupType === "Microsoft 365" && ( - + ({ + label: `${o.displayName} (${o.userPrincipalName})`, + value: o.userPrincipalName, + addedFields: { id: o.id }, + })) || [] + } /> - )} - - - - - - - + + + m?.["@odata.type"] === "#microsoft.graph.orgContact") + ?.map((m) => ({ + label: `${m.displayName} (${m.mail})`, + value: m.mail, + addedFields: { id: m.id }, + })) || [] + } + /> + + + + + Group Settings + + {(groupType === "Microsoft 365" || groupType === "Distribution List") && ( + + + + )} + + {groupType === "Microsoft 365" && ( + + + + )} + + + )} +
+ ); }; diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index 427a2ae002a5..1851119434a5 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -86,10 +86,10 @@ const Page = () => { url: "/api/AddGroupTemplate", icon: , data: { - Displayname: "displayName", - Description: "description", - GroupType: "calculatedGroupType", - MembershipRules: "membershipRule", + displayName: "displayName", + description: "description", + groupType: "calculatedGroupType", + membershipRules: "membershipRule", allowExternal: "allowExternal", username: "mailNickname", }, diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index 3de0924ffc61..7bb042fb9dcf 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -22,7 +22,7 @@ const Page = () => { > - + { allTenants={false} /> - + { compareType="is" compareValue="create" > - + { required={true} /> - + { required={true} /> - + { required={true} /> - + { }} /> - + @@ -108,8 +108,8 @@ const Page = () => { compareType="is" compareValue="select" > - - + + { - + { }} /> - + { }} /> - + { }} /> - + { formControl={formControl} /> - + { }} /> - + { api: { url: "/api/ListGraphRequest", dataKey: "Results", + queryKey: "Users - {tenant}", data: { Endpoint: "users", manualPagination: true, diff --git a/src/pages/identity/administration/users/invite.jsx b/src/pages/identity/administration/users/invite.jsx index b5aa6014181c..cf6cabf8d965 100644 --- a/src/pages/identity/administration/users/invite.jsx +++ b/src/pages/identity/administration/users/invite.jsx @@ -25,7 +25,7 @@ const Page = () => { postUrl="/api/AddGuest" > - + diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx index 632638a7987e..bc2059b55f80 100644 --- a/src/pages/identity/administration/users/user/bec.jsx +++ b/src/pages/identity/administration/users/user/bec.jsx @@ -1,9 +1,8 @@ -import React, { use, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; -import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; import { CheckCircle, Download, Mail, Fingerprint, Launch } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; @@ -199,7 +198,7 @@ const Page = () => { > {/* Remediation Card */} - + { /> {/* Check 1 Card with Loading */} - + { > {/* Remediation Card */} - + { /> {/* All Steps */} - + { {becPollingCall.data.NewRules.map((rule) => ( - + ))} @@ -355,6 +354,7 @@ const Page = () => { {becPollingCall.data.NewUsers.map((user) => ( @@ -396,8 +396,9 @@ const Page = () => { becPollingCall.data.AddedApps.length > 0 && ( - {becPollingCall.data.AddedApps.map((app) => ( + {becPollingCall.data.AddedApps.map((app, index) => ( @@ -439,8 +440,9 @@ const Page = () => { becPollingCall.data.MailboxPermissionChanges.length > 0 && ( - {becPollingCall.data.MailboxPermissionChanges.map((permission) => ( + {becPollingCall.data.MailboxPermissionChanges.map((permission, index) => ( @@ -482,8 +484,9 @@ const Page = () => { becPollingCall.data.MFADevices.length > 0 && ( - {becPollingCall.data.MFADevices.map((permission) => ( + {becPollingCall.data.MFADevices.map((permission, index) => ( @@ -524,8 +527,9 @@ const Page = () => { becPollingCall.data.ChangedPasswords.length > 0 && ( - {becPollingCall.data.ChangedPasswords.map((permission) => ( + {becPollingCall.data.ChangedPasswords.map((permission, index) => ( diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx index 64d2f3f82b0e..a879193b52e1 100644 --- a/src/pages/identity/administration/users/user/conditional-access.jsx +++ b/src/pages/identity/administration/users/user/conditional-access.jsx @@ -1,263 +1,263 @@ -import { useState } from "react"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useSettings } from "/src/hooks/use-settings"; -import { useRouter } from "next/router"; -import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; -import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Mail, Forward, Fingerprint, Launch } from "@mui/icons-material"; -import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; -import tabOptions from "./tabOptions"; -import ReactTimeAgo from "react-time-ago"; -import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; -import { Box, Stack, Typography, Button, CircularProgress } from "@mui/material"; -import { Grid } from "@mui/system"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import countryList from "/src/data/countryList"; -import { CippDataTable } from "/src/components/CippTable/CippDataTable"; -import { useForm } from "react-hook-form"; -import CippButtonCard from "../../../../../components/CippCards/CippButtonCard"; -import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; -import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; - -const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { userId } = router.query; - - const tenant = userSettingsDefaults.currentTenant; - const [formParams, setFormParams] = useState(false); - - const userRequest = ApiGetCall({ - url: `/api/ListUsers?UserId=${userId}&tenantFilter=${tenant}`, - queryKey: `ListUsers-${userId}`, - }); - - // Set the title and subtitle for the layout - const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading..."; - - const subtitle = userRequest.isSuccess - ? [ - { - icon: , - text: , - }, - { - icon: , - text: , - }, - { - icon: , - text: ( - <> - Created: - - ), - }, - { - icon: , - text: ( - - ), - }, - ] - : []; - - // Initialize React Hook Form - const formControl = useForm(); - - const postRequest = ApiPostCall({ - url: "/api/ExecCACheck", - relatedQueryKeys: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`, - }); - const onSubmit = (data) => { - //add userId and tenantFilter to the object - data.userId = {}; - data.userId["value"] = userId; - data.tenantFilter = tenant; - setFormParams(data); - postRequest.mutate({ - url: "/api/ExecCACheck", - data: data, - queryKey: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`, - }); - }; - - return ( - - {userRequest.isLoading && } - {userRequest.isSuccess && ( - - - {/* Form Section */} - - - Test policies - - } - > - {/* Form Starts Here */} -
- - Test your conditional access policies before putting them in production. The - returned results will show you if the user is allowed or denied access based on - the policy. - - - - {/* Mandatory Parameters */} - Mandatory Parameters: - `${option.displayName}`, - valueField: "id", - queryKey: `ServicePrincipals-${tenant}`, - data: { - Endpoint: "ServicePrincipals", - manualPagination: true, - $select: "id,displayName", - $count: true, - $orderby: "displayName", - $top: 999, - }, - }} - formControl={formControl} - /> - - {/* Optional Parameters */} - Optional Parameters: - - {/* Test from this country */} - ({ - value: Code, - label: Name, - }))} - formControl={formControl} - /> - - {/* Test from this IP */} - - - {/* Device Platform */} - - - {/* Client Application Type */} - - - {/* Sign-in risk level */} - - - {/* User risk level */} - - - -
-
-
- - - -
-
- )} -
- ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { useState } from "react"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; +import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; +import { Mail, Fingerprint, Launch } from "@mui/icons-material"; +import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; +import tabOptions from "./tabOptions"; +import ReactTimeAgo from "react-time-ago"; +import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; +import { Box, Stack, Typography, Button } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import countryList from "/src/data/countryList"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { useForm } from "react-hook-form"; +import CippButtonCard from "../../../../../components/CippCards/CippButtonCard"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { userId } = router.query; + + const tenant = userSettingsDefaults.currentTenant; + const [formParams, setFormParams] = useState(false); + + const userRequest = ApiGetCall({ + url: `/api/ListUsers?UserId=${userId}&tenantFilter=${tenant}`, + queryKey: `ListUsers-${userId}`, + }); + + // Set the title and subtitle for the layout + const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading..."; + + const subtitle = userRequest.isSuccess + ? [ + { + icon: , + text: , + }, + { + icon: , + text: , + }, + { + icon: , + text: ( + <> + Created: + + ), + }, + { + icon: , + text: ( + + ), + }, + ] + : []; + + // Initialize React Hook Form + const formControl = useForm(); + + const postRequest = ApiPostCall({ + url: "/api/ExecCACheck", + relatedQueryKeys: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`, + }); + const onSubmit = (data) => { + //add userId and tenantFilter to the object + data.userId = {}; + data.userId["value"] = userId; + data.tenantFilter = tenant; + setFormParams(data); + postRequest.mutate({ + url: "/api/ExecCACheck", + data: data, + queryKey: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`, + }); + }; + + return ( + + {userRequest.isLoading && } + {userRequest.isSuccess && ( + + + {/* Form Section */} + + + Test policies + + } + > + {/* Form Starts Here */} +
+ + Test your conditional access policies before putting them in production. The + returned results will show you if the user is allowed or denied access based on + the policy. + + + + {/* Mandatory Parameters */} + Mandatory Parameters: + `${option.displayName}`, + valueField: "id", + queryKey: `ServicePrincipals-${tenant}`, + data: { + Endpoint: "ServicePrincipals", + manualPagination: true, + $select: "id,displayName", + $count: true, + $orderby: "displayName", + $top: 999, + }, + }} + formControl={formControl} + /> + + {/* Optional Parameters */} + Optional Parameters: + + {/* Test from this country */} + ({ + value: Code, + label: Name, + }))} + formControl={formControl} + /> + + {/* Test from this IP */} + + + {/* Device Platform */} + + + {/* Client Application Type */} + + + {/* Sign-in risk level */} + + + {/* User risk level */} + + + +
+
+
+ + + +
+
+ )} +
+ ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/users/user/devices.jsx b/src/pages/identity/administration/users/user/devices.jsx index 41a1e73edd2d..2b0f4823415c 100644 --- a/src/pages/identity/administration/users/user/devices.jsx +++ b/src/pages/identity/administration/users/user/devices.jsx @@ -343,10 +343,10 @@ const Page = () => { }} > - + - + Latest Logon { const userSettingsDefaults = useSettings(); @@ -102,6 +102,11 @@ const Page = () => { subtitle={subtitle} isFetching={userRequest.isLoading} > + {userRequest.isSuccess && userRequest.data?.[0]?.onPremisesSyncEnabled && ( + + This user is synced from on-premises Active Directory. Changes should be made in the on-premises environment instead. + + )} { Endpoint: `users`, tenantFilter: userSettingsDefaults.currentTenant, $select: "id,displayName,userPrincipalName,mail", - noPagination: true, $top: 999, }, queryKey: `UserNames-${userSettingsDefaults.currentTenant}`, @@ -99,10 +99,68 @@ const Page = () => { waiting: waiting, }); + const groupsList = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `groups`, + tenantFilter: userSettingsDefaults.currentTenant, + $filter: "securityEnabled eq true and mailEnabled eq true", + $select: "id,displayName,mail,description", + $top: 999, + }, + queryKey: `MailEnabledSecurityGroups-${userSettingsDefaults.currentTenant}`, + }); + + const getPermissionInfo = (userIdentifier, groupsList) => { + // Handle undefined/null cases first + if (!userIdentifier) { + return { + type: 'Unknown', + displayName: 'Unknown User' + }; + } + + // Handle special built-in cases + if (userIdentifier === 'Default' || userIdentifier === 'Anonymous') { + return { + type: 'System', + displayName: userIdentifier + }; + } + + // Check if it's a group - handle Exchange's different naming patterns + const matchingGroup = groupsList?.data?.Results?.find(group => { + // Ensure group properties exist before comparison + if (!group) return false; + + return ( + // Exact match on mail address + (group.mail && group.mail === userIdentifier) || + // Exact match on display name + (group.displayName && group.displayName === userIdentifier) || + // Partial match - permission identifier starts with group display name (handles timestamps) + (group.displayName && userIdentifier.startsWith(group.displayName)) + ); + }); + + if (matchingGroup) { + return { + type: 'Group', + displayName: matchingGroup.displayName // Use clean name from Graph API + }; + } + + // If not a system entity or group, assume it's a user + return { + type: 'User', + displayName: userIdentifier // Keep original for users + }; + }; + // Define API configurations for the dialogs const aliasApiConfig = { type: "POST", - url: "/api/SetUserAliases", + url: "/api/EditUserAliases", relatedQueryKeys: `ListUsers-${userId}`, confirmText: "Add the specified proxy addresses to this user?", customDataformatter: (row, action, formData) => { @@ -169,6 +227,7 @@ const Page = () => { const permission = { UserID: data.UserToGetPermissions, PermissionLevel: data.Permissions, + FolderName: calPermissions.data?.[0]?.FolderName ?? "Calendar", Modification: "Add", }; @@ -184,8 +243,6 @@ const Page = () => { }, }; - // This effect is no longer needed since we use CippApiDialog for form handling - useEffect(() => { if (permissionsDialog.open) { usersList.refetch(); @@ -218,8 +275,88 @@ const Page = () => { } }, [userId]); + useEffect(() => { + if (userRequest.isSuccess && userRequest.data?.[0]) { + const currentSettings = userRequest.data[0]; + const forwardingAddress = currentSettings.ForwardingAddress; + const forwardingSmtpAddress = currentSettings.MailboxActionsData?.ForwardingSmtpAddress; + const forwardAndDeliver = currentSettings.ForwardAndDeliver; + + let forwardingType = "disabled"; + let cleanAddress = ""; + + if (forwardingSmtpAddress) { + // External forwarding + forwardingType = "ExternalAddress"; + cleanAddress = forwardingSmtpAddress; + } else if (forwardingAddress) { + // Internal forwarding + forwardingType = "internalAddress"; + cleanAddress = forwardingAddress; + } + + // Set form values + formControl.setValue("forwarding.forwardOption", forwardingType); + formControl.setValue("forwarding.KeepCopy", forwardAndDeliver === true); + + if (forwardingType === "internalAddress") { + formControl.setValue("forwarding.ForwardInternal", cleanAddress); + formControl.setValue("forwarding.ForwardExternal", ""); + } else if (forwardingType === "ExternalAddress") { + formControl.setValue("forwarding.ForwardExternal", cleanAddress); + formControl.setValue("forwarding.ForwardInternal", ""); + } else { + formControl.setValue("forwarding.ForwardInternal", ""); + formControl.setValue("forwarding.ForwardExternal", ""); + } + } + }, [userRequest.isSuccess, userRequest.dataUpdatedAt, formControl]); + const title = graphUserRequest.isSuccess ? graphUserRequest.data?.[0]?.displayName : "Loading..."; + // Combine users and groups into a single options array + const combinedOptions = useMemo(() => { + const options = []; + + // Add special system users for calendar permissions + options.push({ + value: 'Default', + label: 'Default', + type: 'system' + }); + + // Add users + if (usersList?.data?.Results) { + usersList.data.Results.forEach((user) => { + options.push({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + type: 'user' + }); + }); + } + + // Add mail-enabled security groups + if (groupsList?.data?.Results) { + groupsList.data.Results.forEach((group) => { + options.push({ + value: group.mail, + label: `${group.displayName} (${group.mail})`, + type: 'group' + }); + }); + } + + // Sort alphabetically by label, but keep system users at the top + return options.sort((a, b) => { + if (a.type === 'system' && b.type !== 'system') return -1; + if (b.type === 'system' && a.type !== 'system') return 1; + return a.label.localeCompare(b.label); + }); + }, [usersList?.data?.Results, groupsList?.data?.Results]); + + const isUserGroupLoading = usersList.isFetching || groupsList.isFetching; + const subtitle = graphUserRequest.isSuccess ? [ { @@ -267,24 +404,29 @@ const Page = () => { icon: , url: "/api/ExecModifyMBPerms", customDataformatter: (row, action, formData) => { - // build permissions var permissions = []; - // if the row is an array, iterate through it if (Array.isArray(row)) { row.forEach((item) => { + // Safely extract original user identifier + const originalUser = item?._raw?.User || item?.User; + if (originalUser) { // Only add if we have a valid user + permissions.push({ + UserID: originalUser, // Use original identifier for API calls + PermissionLevel: item?.AccessRights || 'Unknown', + Modification: "Remove", + }); + } + }); + } else { + // Safely extract original user identifier + const originalUser = row?._raw?.User || row?.User; + if (originalUser) { // Only add if we have a valid user permissions.push({ - UserID: item.User, - PermissionLevel: item.AccessRights, + UserID: originalUser, // Use original identifier for API calls + PermissionLevel: row?.AccessRights || 'Unknown', Modification: "Remove", }); - }); - } else { - // if it's a single object, just push it - permissions.push({ - UserID: row.User, - PermissionLevel: row.AccessRights, - Modification: "Remove", - }); + } } return { @@ -314,8 +456,8 @@ const Page = () => { text: "Mailbox Permissions", subtext: userRequest.data?.[0]?.Permissions?.length !== 0 - ? "Other users have access to this mailbox" - : "No other users have access to this mailbox", + ? "Other users or groups have access to this mailbox" + : "No other users or groups have access to this mailbox", statusColor: "green.main", cardLabelBoxActions: ( diff --git a/src/pages/index.js b/src/pages/index.js index d12ed2729f1b..4c3b21ba7ce6 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,6 +1,6 @@ import Head from "next/head"; import { useEffect, useState } from "react"; -import { Box, Container, Button } from "@mui/material"; +import { Box, Container, Button, Card, CardContent } from "@mui/material"; import { Grid } from "@mui/system"; import { CippInfoBar } from "../components/CippCards/CippInfoBar"; import { CippChartCard } from "../components/CippCards/CippChartCard"; @@ -13,6 +13,7 @@ import { BulkActionsMenu } from "../components/bulk-actions-menu.js"; import { CippUniversalSearch } from "../components/CippCards/CippUniversalSearch.jsx"; import { ApiGetCall } from "../api/ApiCall.jsx"; import { CippCopyToClipBoard } from "../components/CippComponents/CippCopyToClipboard.jsx"; +import { ExecutiveReportButton } from "../components/ExecutiveReportButton.js"; const Page = () => { const { currentTenant } = useSettings(); @@ -164,6 +165,13 @@ const Page = () => { const [PortalMenuItems, setPortalMenuItems] = useState([]); + const formatStorageSize = (sizeInMB) => { + if (sizeInMB >= 1024) { + return `${(sizeInMB / 1024).toFixed(2)}GB`; + } + return `${sizeInMB}MB`; + }; + useEffect(() => { if (currentTenantInfo.isSuccess) { const tenantLookup = currentTenantInfo.data?.find( @@ -187,20 +195,40 @@ const Page = () => { - - - - - + + + + + + + {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} + + + + - + - + { /> - + { /> - + { Number(sharepoint.data?.GeoUsedStorageMB) || 0, ]} labels={[ - `Free (${ + `Free (${formatStorageSize( sharepoint.data?.TenantStorageMB - sharepoint.data?.GeoUsedStorageMB - }MB)`, - `Used (${Number(sharepoint.data?.GeoUsedStorageMB)}MB)`, + )})`, + `Used (${formatStorageSize(sharepoint.data?.GeoUsedStorageMB)})`, ]} /> {/* Converted Domain Names to Property List */} - + { /> - + { /> - + { alignItems="center" // Center vertically sx={{ height: "100%" }} // Ensure the container takes full height > - + { - + { {/* Defender Setup Section */} - + { compareType="is" compareValue={true} > - + Defender Setup Defender and MEM Reporting - + - + { formControl={formControl} /> - + { {/* Defender Defaults Policy Section */} - + { compareType="is" compareValue={true} > - + Defender Defaults Policy Select Defender policies to deploy - + - + { formControl={formControl} /> - + { {/* Assign to Group */} - + Assign to Group { {/* ASR Section */} - + { compareType="is" compareValue={true} > - + ASR Rules Set Attack Surface Reduction Rules { /> - + - + { formControl={formControl} /> - + { {/* Assign to Group */} - + Assign to Group { + const formControl = useForm({ + mode: "onChange", + defaultValues: { + selectedTenants: [], + TemplateList: [], + }, + }); + + return ( + + + + + + + + option, + url: "/api/ListSafeLinksPolicyTemplates", + }} + placeholder="Select a template" + validators={{ required: "A template must be selected" }} + /> + + + + ); +}; + +DeploySafeLinksPolicyTemplate.getLayout = (page) => {page}; +export default DeploySafeLinksPolicyTemplate; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks-template/create.jsx b/src/pages/security/safelinks/safelinks-template/create.jsx new file mode 100644 index 000000000000..e87962890042 --- /dev/null +++ b/src/pages/security/safelinks/safelinks-template/create.jsx @@ -0,0 +1,52 @@ +import { Box } from "@mui/material"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import { useSettings } from "/src/hooks/use-settings"; +import { SafeLinksForm, safeLinksDataUtils } from "/src/components/CippFormPages/CippSafeLinksPolicyRuleForm"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + + // Main form for policy configuration + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Watch policy name to pass to rule form + const watchPolicyName = useWatch({ control: formControl.control, name: "PolicyName" }); + + // Use the utility to create the data formatter + const customDataFormatter = safeLinksDataUtils.createDataFormatter(formControl, 'createTemplate'); + + return ( + <> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks-template/edit.jsx b/src/pages/security/safelinks/safelinks-template/edit.jsx new file mode 100644 index 000000000000..4945a158aa58 --- /dev/null +++ b/src/pages/security/safelinks/safelinks-template/edit.jsx @@ -0,0 +1,76 @@ +import { Box } from "@mui/material"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import { useSettings } from "/src/hooks/use-settings"; +import { useEffect } from "react"; +import { SafeLinksForm, safeLinksDataUtils } from "/src/components/CippFormPages/CippSafeLinksPolicyRuleForm"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const router = useRouter(); + const { ID } = router.query; + const userSettingsDefaults = useSettings(); + + // Main form for policy configuration + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Watch policy name to pass to rule form + const watchPolicyName = useWatch({ control: formControl.control, name: "PolicyName" }); + + // Get existing template data + const templateData = ApiGetCall({ + url: `/api/ListSafeLinksPolicyTemplateDetails?ID=${ID}`, + queryKey: `SafeLinksTemplate-${ID}`, + enabled: !!ID, + }); + + // Populate forms with existing data when available + useEffect(() => { + if (templateData.isSuccess && templateData.data?.Results) { + const template = templateData.data.Results; + + // Use utility to populate form + safeLinksDataUtils.populateFormData(formControl, template, userSettingsDefaults, 'template'); + } + }, [templateData.isSuccess, templateData.data, ID, formControl, userSettingsDefaults]); + + // Use the utility to create the data formatter + const customDataFormatter = safeLinksDataUtils.createDataFormatter(formControl, 'template', { ID }); + + return ( + <> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks-template/index.jsx b/src/pages/security/safelinks/safelinks-template/index.jsx new file mode 100644 index 000000000000..87388958ff66 --- /dev/null +++ b/src/pages/security/safelinks/safelinks-template/index.jsx @@ -0,0 +1,121 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { Button } from "@mui/material"; +import { RocketLaunch, GitHub, Edit, Add } from "@mui/icons-material"; +import Link from "next/link"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Safe Links Policy Templates"; + + // Check if GitHub integration is enabled + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const actions = [ + { + label: "Edit Template", + link: "/security/safelinks/safelinks-template/edit?ID=[GUID]", + icon: , + color: "success", + target: "_self", + }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveSafeLinksPolicyTemplate", + data: { ID: "GUID" }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["TemplateName", "TemplateDescription", "GUID"], + actions: actions, + }; + + const simpleColumns = ["TemplateName", "TemplateDescription", "GUID"]; + + return ( + + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks/add.jsx b/src/pages/security/safelinks/safelinks/add.jsx new file mode 100644 index 000000000000..0153bdb1dcac --- /dev/null +++ b/src/pages/security/safelinks/safelinks/add.jsx @@ -0,0 +1,52 @@ +import { Box } from "@mui/material"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import { useSettings } from "/src/hooks/use-settings"; +import { SafeLinksForm, safeLinksDataUtils } from "/src/components/CippFormPages/CippSafeLinksPolicyRuleForm"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + + // Main form for policy configuration + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Watch policy name to pass to rule form + const watchPolicyName = useWatch({ control: formControl.control, name: "PolicyName" }); + + // Use the utility to create the data formatter + const customDataFormatter = safeLinksDataUtils.createDataFormatter(formControl, 'add'); + + return ( + <> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks/edit.jsx b/src/pages/security/safelinks/safelinks/edit.jsx new file mode 100644 index 000000000000..c0ed200b0b26 --- /dev/null +++ b/src/pages/security/safelinks/safelinks/edit.jsx @@ -0,0 +1,88 @@ +import { Box } from "@mui/material"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import { useSettings } from "/src/hooks/use-settings"; +import { useEffect } from "react"; +import { SafeLinksForm, safeLinksDataUtils } from "/src/components/CippFormPages/CippSafeLinksPolicyRuleForm"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const router = useRouter(); + const { PolicyName, RuleName } = router.query; + const userSettingsDefaults = useSettings(); + + // Main form for policy configuration + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + PolicyName: PolicyName, + }, + }); + + // Watch policy name for rule synchronization + const watchPolicyName = useWatch({ control: formControl.control, name: "PolicyName" }); + + // Get existing policy and rule data + const policyData = ApiGetCall({ + url: `/api/ListSafeLinksPolicyDetails?PolicyName=${PolicyName}&RuleName=${RuleName}&tenantFilter=${userSettingsDefaults.currentTenant}`, + queryKey: `SafeLinksPolicy-${PolicyName}`, + enabled: !!PolicyName, + }); + + // Populate forms with existing data when available + useEffect(() => { + if (policyData.isSuccess && policyData.data?.Results) { + const results = policyData.data.Results; + const policy = results.Policy || {}; + const rule = results.Rule || {}; + + // Combine policy and rule data + const combinedData = { + ...policy, + ...rule, + RuleName: rule.RuleName || RuleName, + SafeLinksPolicy: policy.PolicyName || PolicyName, + State: rule.State, + }; + + // Use utility to populate form + safeLinksDataUtils.populateFormData(formControl, combinedData, userSettingsDefaults, 'edit'); + } + }, [policyData.isSuccess, policyData.data, PolicyName, RuleName, formControl, userSettingsDefaults]); + + // Use the utility to create the data formatter + const customDataFormatter = safeLinksDataUtils.createDataFormatter(formControl, 'edit'); + + return ( + <> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks/index.jsx b/src/pages/security/safelinks/safelinks/index.jsx new file mode 100644 index 000000000000..70ac3bcf51c0 --- /dev/null +++ b/src/pages/security/safelinks/safelinks/index.jsx @@ -0,0 +1,169 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check, LowPriority, Edit, DeleteForever, Policy, Book } from "@mui/icons-material"; +import { Button } from "@mui/material"; +import Link from "next/link"; + +const Page = () => { + const pageTitle = "Safe Links Policies"; + const apiUrl = "/api/ListSafeLinksPolicy"; + + const filterList = [ + { + filterName: "Enabled Rules", + value: [{ id: "State", value: "Enabled" }], + type: "column", + }, + { + filterName: "Disabled Rules", + value: [{ id: "State", value: "Disabled" }], + type: "column", + } + ]; + + const actions = [ + { + label: "Edit Safe Links Policy", + link: "/security/safelinks/safelinks/edit?PolicyName=[PolicyName]&RuleName=[RuleName]&tenantFilter=[tenantFilter]", + icon: , + color: "success", + target: "_self", + condition: (row) => !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy") && row.PolicyName !== "Built-In Protection Policy", + }, + { + label: "Enable Rule", + type: "POST", + icon: , + url: "/api/EditSafeLinksPolicy", + data: { + PolicyName: "PolicyName", + Name: "PolicyName", + Enabled: true + }, + confirmText: "Are you sure you want to enable this rule?", + color: "info", + condition: (row) => row.State === "Disabled" && !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + }, + { + label: "Disable Rule", + type: "POST", + icon: , + url: "/api/EditSafeLinksPolicy", + data: { + PolicyName: "PolicyName", + Name: "PolicyName", + Enabled: false + }, + confirmText: "Are you sure you want to disable this rule?", + color: "info", + condition: (row) => row.State === "Enabled" && !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + }, + { + label: "Set Priority", + type: "POST", + icon: , + url: "/api/EditSafeLinksPolicy", + condition: (row) => !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + data: { + PolicyName: "PolicyName", + Name: "PolicyName" + }, + confirmText: "What would you like to set the priority to?", + color: "info", + hideBulk: true, + fields: [ + { + type: "number", + name: "Priority", + label: "Priority", + placeholder: "Enter a number", + validators: { + required: "Priority is required", + min: { + value: 0, + message: "Priority must be at least 0 and no more than -1 of the lowest priority", + }, + }, + }, + ], + }, + { + label: "Create template based on policy", + type: "POST", + url: "/api/AddSafeLinksPolicyTemplate", + postEntireRow: true, + confirmText: "Are you sure you want to create a template based on this policy?", + icon: , + hideBulk: true, + condition: (row) => !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + }, + { + label: "Delete Rule", + type: "GET", + icon: , + url: "/api/ExecDeleteSafeLinksPolicy", + data: { + RuleName: "RuleName", + PolicyName: "PolicyName", + }, + confirmText: "Are you sure you want to delete this policy and rule?", + color: "danger", + condition: (row) => !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + } + ]; + + // Define columns for the table + const simpleColumns = [ + "PolicyName", + "ConfigurationStatus", + "IsValid", + "State", + "Priority", + "Description", + "RecipientDomainIs", + "SentTo", + "SentToMemberOf", + "ExceptIfSentTo", + "ExceptIfSentToMemberOf", + "ExceptIfRecipientDomainIs", + "DoNotRewriteUrls", + "EnableSafeLinksForEmail", + "EnableSafeLinksForTeams", + "EnableSafeLinksForOffice", + "TrackClicks", + "ScanUrls", + "EnableForInternalSenders", + "DeliverMessageAfterScan", + "AllowClickThrough", + "DisableUrlRewrite", + "EnableOrganizationBranding", + "WhenCreated", + "WhenChanged", + ]; + + const offCanvas = { + extendedInfoFields: ["RuleName", "ConfigurationStatus", "IsValid", "PolicyName", "State", "WhenCreated", "WhenChanged"], + actions: actions, + }; + + return ( + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; \ No newline at end of file diff --git a/src/pages/teams-share/sharepoint/add-site.js b/src/pages/teams-share/sharepoint/add-site.js index 23b7e08c64ef..d901d6156c61 100644 --- a/src/pages/teams-share/sharepoint/add-site.js +++ b/src/pages/teams-share/sharepoint/add-site.js @@ -23,10 +23,10 @@ const AddSiteForm = () => { backButtonTitle="Back to Sites" > - + - + { required /> - + { }} /> - + { }} /> - + { > {/* Display Name */} - + { {/* Description */} - + { - + { {/* Visibility */} - + {
{/* Request Status Filter */} - + { {/* Submit Button */} - + diff --git a/src/pages/tenant/administration/applications/permission-sets/add.js b/src/pages/tenant/administration/applications/permission-sets/add.js index 112272d3a51e..6f27122c5562 100644 --- a/src/pages/tenant/administration/applications/permission-sets/add.js +++ b/src/pages/tenant/administration/applications/permission-sets/add.js @@ -4,7 +4,7 @@ import { useForm } from "react-hook-form"; import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; import CippAppPermissionBuilder from "/src/components/CippComponents/CippAppPermissionBuilder"; import CippPageCard from "/src/components/CippCards/CippPageCard"; -import { Alert, CardContent, Skeleton, Stack, Typography, Button, Box } from "@mui/material"; +import { Alert, CardContent, Stack, Typography, Button, Box } from "@mui/material"; import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; import { useEffect, useState } from "react"; import { CopyAll } from "@mui/icons-material"; diff --git a/src/pages/tenant/administration/applications/templates/add.js b/src/pages/tenant/administration/applications/templates/add.js index 1fdbda3ca8c8..e927db0acdcf 100644 --- a/src/pages/tenant/administration/applications/templates/add.js +++ b/src/pages/tenant/administration/applications/templates/add.js @@ -4,7 +4,7 @@ import { useForm } from "react-hook-form"; import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; import CippPageCard from "/src/components/CippCards/CippPageCard"; import { CardContent } from "@mui/material"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import AppApprovalTemplateForm from "/src/components/CippComponents/AppApprovalTemplateForm"; const Page = () => { diff --git a/src/pages/tenant/administration/audit-logs/index.js b/src/pages/tenant/administration/audit-logs/index.js index fab698e09464..1c0809755fda 100644 --- a/src/pages/tenant/administration/audit-logs/index.js +++ b/src/pages/tenant/administration/audit-logs/index.js @@ -61,7 +61,7 @@ const Page = () => { {/* Date Filter Type */} - + { {/* Relative Time Filter */} {formControl.watch("dateFilter") === "relative" && ( <> - + - + { formControl={formControl} /> - + { {/* Start and End Date Filters */} {formControl.watch("dateFilter") === "startEnd" && ( <> - + { formControl={formControl} /> - + { )} {/* Submit Button */} - + diff --git a/src/pages/tenant/administration/audit-logs/log.js b/src/pages/tenant/administration/audit-logs/log.js index e88d096b39a8..162fdd2f6b37 100644 --- a/src/pages/tenant/administration/audit-logs/log.js +++ b/src/pages/tenant/administration/audit-logs/log.js @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; +import { ApiGetCall } from "/src/api/ApiCall"; import { Box, Typography, @@ -152,7 +152,7 @@ const Page = () => { {logData.Title} - + { {lookupIp && ( - + @@ -186,7 +186,7 @@ const Page = () => { )} - + { > {currentTenant === "AllTenants" && ( - + { )} {currentTenant !== "AllTenants" && ( <> - + { ]} /> - + { {currentTenant !== "AllTenants" && secureScore.isSuccess && secureScore.translatedData.controlScores.map((secureScoreControl) => ( - + { > {currentTenant === "AllTenants" && ( - + { diff --git a/src/pages/tenant/administration/tenants/edit.js b/src/pages/tenant/administration/tenants/edit.js index ad43de7e7811..b764f37ff2da 100644 --- a/src/pages/tenant/administration/tenants/edit.js +++ b/src/pages/tenant/administration/tenants/edit.js @@ -72,7 +72,7 @@ const Page = () => { - + { isFetching={tenantDetails.isFetching} /> - + { - const router = useRouter(); const formControl = useForm({ mode: "onChange", }); @@ -36,7 +32,7 @@ const Page = () => { - + { > - + { const pageTitle = "Tenants"; diff --git a/src/pages/tenant/backup/backup-wizard/add.jsx b/src/pages/tenant/backup/backup-wizard/add.jsx index fa6a9dede065..cbceda81daf6 100644 --- a/src/pages/tenant/backup/backup-wizard/add.jsx +++ b/src/pages/tenant/backup/backup-wizard/add.jsx @@ -59,7 +59,7 @@ const CreateBackup = () => { Wizard. Backups run daily or on demand by clicking the backup now button. - + { /> - + Identity - + { formControl={formControl} /> - + - + Conditional Access - + { formControl={formControl} /> - {/* Optional: Add an empty Grid item to balance the layout */} - + {/* Optional: Add an empty Grid to balance the layout */} + - + Intune - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - {/* Add an empty Grid item to fill the second column */} - + {/* Add an empty Grid to fill the second column */} + - + Email Security - + { formControl={formControl} /> - + { /> - + CIPP - + { formControl={formControl} /> - + { formControl={formControl} /> - {/* Add an empty Grid item to fill the second column */} - + {/* Add an empty Grid to fill the second column */} + ); diff --git a/src/pages/tenant/backup/backup-wizard/restore.jsx b/src/pages/tenant/backup/backup-wizard/restore.jsx index 566d1d6857b7..7f4a6414346a 100644 --- a/src/pages/tenant/backup/backup-wizard/restore.jsx +++ b/src/pages/tenant/backup/backup-wizard/restore.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Alert, Divider, Typography } from "@mui/material"; import { Grid } from "@mui/system"; -import { useForm, useWatch } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -108,7 +108,7 @@ const RestoreBackupForm = () => { {/* Backup Selector */} - + { {/* Restore Settings */} - + Restore Settings {/* Identity */} - + Identity { {/* Conditional Access */} - + Conditional Access { {/* Intune */} - + Intune { {/* Email Security */} - + Email Security { {/* CIPP */} - + CIPP { {/* Overwrite Existing Entries */} - + { compareType="is" compareValue={true} > - + Warning: Overwriting existing entries will remove the current settings and replace them with the backup settings. If you have selected to restore @@ -247,10 +247,10 @@ const RestoreBackupForm = () => { {/* Send Results To */} - + Send Restore results to: - + { formControl={formControl} /> - + - + - + {/* Review and Confirm */} - + Review and Confirm Please review the selected options before submitting. - + Selected Tenant: {tenantFilter} - + Selected Backup: {formControl.watch("backup")?.label || "None selected"} - + Overwrite Existing Configuration: {formControl.watch("overwrite") ? "Yes" : "No"} - + Send Results To: diff --git a/src/pages/tenant/conditional/deploy-vacation/add.jsx b/src/pages/tenant/conditional/deploy-vacation/add.jsx index 2e3a1bad9665..5e1c23e9c808 100644 --- a/src/pages/tenant/conditional/deploy-vacation/add.jsx +++ b/src/pages/tenant/conditional/deploy-vacation/add.jsx @@ -46,12 +46,12 @@ const Page = () => { exclusions for a specific period of time. Select the CA policy and the date range. - + {/* User Selector */} - + { {/* Conditional Access Policy Selector */} - + { {/* Start Date Picker */} - + { {/* End Date Picker */} - + { - + { /> - + { /> - + { compareType="is" compareValue="IPLocation" > - + { validators={{ required: "IPs are required" }} /> - + { compareType="is" compareValue="Countries" > - + { validators={{ required: "At least one country must be selected" }} /> - + { const pageTitle = "Named Locations"; const actions = [ + { + label: "Rename named location", + type: "POST", + url: "/api/ExecNamedLocation", + icon: , + data: { + namedLocationId: "id", + change: "!rename", + }, + fields: [{ type: "textField", name: "input", label: "New Name" }], + confirmText: "Enter the new name for this named location.", + }, + { + label: "Mark as Trusted", + type: "POST", + url: "/api/ExecNamedLocation", + icon: , + data: { + namedLocationId: "id", + change: "!setTrusted", + }, + confirmText: "Are you sure you want to mark this IP location as trusted?", + condition: (row) => + row["@odata.type"] == "#microsoft.graph.ipNamedLocation" && !row.isTrusted, + }, + { + label: "Mark as Untrusted", + type: "POST", + url: "/api/ExecNamedLocation", + icon: , + data: { + namedLocationId: "id", + change: "!setUntrusted", + }, + confirmText: "Are you sure you want to mark this IP location as untrusted?", + condition: (row) => row["@odata.type"] == "#microsoft.graph.ipNamedLocation" && row.isTrusted, + }, { label: "Add location to named location", type: "POST", @@ -16,7 +60,7 @@ const Page = () => { icon: , data: { namedLocationId: "id", - change: "addLocation", + change: "!addLocation", }, fields: [{ type: "textField", name: "input", label: "Country Code" }], confirmText: "Enter a two-letter country code, e.g., US.", @@ -29,7 +73,7 @@ const Page = () => { icon: , data: { namedLocationId: "id", - change: "removeLocation", + change: "!removeLocation", }, fields: [{ type: "textField", name: "input", label: "Country Code" }], confirmText: "Enter a two-letter country code, e.g., US.", @@ -42,7 +86,7 @@ const Page = () => { icon: , data: { namedLocationId: "id", - change: "addIp", + change: "!addIp", }, fields: [{ type: "textField", name: "input", label: "IP" }], confirmText: "Enter an IP in CIDR format, e.g., 1.1.1.1/32.", @@ -55,25 +99,32 @@ const Page = () => { icon: , data: { namedLocationId: "id", - change: "removeIp", + change: "!removeIp", }, fields: [{ type: "textField", name: "input", label: "IP" }], confirmText: "Enter an IP in CIDR format, e.g., 1.1.1.1/32.", condition: (row) => row["@odata.type"] == "#microsoft.graph.ipNamedLocation", }, + { + label: "Delete named location", + type: "POST", + url: "/api/ExecNamedLocation", + icon: , + data: { + namedLocationId: "id", + change: "!delete", + }, + confirmText: + "Are you sure you want to delete this named location? This action cannot be undone.", + color: "error", + }, ]; - const offCanvas = { - extendedInfoFields: ["displayName", "rangeOrLocation"], - actions: actions, - }; - return ( - + {pageTitle} @@ -144,7 +144,7 @@ const Page = () => { {currentTenant === "AllTenants" && layoutMode !== "Table" ? ( - + { // The standard should be reportable if there's an action with value === 'Report' const actions = standardConfig?.action ?? []; const reportingEnabled = - actions.filter((action) => action?.value === "Report").length > 0; + //if actions contains Report or Remediate, case insensitive, then we good. + actions.filter( + (action) => + action?.value.toLowerCase() === "report" || + action?.value.toLowerCase() === "remediate" + ).length > 0; // Find the tenant's value for this standard const currentTenantStandard = currentTenantData.find( @@ -428,7 +433,7 @@ const Page = () => { <> {[1, 2, 3].map((item) => ( - + { - + { - + { {filteredGroupedStandards[category].map((standard, index) => ( - + { - + { {standard.complianceDetails && ( - + { const currentTenant = useSettings().currentTenant; const pageTitle = "Domains Analyser"; + const analyserDialog = useDialog(); const apiGetCall = ApiGetCall({ url: "/api/ExecDomainAnalyser", waiting: false, @@ -31,41 +34,43 @@ const Page = () => { children: (extendedData) => , }; return ( - - - {/* This needs to be replaced with a CippApiDialog. */} - - - } - prependComponents={} - queryKey={`ListDomains-${currentTenant}`} - simpleColumns={[ - "Domain", - "ScorePercentage", - "MailProvider", - "SPFPassAll", - "MXPassTest", - "DMARCPresent", - "DMARCActionPolicy", - "DMARCPercentagePass", - "DNSSECPresent", - "DKIMEnabled", - ]} - offCanvas={offCanvas} - actions={actions} - /> + <> + + + + + } + prependComponents={} + queryKey={`ListDomains-${currentTenant}`} + simpleColumns={[ + "Domain", + "ScorePercentage", + "MailProvider", + "SPFPassAll", + "MXPassTest", + "DMARCPresent", + "DMARCActionPolicy", + "DMARCPercentagePass", + "DNSSECPresent", + "DKIMEnabled", + ]} + offCanvas={offCanvas} + actions={actions} + /> + + ); }; diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index 2f0c84037842..dcaa36640959 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -135,20 +135,20 @@ const Page = () => { severity="warning" style={{ display: "flex", alignItems: "center", width: "100%" }} > - + You have legacy standards available. Press the button to convert these standards to the new format. This will create a new template for each standard you had, but will disable the schedule. After conversion, please check the new templates to ensure they are correct and re-enable the schedule. - + - + diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index b3c28ba02ffb..c5f32f98e8f0 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -332,47 +332,49 @@ const Page = () => { - - {/* Left Column for Accordions */} - - { - // Reset unsaved changes flag - setHasUnsavedChanges(false); - // Update reference for future change detection - initialStandardsRef.current = { ...selectedStandards }; - }} - /> + + + {/* Left Column for Accordions */} + + { + // Reset unsaved changes flag + setHasUnsavedChanges(false); + // Update reference for future change detection + initialStandardsRef.current = { ...selectedStandards }; + }} + /> + + + + {/* Show accordions based on selectedStandards (which is populated by API when editing) */} + {existingTemplate.isLoading ? ( + + ) : ( + + )} + + - - - {/* Show accordions based on selectedStandards (which is populated by API when editing) */} - {existingTemplate.isLoading ? ( - - ) : ( - - )} - - - + {/* Only render the dialog when it's needed */} diff --git a/src/pages/tenant/tools/appapproval/index.js b/src/pages/tenant/tools/appapproval/index.js index 52e6040e62f9..a18484cc85ce 100644 --- a/src/pages/tenant/tools/appapproval/index.js +++ b/src/pages/tenant/tools/appapproval/index.js @@ -2,7 +2,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippWizardConfirmation } from "/src/components/CippWizard/CippWizardConfirmation"; import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx"; import { CippTenantStep } from "/src/components/CippWizard/CippTenantStep.jsx"; -import { useSettings } from "../../../../hooks/use-settings"; import { CippWizardAppApproval } from "../../../../components/CippWizard/CippWizardAppApproval"; import { Alert } from "@mui/material"; diff --git a/src/pages/tenant/tools/geoiplookup/index.js b/src/pages/tenant/tools/geoiplookup/index.js index 31aa5967367d..0cd392f37006 100644 --- a/src/pages/tenant/tools/geoiplookup/index.js +++ b/src/pages/tenant/tools/geoiplookup/index.js @@ -1,4 +1,4 @@ -import { Box, Button, Container, Skeleton } from "@mui/material"; +import { Box, Button, Container } from "@mui/material"; import { Grid, Stack } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm, useWatch } from "react-hook-form"; @@ -91,13 +91,13 @@ const Page = () => { > - + - + { required /> - +