diff --git a/public/version.json b/public/version.json index 65b1ded573e3..c46c1c3a30a4 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.2.1" + "version": "8.3.0" } diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index cc7c0639aa83..7c96e641df6e 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -72,7 +72,6 @@ export const CippBannerListCard = (props) => {
  • { onClick={isCollapsible ? () => handleExpand(item.id) : undefined} > {/* Left Side: cardLabelBox */} - + {typeof item.cardLabelBox === "object" ? ( @@ -111,8 +116,16 @@ export const CippBannerListCard = (props) => { {/* Main Text and Subtext */} - - + + {item.text} @@ -122,7 +135,7 @@ export const CippBannerListCard = (props) => { {/* Right Side: Status and Expand Icon */} - + {item?.statusText && ( { {item?.propertyItems?.length > 0 && ( )} diff --git a/src/components/CippCards/CippExchangeInfoCard.jsx b/src/components/CippCards/CippExchangeInfoCard.jsx index cfb08497dcf9..5994bf7922a5 100644 --- a/src/components/CippCards/CippExchangeInfoCard.jsx +++ b/src/components/CippCards/CippExchangeInfoCard.jsx @@ -8,6 +8,7 @@ import { IconButton, Typography, CircularProgress, + Alert, } from "@mui/material"; import { PropertyList } from "/src/components/property-list"; import { PropertyListItem } from "/src/components/property-list-item"; @@ -59,6 +60,11 @@ export const CippExchangeInfoCard = (props) => { } /> + {exchangeData?.BlockedForSpam ? ( + + This mailbox is currently blocked for spam. + + ) : null} { Hidden from GAL: - {getCippFormatting(exchangeData?.HiddenFromAddressLists, "HiddenFromAddressLists")} + {getCippFormatting( + exchangeData?.HiddenFromAddressLists, + "HiddenFromAddressLists" + )} @@ -127,14 +136,15 @@ export const CippExchangeInfoCard = (props) => { value={ isLoading ? ( - ) : (() => { + ) : ( + (() => { 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"; @@ -144,7 +154,7 @@ export const CippExchangeInfoCard = (props) => { cleanAddress = forwardingAddress; } } - + return ( @@ -152,10 +162,9 @@ export const CippExchangeInfoCard = (props) => { Forwarding Status: - {forwardingType === "None" + {forwardingType === "None" ? getCippFormatting(false, "ForwardingStatus") - : `${forwardingType} Forwarding` - } + : `${forwardingType} Forwarding`} {forwardingType !== "None" && ( @@ -172,18 +181,17 @@ export const CippExchangeInfoCard = (props) => { Forwarding Address: - - {cleanAddress} - + {cleanAddress} )} ); })() + ) } /> - + {/* Archive section - always show status */} { Auto Expanding Archive: - {getCippFormatting(exchangeData?.AutoExpandingArchive, "AutoExpandingArchive")} + {getCippFormatting( + exchangeData?.AutoExpandingArchive, + "AutoExpandingArchive" + )} diff --git a/src/components/CippComponents/BreachSearchDialog.jsx b/src/components/CippComponents/BreachSearchDialog.jsx new file mode 100644 index 000000000000..e089908ffb43 --- /dev/null +++ b/src/components/CippComponents/BreachSearchDialog.jsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { Dialog, DialogContent, DialogTitle, Button, DialogActions } from "@mui/material"; +import { Search } from "@mui/icons-material"; +import { useForm, FormProvider } from "react-hook-form"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "./CippApiResults"; +import { useSettings } from "../../hooks/use-settings"; + +export const BreachSearchDialog = ({ createDialog }) => { + const tenantFilter = useSettings()?.currentTenant; + const methods = useForm({ + defaultValues: {}, + }); + + // Use methods for form handling and control + const { handleSubmit } = methods; + + const [isRunning, setIsRunning] = useState(false); + const breachSearchResults = ApiPostCall({ + urlFromData: true, + }); + + const handleForm = () => { + setIsRunning(true); + breachSearchResults.mutate({ + url: "/api/ExecBreachSearch", + queryKey: `breach-search-${tenantFilter}`, + data: { tenantFilter: tenantFilter }, + }); + }; + + // Reset running state when dialog is closed + const handleClose = () => { + setIsRunning(false); + createDialog.handleClose(); + }; + + return ( + + +
    + Run Breach Search + +
    +

    + This will run a breach search to check for potentially compromised passwords and information + for the current tenant: {tenantFilter?.displayName || tenantFilter} +

    +
    + +
    + + + + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/components/CippComponents/CippAuditLogDetails.jsx b/src/components/CippComponents/CippAuditLogDetails.jsx new file mode 100644 index 000000000000..b5a3077a0da6 --- /dev/null +++ b/src/components/CippComponents/CippAuditLogDetails.jsx @@ -0,0 +1,359 @@ +import { useEffect } from "react"; +import { getCippTranslation } from "/src/utils/get-cipp-translation"; +import { getCippFormatting } from "/src/utils/get-cipp-formatting"; +import CippGeoLocation from "/src/components/CippComponents/CippGeoLocation"; +import { Tooltip, CircularProgress, Stack } from "@mui/material"; +import { useGuidResolver } from "/src/hooks/use-guid-resolver"; +import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; + +const CippAuditLogDetails = ({ row }) => { + const { + guidMapping, + upnMapping, + isLoadingGuids, + resolveGuids, + isGuid, + replaceGuidsAndUpnsInString, + } = useGuidResolver(); + + // Use effect for initial scan to resolve GUIDs and special UPNs + useEffect(() => { + if (row) { + // Scan the main row data + resolveGuids(row); + + // Scan audit data if present + if (row.auditData) { + resolveGuids(row.auditData); + } + } + }, [row?.id, resolveGuids]); // Dependencies for when to resolve GUIDs + + // Function to replace GUIDs and special UPNs in strings with resolved names + const replaceGuidsInString = (str) => { + if (typeof str !== "string") return str; + + // Use the hook's helper function to replace both GUIDs and special UPNs + const { result, hasResolvedNames } = replaceGuidsAndUpnsInString(str); + + // If we have resolved names, return a tooltip showing original and resolved + if (hasResolvedNames) { + return ( + + {result} + + ); + } + + // Check for GUIDs and special UPNs to see if we should show loading state + const guidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi; + const partnerUpnRegex = /user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)/gi; + + let hasGuids = guidRegex.test(str); + + // Reset regex state and check for partner UPNs + partnerUpnRegex.lastIndex = 0; + let hasUpns = false; + let match; + + // Need to extract and check if the GUIDs from UPNs are in the pending state + while ((match = partnerUpnRegex.exec(str)) !== null) { + const hexId = match[1]; + if (hexId && hexId.length === 32) { + hasUpns = true; + break; // At least one UPN pattern found + } + } + + // If we have unresolved GUIDs or UPNs and are currently loading + if ((hasGuids || hasUpns) && isLoadingGuids) { + return ( +
    + + {str} +
    + ); + } + + return str; + }; + + // Convert data to property items format for CippPropertyListCard + const convertToPropertyItems = (data, excludeAuditData = false) => { + if (!data) return []; + + return Object.entries(data) + .map(([key, value]) => { + // Skip certain blacklisted fields + const blacklist = ["selectedOption", "GUID", "ID", "id", "noSubmitButton"]; + if (blacklist.includes(key)) return null; + + // Exclude auditData from main log items if specified + if (excludeAuditData && key === "auditData") return null; + + let displayValue; + // Handle different value types + if (typeof value === "string" && isGuid(value)) { + // Handle pure GUID strings + displayValue = renderGuidValue(value); + } else if ( + typeof value === "string" && + value.match(/^user_[0-9a-f]{32}@[^@]+\.onmicrosoft\.com$/i) + ) { + // Handle special partner UPN format as direct values + displayValue = renderGuidValue(value); + } else if ( + key.toLowerCase().includes("clientip") && + value && + value !== null && + isValidIpAddress(value) + ) { + // Handle IP addresses (with optional ports) using CippGeoLocation + // Check for various IP field names: clientIp, ClientIP, IP, etc. + const cleanIp = extractIpForGeolocation(value); + displayValue = ( +
    + +
    + ); + } else if (typeof value === "string") { + // Handle strings that might contain embedded GUIDs + // First apply GUID replacement to get the processed string + const guidProcessedValue = replaceGuidsInString(value); + + // If GUID replacement returned a React element (with tooltips), use it directly + if (typeof guidProcessedValue === "object" && guidProcessedValue?.type) { + displayValue = guidProcessedValue; + } else { + // Otherwise, apply getCippFormatting to the GUID-processed string + // This preserves key-based formatting while including GUID replacements + displayValue = getCippFormatting(guidProcessedValue, key); + } + } else if (typeof value === "object" && value !== null) { + // Handle nested objects and arrays - expand GUIDs within them + displayValue = renderNestedValue(value); + } else { + // Handle regular values + displayValue = getCippFormatting(value, key); + } + + return { + label: getCippTranslation(key), + value: displayValue, + }; + }) + .filter(Boolean); + }; + + // Render GUID values with proper resolution states + const renderGuidValue = (guidValue) => { + // Handle standard GUIDs directly + if (guidMapping[guidValue]) { + return ( + + {guidMapping[guidValue]} + + ); + } + + // Special handling for partner UPN format (user_@partnertenant.onmicrosoft.com) + const partnerUpnRegex = /^user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)$/i; + const upnMatch = typeof guidValue === "string" ? guidValue.match(partnerUpnRegex) : null; + + if (upnMatch) { + const hexId = upnMatch[1]; + if (hexId && hexId.length === 32) { + const guid = [ + hexId.slice(0, 8), + hexId.slice(8, 12), + hexId.slice(12, 16), + hexId.slice(16, 20), + hexId.slice(20, 32), + ].join("-"); + + // For partner UPN format, use the actual UPN if available, otherwise fall back to display name + if (upnMapping && upnMapping[guid]) { + return ( + + {upnMapping[guid]} + + ); + } else if (guidMapping[guid]) { + return ( + + {guidMapping[guid]} + + ); + } + } + } + + // Loading state + if (isLoadingGuids) { + return ( +
    + + {guidValue} +
    + ); + } + + // Fallback for unresolved values + return ( + + {guidValue} + + ); + }; + + // Recursively render nested objects and arrays with GUID expansion + const renderNestedValue = (value) => { + if (Array.isArray(value)) { + // Handle arrays + return renderArrayValue(value); + } else if (typeof value === "object" && value !== null) { + // Handle objects + return renderObjectValue(value); + } + return getCippFormatting(value, "nested"); + }; + + // Render array values with GUID expansion + const renderArrayValue = (arrayValue) => { + if (arrayValue.length === 0) return "[]"; + + // If it's a simple array, show it formatted + if (arrayValue.length <= 5 && arrayValue.every((item) => typeof item !== "object")) { + return ( +
    + {arrayValue.map((item, index) => ( +
    + {typeof item === "string" && isGuid(item) + ? renderGuidValue(item) + : typeof item === "string" + ? replaceGuidsInString(item) + : getCippFormatting(item, `item-${index}`)} +
    + ))} +
    + ); + } + + // For complex arrays, use the formatted version which might include table buttons + return getCippFormatting(arrayValue, "array"); + }; + + // Render object values with GUID expansion + const renderObjectValue = (objectValue) => { + const entries = Object.entries(objectValue); + + // If it's a simple object with few properties, show them inline + if (entries.length <= 3 && entries.every(([, val]) => typeof val !== "object")) { + return ( +
    + {entries.map(([objKey, objVal]) => ( +
    + {getCippTranslation(objKey)}:{" "} + {typeof objVal === "string" && isGuid(objVal) + ? renderGuidValue(objVal) + : typeof objVal === "string" + ? replaceGuidsInString(objVal) + : getCippFormatting(objVal, objKey)} +
    + ))} +
    + ); + } + + // For complex objects, use the formatted version which might include table buttons + return getCippFormatting(objectValue, "object"); + }; + + // Helper function to validate IP addresses (with optional ports) + const isValidIpAddress = (ip) => { + if (typeof ip !== "string") return false; + + // Extract IP part if there's a port (split by last colon for IPv6 compatibility) + let ipPart = ip; + let portPart = null; + + // Check for IPv4:port format + const ipv4PortMatch = ip.match(/^(.+):(\d+)$/); + if (ipv4PortMatch) { + ipPart = ipv4PortMatch[1]; + portPart = ipv4PortMatch[2]; + } + + // IPv4 regex + const ipv4Regex = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + + // IPv6 regex (simplified) - note: IPv6 with ports use [::]:port format, handled separately + const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/; + + // Check for IPv6 with port [::]:port format + const ipv6PortMatch = ip.match(/^\[(.+)\]:(\d+)$/); + if (ipv6PortMatch) { + ipPart = ipv6PortMatch[1]; + portPart = ipv6PortMatch[2]; + } + + // Validate port number if present + if (portPart !== null) { + const port = parseInt(portPart, 10); + if (port < 1 || port > 65535) return false; + } + + return ipv4Regex.test(ipPart) || ipv6Regex.test(ipPart); + }; + + // Extract clean IP address from IP:port combinations for geolocation + const extractIpForGeolocation = (ipWithPort) => { + if (typeof ipWithPort !== "string") return ipWithPort; + + // IPv4:port format + const ipv4PortMatch = ipWithPort.match(/^(.+):(\d+)$/); + if (ipv4PortMatch) { + return ipv4PortMatch[1]; + } + + // IPv6 with port [::]:port format + const ipv6PortMatch = ipWithPort.match(/^\[(.+)\]:(\d+)$/); + if (ipv6PortMatch) { + return ipv6PortMatch[1]; + } + + // Return as-is if no port detected + return ipWithPort; + }; + + const mainLogItems = convertToPropertyItems(row, true); // Exclude auditData from main items + const auditDataItems = row?.auditData ? convertToPropertyItems(row.auditData) : []; + + return ( + + + + {auditDataItems.length > 0 && ( + + )} + + ); +}; + +export default CippAuditLogDetails; diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 4fb18221e0b0..106b3f0f646d 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -71,6 +71,8 @@ export const CippAutoComplete = (props) => { removeOptions = [], sortOptions = false, preselectedValue, + groupBy, + renderGroup, ...other } = props; @@ -300,7 +302,7 @@ export const CippAutoComplete = (props) => { value: item?.label ? item.value : item, }; if (onCreateOption) { - onCreateOption(item, item?.addedFields); + item = onCreateOption(item, item?.addedFields); } } return item; @@ -315,7 +317,7 @@ export const CippAutoComplete = (props) => { value: newValue?.label ? newValue.value : newValue, }; if (onCreateOption) { - onCreateOption(newValue, newValue?.addedFields); + newValue = onCreateOption(newValue, newValue?.addedFields); } } if (!newValue?.value || newValue.value === "error") { @@ -336,7 +338,9 @@ export const CippAutoComplete = (props) => { } // For API options, use the existing logic if (api) { - return option.label === null ? "" : option.label || "Label not found - Are you missing a labelField?"; + return option.label === null + ? "" + : option.label || "Label not found - Are you missing a labelField?"; } // Fallback for any edge cases return option.label || option.value || ""; @@ -365,6 +369,8 @@ export const CippAutoComplete = (props) => { )}
    )} + groupBy={groupBy} + renderGroup={renderGroup} {...other} /> ); diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx index 5e01fc542588..f0015c0fd3f6 100644 --- a/src/components/CippComponents/CippExchangeActions.jsx +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -2,10 +2,7 @@ import { Archive, MailOutline, - Person, - Room, Visibility, - VisibilityOff, PhonelinkLock, Key, PostAdd, @@ -17,12 +14,134 @@ import { MailLock, SettingsEthernet, CalendarMonth, + PersonAdd, Email, } from "@mui/icons-material"; +import { useSettings } from "/src/hooks/use-settings.js"; +import { useMemo } from "react"; export const CippExchangeActions = () => { - // const tenant = useSettings().currentTenant; + const tenant = useSettings().currentTenant; + + // API configuration for all user selection fields + const userApiConfig = useMemo(() => ({ + url: "/api/ListGraphRequest", + dataKey: "Results", + labelField: (option) => `${option.displayName} (${option.userPrincipalName})`, + valueField: "userPrincipalName", + queryKey: `users-${tenant}`, + data: { + Endpoint: "users", + tenantFilter: tenant, + $select: "id,displayName,userPrincipalName,mail", + $top: 999, + }, + }), [tenant]); + return [ + { + label: "Bulk Add Mailbox Permissions", + type: "POST", + url: "/api/ExecModifyMBPerms", + icon: , + data: { + userID: "UPN", + }, + confirmText: "Add the specified permissions to selected mailboxes?", + multiPost: false, + data: { + }, + fields: [ + { + type: "autoComplete", + name: "fullAccessUser", + label: "Add Full Access User", + multiple: true, + creatable: false, + api: userApiConfig, + }, + { + type: "switch", + name: "autoMap", + label: "Enable Automapping", + defaultValue: true, + labelLocation: "behind", + }, + { + type: "autoComplete", + name: "sendAsUser", + label: "Add Send As User", + multiple: true, + creatable: false, + api: userApiConfig, + }, + { + type: "autoComplete", + name: "sendOnBehalfUser", + label: "Add Send On Behalf User", + multiple: true, + creatable: false, + api: userApiConfig, + }, + ], + customDataformatter: (rows, action, formData) => { + + const mailboxArray = Array.isArray(rows) ? rows : [rows]; + + // Create bulk request array - one object per mailbox + const bulkRequestData = mailboxArray.map(mailbox => { + const permissions = []; + const autoMap = formData.autoMap === undefined ? true : formData.autoMap; + + // Add type: "user" to match format + const addTypeToUsers = (users) => { + return users.map(user => ({ + ...user, + type: "user" + })); + }; + + // Handle FullAccess - formData.fullAccessUser is an array since multiple: true + if (formData.fullAccessUser && formData.fullAccessUser.length > 0) { + permissions.push({ + UserID: addTypeToUsers(formData.fullAccessUser), + PermissionLevel: "FullAccess", + Modification: "Add", + AutoMap: autoMap, + }); + } + + // Handle SendAs - formData.sendAsUser is an array since multiple: true + if (formData.sendAsUser && formData.sendAsUser.length > 0) { + permissions.push({ + UserID: addTypeToUsers(formData.sendAsUser), + PermissionLevel: "SendAs", + Modification: "Add", + }); + } + + // Handle SendOnBehalf - formData.sendOnBehalfUser is an array since multiple: true + if (formData.sendOnBehalfUser && formData.sendOnBehalfUser.length > 0) { + permissions.push({ + UserID: addTypeToUsers(formData.sendOnBehalfUser), + PermissionLevel: "SendOnBehalf", + Modification: "Add", + }); + } + + return { + userID: mailbox.UPN, + permissions: permissions, + }; + }); + + return { + mailboxRequests: bulkRequestData, + tenantFilter: tenant + }; + }, + color: "primary", + }, { label: "Edit permissions", link: "/identity/administration/users/user/exchange?userId=[ExternalDirectoryObjectId]", @@ -50,7 +169,7 @@ export const CippExchangeActions = () => { type: "POST", icon: , url: "/api/ExecConvertMailbox", - data: { ID: "userPrincipalName" }, + data: { ID: "UPN" }, fields: [ { type: "radio", @@ -70,7 +189,6 @@ export const CippExchangeActions = () => { multiPost: false, }, { - //tested label: "Enable Online Archive", type: "POST", icon: , diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index 7ddc2965b0b0..7c6f7ccb6050 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -292,7 +292,6 @@ export const CippFormComponent = (props) => { label={label} multiple={false} onChange={(value) => field.onChange(value?.value)} - helperText={helperText} /> )} /> @@ -319,7 +318,6 @@ export const CippFormComponent = (props) => { defaultValue={field.value} label={label} onChange={(value) => field.onChange(value)} - helperText={helperText} /> )} /> @@ -327,6 +325,11 @@ export const CippFormComponent = (props) => { {get(errors, convertedName, {}).message} + {helperText && ( + + {helperText} + + )} ); @@ -495,9 +498,10 @@ export const CippFormComponent = (props) => { disabled={other?.disabled} onClick={() => { const now = new Date(); - // Round to nearest 15-minute interval + // Always round down to the previous 15-minute mark, unless exactly on a 15-min mark const minutes = now.getMinutes(); - const roundedMinutes = Math.round(minutes / 15) * 15; + const roundedMinutes = + minutes % 15 === 0 ? minutes : Math.floor(minutes / 15) * 15; now.setMinutes(roundedMinutes, 0, 0); // Set seconds and milliseconds to 0 const unixTimestamp = Math.floor(now.getTime() / 1000); field.onChange(unixTimestamp); diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx index fc121753b9ab..b3630acd7098 100644 --- a/src/components/CippComponents/CippFormCondition.jsx +++ b/src/components/CippComponents/CippFormCondition.jsx @@ -45,7 +45,12 @@ export const CippFormCondition = (props) => { if (propertyName && propertyName !== "value") { watchedValue = get(watcher, propertyName); - compareTargetValue = get(compareValue, propertyName); + // Only extract from compareValue if it's an object, otherwise use as-is + if (typeof compareValue === "object" && compareValue !== null) { + compareTargetValue = get(compareValue, propertyName); + } else { + compareTargetValue = compareValue; + } } /*console.log("CippFormCondition: ", { @@ -156,9 +161,19 @@ export const CippFormCondition = (props) => { ) ); case "valueEq": - return Array.isArray(watcher) && watcher.some((item) => item?.value === compareValue); + if (Array.isArray(watcher)) { + return watcher.some((item) => item?.value === compareValue); + } else if (typeof watcher === "object" && watcher !== null) { + return watcher?.value === compareValue; + } + return false; case "valueNotEq": - return Array.isArray(watcher) && watcher.some((item) => item?.value !== compareValue); + if (Array.isArray(watcher)) { + return watcher.some((item) => item?.value !== compareValue); + } else if (typeof watcher === "object" && watcher !== null) { + return watcher?.value !== compareValue; + } + return false; case "valueContains": return ( Array.isArray(watcher) && diff --git a/src/components/CippComponents/CippGeoLocation.jsx b/src/components/CippComponents/CippGeoLocation.jsx index 7a1609a2bedf..e7a4be63ed66 100644 --- a/src/components/CippComponents/CippGeoLocation.jsx +++ b/src/components/CippComponents/CippGeoLocation.jsx @@ -8,14 +8,27 @@ import { getCippTranslation } from "../../utils/get-cipp-translation"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; const CippMap = dynamic(() => import("./CippMap"), { ssr: false }); -export default function CippGeoLocation({ ipAddress, cardProps }) { +export default function CippGeoLocation({ + ipAddress, + cardProps, + showIpAddress = false, + displayIpAddress = null, +}) { const [locationInfo, setLocationInfo] = useState(null); const markerProperties = ["timezone", "as", "proxy", "hosting", "mobile"]; const includeProperties = ["org", "city", "region", "country", "zip"]; - const initialPropertyList = includeProperties.map((key) => ({ - label: getCippTranslation(key), - value: "", + + // Use displayIpAddress if provided, otherwise use ipAddress + const ipToDisplay = displayIpAddress || ipAddress; + + // Add IP address to properties if showIpAddress is true + const initialIncludeProperties = showIpAddress + ? ["ipAddress", ...includeProperties] + : includeProperties; + const initialPropertyList = initialIncludeProperties.map((key) => ({ + label: getCippTranslation(key === "ipAddress" ? "IP Address" : key), + value: key === "ipAddress" ? ipToDisplay : "", })); const [properties, setProperties] = useState(initialPropertyList); @@ -28,6 +41,16 @@ export default function CippGeoLocation({ ipAddress, cardProps }) { onResult: (result) => { setLocationInfo(result); var propertyList = []; + + // Add IP address property if showIpAddress is true + if (showIpAddress) { + propertyList.push({ + label: getCippTranslation("IP Address"), + value: getCippFormatting(ipToDisplay, "ipAddress"), + }); + } + + // Add other properties includeProperties.map((key) => { propertyList.push({ label: getCippTranslation(key), diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 46c7ec1f4bb6..59312d5401f4 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -17,6 +17,7 @@ import { PhonelinkLock, PhonelinkSetup, Shortcut, + EditAttributes, } from "@mui/icons-material"; import { getCippLicenseTranslation } from "../../utils/get-cipp-license-translation"; import { useSettings } from "/src/hooks/use-settings.js"; @@ -456,6 +457,28 @@ export const CippUserActions = () => { multiPost: false, condition: () => canWriteUser, }, + { + label: "Edit Properties", + icon: , + multiPost: true, + noConfirm: true, + customFunction: (users, action, formData) => { + // Handle both single user and multiple users + const userData = Array.isArray(users) ? users : [users]; + + // Store users in session storage to avoid URL length limits + sessionStorage.setItem('patchWizardUsers', JSON.stringify(userData)); + + // Use Next.js router for internal navigation + import('next/router').then(({ default: router }) => { + router.push('/identity/administration/users/patch-wizard'); + }).catch(() => { + // Fallback to window.location if router is not available + window.location.href = '/identity/administration/users/patch-wizard'; + }); + }, + condition: () => canWriteUser, + }, ]; }; diff --git a/src/components/CippFormPages/CippCustomDataMappingForm.jsx b/src/components/CippFormPages/CippCustomDataMappingForm.jsx index 627bc289f2a1..bf595e701e97 100644 --- a/src/components/CippFormPages/CippCustomDataMappingForm.jsx +++ b/src/components/CippFormPages/CippCustomDataMappingForm.jsx @@ -47,7 +47,7 @@ const CippCustomDataMappingForm = ({ formControl }) => { creatable: false, condition: { field: "sourceType", - compareType: "is", + compareType: "valueEq", compareValue: "extensionSync", }, }, diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index 8e1532560be3..e5ad67d6f925 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -95,6 +95,7 @@ const CippStandardAccordion = ({ handleAddMultipleStandard, formControl, editMode = false, + isDriftMode = false, }) => { const [configuredState, setConfiguredState] = useState({}); const [filter, setFilter] = useState("all"); @@ -106,6 +107,43 @@ const CippStandardAccordion = ({ control: formControl.control, }); + // Watch all trackDrift values for all standards at once + const allTrackDriftValues = useWatch({ + control: formControl.control, + name: Object.keys(selectedStandards).map((standardName) => `${standardName}.trackDrift`), + }); + + // Handle drift mode automatic action setting + useEffect(() => { + if (isDriftMode && selectedStandards) { + Object.keys(selectedStandards).forEach((standardName) => { + const currentValues = formControl.getValues(standardName) || {}; + const autoRemediate = currentValues.autoRemediate; + + // Set default action based on autoRemediate setting + const defaultAction = autoRemediate + ? [ + { label: "Report", value: "Report" }, + { label: "Remediate", value: "Remediate" }, + ] + : [{ label: "Report", value: "Report" }]; + + // Only set if action is not already set + if (!currentValues.action) { + formControl.setValue(`${standardName}.action`, defaultAction); + } + + // Set default autoRemediate if not set + if (currentValues.autoRemediate === undefined) { + formControl.setValue(`${standardName}.autoRemediate`, false); + formControl.setValue(`${standardName}.action`, [ + { label: "Report", value: "Report" }, + ]); + } + }); + } + }, [isDriftMode, selectedStandards, formControl]); + // Check if a standard is configured based on its values const isStandardConfigured = (standardName, standard, values) => { if (!values) return false; @@ -202,7 +240,6 @@ const CippStandardAccordion = ({ return; } - console.log("Initializing configuration state from template values"); const initial = {}; const initialConfigured = {}; @@ -246,7 +283,6 @@ const CippStandardAccordion = ({ // Update configured state right away const isConfigured = isStandardConfigured(standardName, standard, newValues); - console.log(`Saving standard ${standardName}, configured: ${isConfigured}`); setConfiguredState((prev) => ({ ...prev, @@ -257,6 +293,19 @@ const CippStandardAccordion = ({ handleAccordionToggle(null); }; + // Handle auto-remediate toggle in drift mode + const handleAutoRemediateChange = (standardName, value) => { + const action = value + ? [ + { label: "Report", value: "Report" }, + { label: "Remediate", value: "Remediate" }, + ] + : [{ label: "Report", value: "Report" }]; + + formControl.setValue(`${standardName}.autoRemediate`, value); + formControl.setValue(`${standardName}.action`, action); + }; + // Cancel changes for a standard const handleCancel = (standardName) => { // Get the last saved values @@ -523,8 +572,6 @@ const CippStandardAccordion = ({ // Get current values and check if they differ from saved values const current = _.get(watchedValues, standardName); const saved = _.get(savedValues, standardName) || {}; - console.log(`Current values for ${standardName}:`, current); - console.log(`Saved values for ${standardName}:`, saved); const hasUnsaved = !_.isEqual(current, saved); @@ -576,8 +623,7 @@ const CippStandardAccordion = ({ // Get field value for validation using lodash's get to properly handle nested properties const fieldValue = _.get(current, component.name); - console.log(`Checking field: ${component.name}, value:`, fieldValue); - console.log(current); + // Check if required field has a value based on its type and multiple property if (component.type === "autoComplete" || component.type === "select") { if (component.multiple) { @@ -615,10 +661,6 @@ const CippStandardAccordion = ({ // 3. There are unsaved changes const canSave = hasAction && requiredFieldsFilled && hasUnsaved; - console.log( - `Standard: ${standardName}, Action Required: ${actionRequired}, Has Action: ${hasAction}, Required Fields Filled: ${requiredFieldsFilled}, Unsaved Changes: ${hasUnsaved}, Can Save: ${canSave}` - ); - return ( {accordionTitle} - {selectedActions && selectedActions?.length > 0 && ( + {/* Hide action chips in drift mode */} + {!isDriftMode && selectedActions && selectedActions?.length > 0 && ( <> {selectedActions?.map((action, index) => ( @@ -696,12 +739,7 @@ const CippStandardAccordion = ({ components={{ // Make links open in new tab with security attributes a: ({ href, children, ...props }) => ( - + {children} ), @@ -751,23 +789,27 @@ const CippStandardAccordion = ({ - - {/* Always show action field as it's required */} - - - + {isDriftMode ? ( + /* Drift mode layout - full width with slider first */ + + {/* Auto-remediate switch takes full width and is first */} + + + handleAutoRemediateChange(standardName, e.target.checked) + } + fullWidth + /> + - {hasAddedComponents && ( - - + {/* Additional components take full width */} + {hasAddedComponents && ( + <> {standard.addedComponent?.map((component, idx) => component?.condition ? ( ) )} - + + )} + + ) : ( + /* Standard mode layout - original grid layout */ + + + - )} - + + {hasAddedComponents && ( + + + {standard.addedComponent?.map((component, idx) => + component?.condition ? ( + + + + ) : ( + + ) + )} + + + )} + + )} diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index ca2a16cf0f75..d74d6f4d3630 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -103,7 +103,7 @@ const StandardCard = memo( display: "flex", flexDirection: "column", ...(isNewStandard(standard.addedDate) && { - mt: 1.5, // Add top margin to accommodate the "New" label + mt: 1.2, // Add top margin to accommodate the "New" label }), }} > @@ -324,12 +324,12 @@ const VirtualizedStandardGrid = memo(({ items, renderItem }) => { return ( ( - + { return ( - + {items.map(({ standard, category }) => { const isSelected = !!selectedStandards[standard.name]; @@ -960,6 +960,7 @@ const CippStandardDialog = ({ sx: { minWidth: "720px", maxHeight: "90vh", + height: "90vh", display: "flex", flexDirection: "column", }, @@ -969,7 +970,7 @@ const CippStandardDialog = ({ {viewMode === "card" ? ( - + ) : ( - + )} - + diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 601ebe1e7aa3..9009c7cb58ad 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -21,6 +21,8 @@ import CippFormComponent from "/src/components/CippComponents/CippFormComponent" import { CippFormTenantSelector } from "../CippComponents/CippFormTenantSelector"; import { CippApiDialog } from "../CippComponents/CippApiDialog"; import ReactTimeAgo from "react-time-ago"; +import { Alert } from "@mui/material"; +import { ApiGetCall } from "../../api/ApiCall"; const StyledTimelineDot = (props) => { const { complete } = props; @@ -65,19 +67,23 @@ const CippStandardsSideBar = ({ createDialog, edit, onSaveSuccess, + onDriftConflictChange, + isDriftMode = false, }) => { const [currentStep, setCurrentStep] = useState(0); const [savedItem, setSavedItem] = useState(null); + const [driftError, setDriftError] = useState(""); + const dialogAfterEffect = (id) => { setSavedItem(id); - + // Reset form's dirty state to prevent unsaved changes warning if (formControl && formControl.reset) { // Get current values and reset the form with them to clear dirty state const currentValues = formControl.getValues(); formControl.reset(currentValues); } - + // Call the onSaveSuccess callback if provided if (typeof onSaveSuccess === "function") { onSaveSuccess(); @@ -86,6 +92,174 @@ const CippStandardsSideBar = ({ const watchForm = useWatch({ control: formControl.control }); + // Use proper CIPP ApiGetCall for drift validation + const driftValidationApi = ApiGetCall({ + url: "/api/ListTenantAlignment", + queryKey: "ListTenantAlignment-drift-validation", + }); + + // Get tenant groups for group membership validation + const tenantGroupsApi = ApiGetCall({ + url: "/api/ListTenantGroups", + queryKey: "ListTenantGroups-drift-validation", + }); + + // Helper function to expand groups to their member tenants + const expandGroupsToTenants = (tenants, groups) => { + const expandedTenants = []; + + tenants.forEach((tenant) => { + const tenantValue = typeof tenant === "object" ? tenant.value : tenant; + const tenantType = typeof tenant === "object" ? tenant.type : null; + + if (tenantType === "Group") { + // Find the group and add all its members + const group = groups?.find((g) => g.Id === tenantValue); + if (group && group.Members) { + group.Members.forEach((member) => { + expandedTenants.push(member.defaultDomainName); + }); + } + } else { + // Regular tenant + expandedTenants.push(tenantValue); + } + }); + + return expandedTenants; + }; + + // Enhanced drift validation using CIPP patterns with group support + const validateDrift = async (tenants) => { + if (!isDriftMode || !tenants || tenants.length === 0) { + setDriftError(""); + onDriftConflictChange?.(false); + return; + } + + try { + // Wait for both APIs to load + if (!driftValidationApi.data || !tenantGroupsApi.data) { + return; + } + + // Filter out current template if editing + console.log("Duplicate detection debug:", { + edit, + currentGUID: watchForm.GUID, + allTemplates: driftValidationApi.data?.map((t) => ({ + GUID: t.GUID, + standardId: t.standardId, + standardName: t.standardName, + })), + }); + + const existingTemplates = driftValidationApi.data.filter((template) => { + const shouldInclude = edit && watchForm.GUID ? template.standardId !== watchForm.GUID : true; + console.log( + `Template ${template.standardId} (${template.standardName}): shouldInclude=${shouldInclude}, currentGUID=${watchForm.GUID}` + ); + return shouldInclude; + }); + + console.log( + "Filtered templates:", + existingTemplates?.map((t) => ({ GUID: t.GUID, standardId: t.standardId, standardName: t.standardName })) + ); + + // Get tenant groups data + const groups = tenantGroupsApi.data?.Results || []; + + // Expand selected tenants (including group members) + const selectedTenantList = expandGroupsToTenants(tenants, groups); + + // Simple conflict check + const conflicts = []; + + // Filter for drift templates only and group by standardId + const driftTemplates = existingTemplates.filter( + (template) => template.standardType === "drift" + ); + const uniqueTemplates = {}; + + driftTemplates.forEach((template) => { + if (!uniqueTemplates[template.standardId]) { + uniqueTemplates[template.standardId] = { + standardName: template.standardName, + tenants: [], + }; + } + uniqueTemplates[template.standardId].tenants.push(template.tenantFilter); + }); + + // Check for conflicts with unique templates + console.log("Checking conflicts with unique templates:", uniqueTemplates); + console.log("Selected tenant list:", selectedTenantList); + + for (const templateId in uniqueTemplates) { + const template = uniqueTemplates[templateId]; + const templateTenants = template.tenants; + + console.log( + `Checking template ${templateId} (${template.standardName}) with tenants:`, + templateTenants + ); + + const hasConflict = selectedTenantList.some((selectedTenant) => { + // Check if any template tenant matches the selected tenant + const conflict = templateTenants.some((templateTenant) => { + if (selectedTenant === "AllTenants" || templateTenant === "AllTenants") { + console.log( + `Conflict found: ${selectedTenant} vs ${templateTenant} (AllTenants match)` + ); + return true; + } + const match = selectedTenant === templateTenant; + if (match) { + console.log(`Conflict found: ${selectedTenant} vs ${templateTenant} (exact match)`); + } + return match; + }); + return conflict; + }); + + console.log(`Template ${templateId} has conflict: ${hasConflict}`); + + if (hasConflict) { + conflicts.push(template.standardName || "Unknown Template"); + } + } + + console.log("Final conflicts:", conflicts); + + if (conflicts.length > 0) { + setDriftError( + `This template has tenants that are assigned to another Drift Template. You can only assign one Drift Template to each tenant. Please check the ${conflicts.join( + ", " + )} template.` + ); + onDriftConflictChange?.(true); + } else { + setDriftError(""); + onDriftConflictChange?.(false); + } + } catch (error) { + setDriftError("Error checking for conflicts" + (error.message ? `: ${error.message}` : "")); + onDriftConflictChange?.(true); + } + }; + + // Watch tenant changes + useEffect(() => { + if (!isDriftMode) return; + + const timeoutId = setTimeout(() => { + validateDrift(watchForm.tenantFilter); + }, 500); + + return () => clearTimeout(timeoutId); + }, [watchForm.tenantFilter, isDriftMode, driftValidationApi.data, tenantGroupsApi.data]); + useEffect(() => { const stepsStatus = { step1: !!_.get(watchForm, "templateName"), @@ -130,12 +304,86 @@ const CippStandardsSideBar = ({ return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); }), }; + return ( + + {isDriftMode ? "About Drift Templates" : "About Standard Templates"} + + {isDriftMode ? ( + + + Drift templates provide continuous monitoring of tenant configurations to detect + unauthorized changes. Each tenant can only have one drift template applied at a time. + + + Remediation Options: + + + • Automatic Remediation: Immediately reverts unauthorized changes + back to the template configuration +
    Manual Remediation: Sends email notifications for review, + allowing you to accept or deny detected changes +
    + + Key Features: + + + • Monitors all security standards, Conditional Access policies, and Intune policies +
    + • Detects changes made outside of CIPP +
    + • Configurable webhook and email notifications +
    • Granular control over deviation acceptance +
    +
    + ) : ( + + + Standard templates can be applied to multiple tenants and allow overlapping + configurations with intelligent merging based on specificity and timing. + + + + Merge Priority (Specificity): + + + 1. Individual Tenant - Highest priority, overrides all others +
    + 2. Tenant Group - Overrides "All Tenants" settings +
    + 3. All Tenants - Lowest priority, default baseline +
    + + + Conflict Resolution: + + + When multiple standards target the same scope (e.g., two tenant-specific templates), + the most recently created template takes precedence. + + + + Example: An "All Tenants" template enables audit log retention for 90 + days, but you need 365 days for one specific tenant. Create a tenant-specific template + with 365-day retention - it will override the global setting for that tenant only. + +
    + )} + + {/* Hidden field to mark drift templates */} + {isDriftMode && ( + + )} + + {/* Show drift error */} + {isDriftMode && driftError && {driftError}} + {watchForm.tenantFilter?.some( (tenant) => tenant.value === "AllTenants" || tenant.type === "Group" ) && ( @@ -175,58 +427,90 @@ const CippStandardsSideBar = ({ /> )} - {updatedAt.date && ( + {/* Drift-specific fields */} + {isDriftMode && ( <> + + + + + )} + {/* Hide schedule options in drift mode */} + {!isDriftMode && ( + <> + {updatedAt.date && ( + <> + + Last Updated by {updatedAt?.user} + + + )} + - Last Updated by {updatedAt?.user} + This setting allows you to create this template and run it only by using "Run Now". )} - - - This setting allows you to create this template and run it only by using "Run Now". -
    - - - - {steps.map((step, index) => ( - - - - {index < steps.length - 1 && } - - {step} - - ))} - - + {/* Hide timeline/ticker in drift mode */} + {!isDriftMode && ( + <> + + + + {steps.map((step, index) => ( + + + + {index < steps.length - 1 && } + + {step} + + ))} + + + + )} {actions.map((action, index) => ( @@ -236,7 +520,9 @@ const CippStandardsSideBar = ({ label={action.label} onClick={action.handler} disabled={ - !(watchForm.tenantFilter && watchForm.tenantFilter.length > 0) || currentStep < 3 + !(watchForm.tenantFilter && watchForm.tenantFilter.length > 0) || + currentStep < 3 || + (isDriftMode && driftError) } /> ))} @@ -247,7 +533,9 @@ const CippStandardsSideBar = ({ createDialog={createDialog} title="Add Standard" api={{ - confirmText: watchForm.runManually + confirmText: isDriftMode + ? "This template will automatically run every 3 hours to detect drift. Are you sure you want to apply this Drift Template?" + : watchForm.runManually ? "Are you sure you want to apply this standard? This template has been set to never run on a schedule. After saving the template you will have to run it manually." : "Are you sure you want to apply this standard? This will apply the template and run every 3 hours.", url: "/api/AddStandardsTemplate", @@ -261,7 +549,15 @@ const CippStandardsSideBar = ({ standards: "standards", ...(edit ? { GUID: "GUID" } : {}), ...(savedItem ? { GUID: savedItem } : {}), - runManually: "runManually", + runManually: isDriftMode ? false : "runManually", + isDriftTemplate: "isDriftTemplate", + ...(isDriftMode + ? { + type: "drift", + driftAlertWebhook: "driftAlertWebhook", + driftAlertEmail: "driftAlertEmail", + } + : {}), }, }} row={formControl.getValues()} @@ -270,6 +566,8 @@ const CippStandardsSideBar = ({ "listStandardTemplates", "listStandards", `listStandardTemplates-${watchForm.GUID}`, + "ListTenantAlignment-drift-validation", + "ListTenantGroups-drift-validation", ]} />
    @@ -290,6 +588,7 @@ CippStandardsSideBar.propTypes = { updatedAt: PropTypes.string, formControl: PropTypes.object.isRequired, onSaveSuccess: PropTypes.func, + onDriftConflictChange: PropTypes.func, }; export default CippStandardsSideBar; diff --git a/src/components/ExecutiveReportButton.js b/src/components/ExecutiveReportButton.js index 764a568e7959..c0fa5088694a 100644 --- a/src/components/ExecutiveReportButton.js +++ b/src/components/ExecutiveReportButton.js @@ -1,6 +1,21 @@ -import React from "react"; -import { Button, Tooltip } from "@mui/material"; -import { PictureAsPdf } from "@mui/icons-material"; +import React, { useState, useMemo } from "react"; +import { + Button, + Tooltip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Box, + Typography, + FormControlLabel, + Switch, + Grid, + Paper, + IconButton, + Divider, +} from "@mui/material"; +import { PictureAsPdf, Visibility, Download, Close, Settings } from "@mui/icons-material"; import { Document, Page, @@ -8,6 +23,7 @@ import { View, StyleSheet, PDFDownloadLink, + PDFViewer, Image, Svg, Path, @@ -19,7 +35,7 @@ import { useSettings } from "../hooks/use-settings"; import { useSecureScore } from "../hooks/use-securescore"; import { ApiGetCall } from "../api/ApiCall"; -// PRODUCTION-GRADE PDF SYSTEM +// PRODUCTION-GRADE PDF SYSTEM WITH CONDITIONAL RENDERING const ExecutiveReportDocument = ({ tenantName, userStats, @@ -29,6 +45,15 @@ const ExecutiveReportDocument = ({ deviceData, conditionalAccessData, standardsCompareData, + sectionConfig = { + executiveSummary: true, + securityStandards: true, + secureScore: true, + licenseManagement: true, + deviceManagement: true, + conditionalAccess: true, + infographics: true, + }, }) => { const currentDate = new Date().toLocaleDateString("en-US", { year: "numeric", @@ -774,95 +799,13 @@ const ExecutiveReportDocument = ({ {/* 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; - })() && ( + {sectionConfig.executiveSummary && ( - Security Standards Assessment + Executive Summary - Detailed evaluation of implemented security standards + Strategic overview of your Microsoft 365 security posture {brandingSettings?.logo && ( @@ -872,88 +815,40 @@ const ExecutiveReportDocument = ({ - 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. + 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. - - - - Security Standards Status - - - Standard - Description - Tags - - Status - - - - {securityControls.map((control, index) => ( - - - {control.name} - - - {control.description} - - - {(() => { - if (typeof control.tags === "object") { - console.log( - "DEBUG: control.tags is an object:", - control.tags, - "for control:", - control.name - ); - } - return control.tags; - })()} - - - {control.status} - - - ))} - + + 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. + - Key Recommendations - - - - - - Immediate Actions: Address - standards marked as "Review" to enhance security posture - + Environment Overview + + + + {userStats?.licensedUsers || "0"} + Licensed Users - - - - Compliance: Ensure all security - standards are properly implemented and maintained - + + {userStats?.unlicensedUsers || "0"} + Unlicensed Users - - - - Monitoring: Establish regular - review cycles for all security standards - + + {userStats?.guests || "0"} + Guest Users - - - - Training: Implement security - awareness programs to reduce human risk factors - + + {userStats?.globalAdmins || "0"} + Global Admins @@ -967,284 +862,37 @@ const ExecutiveReportDocument = ({ )} - {/* STATISTIC PAGE 2 - CHAPTER SPLITTER - Only show if secure score data is available */} - {secureScoreData && secureScoreData?.isSuccess && secureScoreData?.translatedData && ( + {/* STATISTIC PAGE 1 - CHAPTER SPLITTER */} + {sectionConfig.infographics && ( - + - 95% + 83% - of successful cyber attacks{"\n"} - could have been prevented with{"\n"} - proactive security measures + of organizations experienced{"\n"} + more than one cyberattack + {"\n"} + in the past year - Your security resilience is{"\n"} - our primary mission + Proactive security prevents{"\n"} + repeated attacks )} - {/* 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 - - + {/* SECURITY CONTROLS - Only show if standards data is available and enabled */} + {sectionConfig.securityStandards && + (() => { + return securityControls && securityControls.length > 0; + })() && ( - License Management + Security Standards Assessment - Microsoft 365 license allocation and utilization analysis + Detailed evaluation of implemented security standards {brandingSettings?.logo && ( @@ -1254,132 +902,87 @@ const ExecutiveReportDocument = ({ - 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. + 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. - License Allocation Summary + Security Standards Status - License Type - Used - - Available + Standard + Description + Tags + + Status - Total - {licensingData.map((license, index) => ( + {securityControls.map((control, index) => ( - - {(() => { - const licenseValue = license.License || license.license || "N/A"; - if (typeof licenseValue === "object") { - console.log( - "DEBUG: license name is an object:", - licenseValue, - "full license:", - license - ); - } - return licenseValue; - })()} + + {control.name} - - {(() => { - const countUsed = license.CountUsed || license.countUsed || "0"; - if (typeof countUsed === "object") { - console.log( - "DEBUG: license.CountUsed is an object:", - countUsed, - "full license:", - license - ); - } - return countUsed; - })()} + + {control.description} - + {(() => { - const countAvailable = - license.CountAvailable || license.countAvailable || "0"; - if (typeof countAvailable === "object") { + if (typeof control.tags === "object") { console.log( - "DEBUG: license.CountAvailable is an object:", - countAvailable, - "full license:", - license + "DEBUG: control.tags is an object:", + control.tags, + "for control:", + control.name ); } - return countAvailable; - })()} - - - {(() => { - const totalLicenses = license.TotalLicenses || license.totalLicenses || "0"; - if (typeof totalLicenses === "object") { - console.log( - "DEBUG: license.TotalLicenses is an object:", - totalLicenses, - "full license:", - license - ); - } - return totalLicenses; + return control.tags; })()} + + {control.status} + ))} - License Optimization Recommendations + Key Recommendations - Usage Monitoring: Track how - licenses are being used to identify cost-saving opportunities + Immediate Actions: Address + standards marked as "Review" to enhance security posture - Cost Control: Review unused - licenses to reduce unnecessary spending + Compliance: Ensure all security + standards are properly implemented and maintained - Growth Planning: Ensure you have - enough licenses for business expansion without overspending + Monitoring: Establish regular + review cycles for all security standards - Regular Reviews: Conduct - quarterly reviews to maintain cost-effective license allocation + Training: Implement security + awareness programs to reduce human risk factors @@ -1392,34 +995,42 @@ const ExecutiveReportDocument = ({ /> - - )} + )} - {/* DEVICES PAGE - Only show if device data is available */} - {deviceData && Array.isArray(deviceData) && deviceData.length > 0 && ( - <> - {/* STATISTIC PAGE 4 - CHAPTER SPLITTER */} + {/* STATISTIC PAGE 2 - CHAPTER SPLITTER - Only show if secure score data is available and enabled */} + {sectionConfig.infographics && + sectionConfig.secureScore && + secureScoreData && + secureScoreData?.isSuccess && + secureScoreData?.translatedData && ( - + - $4.45M + 95% - average cost of a{"\n"} - data breach in 2024 + of successful cyber attacks{"\n"} + could have been prevented with{"\n"} + proactive security measures - Investment in security - {"\n"} - saves millions in recovery + Your security resilience is{"\n"} + our primary mission + )} + + {/* MICROSOFT SECURE SCORE - DEDICATED PAGE - Only show if secure score data is available and enabled */} + {sectionConfig.secureScore && + secureScoreData && + secureScoreData?.isSuccess && + secureScoreData?.translatedData && ( - Device Management + Microsoft Secure Score - Device compliance status and management overview + Comprehensive security posture measurement and benchmarking {brandingSettings?.logo && ( @@ -1429,210 +1040,654 @@ const ExecutiveReportDocument = ({ - 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. + 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. - Device Compliance Overview + Score Comparison - - - {deviceData.length} - Total Devices + + + + {secureScoreData?.translatedData?.currentScore || "N/A"} + + Current Score - - - { - deviceData.filter( - (device) => - device.complianceState === "compliant" || - device.ComplianceState === "compliant" - ).length - } + + + {secureScoreData?.translatedData?.maxScore || "N/A"} - Compliant + Max Score - - - { - deviceData.filter( - (device) => - device.complianceState !== "compliant" && - device.ComplianceState !== "compliant" - ).length - } + + + {secureScoreData?.translatedData?.percentageVsSimilar || "N/A"}% - Non-Compliant + vs Similar Orgs - - - {Math.round( - (deviceData.filter( - (device) => - device.complianceState === "Compliant" || - device.ComplianceState === "Compliant" - ).length / - deviceData.length) * - 100 - )} - % + + + {secureScoreData?.translatedData?.percentageVsAllTenants || "N/A"}% - Compliance Rate + vs All Orgs - Device Management Summary + 7-Day Score Trend + + + Secure Score Progress + {secureScoreData?.secureScore?.data?.Results && + secureScoreData.secureScore.data.Results.length > 0 ? ( + + + {/* Chart Background */} + - - - Device Name - OS - Compliance - Last Sync + {/* 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 */} + {sectionConfig.licenseManagement && + licensingData && + Array.isArray(licensingData) && + licensingData.length > 0 && ( + <> + {/* STATISTIC PAGE 3 - CHAPTER SPLITTER */} + {sectionConfig.infographics && ( + + + + 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 + + - {deviceData.slice(0, 8).map((device, index) => { - const lastSync = device.lastSyncDateTime - ? new Date(device.lastSyncDateTime).toLocaleDateString() - : "N/A"; - return ( + {licensingData.map((license, index) => ( - + {(() => { - const deviceName = device.deviceName || "N/A"; - if (typeof deviceName === "object") { + const licenseValue = license.License || license.license || "N/A"; + if (typeof licenseValue === "object") { console.log( - "DEBUG: device.deviceName is an object:", - deviceName, - "full device:", - device + "DEBUG: license name is an object:", + licenseValue, + "full license:", + license ); } - return deviceName; + return licenseValue; })()} - + {(() => { - const operatingSystem = device.operatingSystem || "N/A"; - if (typeof operatingSystem === "object") { + const countUsed = license.CountUsed || license.countUsed || "0"; + if (typeof countUsed === "object") { console.log( - "DEBUG: device.operatingSystem is an object:", - operatingSystem, - "full device:", - device + "DEBUG: license.CountUsed is an object:", + countUsed, + "full license:", + license ); } - return operatingSystem; + return countUsed; })()} - - + + {(() => { + const countAvailable = + license.CountAvailable || license.countAvailable || "0"; + if (typeof countAvailable === "object") { + console.log( + "DEBUG: license.CountAvailable is an object:", + countAvailable, + "full license:", + license + ); + } + return countAvailable; + })()} + + + {(() => { + const totalLicenses = + license.TotalLicenses || license.totalLicenses || "0"; + if (typeof totalLicenses === "object") { + console.log( + "DEBUG: license.TotalLicenses is an object:", + totalLicenses, + "full license:", + license + ); + } + return totalLicenses; + })()} + + + ))} + + + + + 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 */} + {sectionConfig.deviceManagement && + deviceData && + Array.isArray(deviceData) && + deviceData.length > 0 && ( + <> + {/* STATISTIC PAGE 4 - CHAPTER SPLITTER */} + {sectionConfig.infographics && ( + + + + $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 ( + + + {(() => { + const deviceName = device.deviceName || "N/A"; + if (typeof deviceName === "object") { + console.log( + "DEBUG: device.deviceName is an object:", + deviceName, + "full device:", + device + ); + } + return deviceName; + })()} + + {(() => { - const complianceState = device.complianceState || "Unknown"; - if (typeof complianceState === "object") { + const operatingSystem = device.operatingSystem || "N/A"; + if (typeof operatingSystem === "object") { console.log( - "DEBUG: device.complianceState is an object:", - complianceState, + "DEBUG: device.operatingSystem is an object:", + operatingSystem, "full device:", device ); } - return complianceState; + return operatingSystem; })()} + + + {(() => { + const complianceState = device.complianceState || "Unknown"; + if (typeof complianceState === "object") { + console.log( + "DEBUG: device.complianceState is an object:", + complianceState, + "full device:", + device + ); + } + return complianceState; + })()} + + + {lastSync} - {lastSync} - - ); - })} + ); + })} + - - - Device Insights + + 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 + + + + {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. - - + + 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}`} - /> - - - - )} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + )} {/* CONDITIONAL ACCESS POLICIES PAGE - Only show if data is available */} - {conditionalAccessData && + {sectionConfig.conditionalAccess && + conditionalAccessData && Array.isArray(conditionalAccessData) && conditionalAccessData.length > 0 && ( <> {/* STATISTIC PAGE 5 - CHAPTER SPLITTER */} - - - - 277 - days - - average time to identify and{"\n"} - contain a data breach + {sectionConfig.infographics && ( + + + + 277 + days + + average time to identify and{"\n"} + contain a data breach + + + + Early detection minimizes{"\n"} + business impact - - - Early detection minimizes{"\n"} - business impact - - + + )} @@ -1876,6 +1931,18 @@ export const ExecutiveReportButton = (props) => { const settings = useSettings(); const brandingSettings = settings.customBranding; + // Preview state + const [previewOpen, setPreviewOpen] = useState(false); + const [sectionConfig, setSectionConfig] = useState({ + executiveSummary: true, + securityStandards: true, + secureScore: true, + licenseManagement: true, + deviceManagement: true, + conditionalAccess: true, + infographics: true, + }); + // Get real secure score data const secureScore = useSecureScore(); @@ -1936,6 +2003,126 @@ export const ExecutiveReportButton = (props) => { new Date().toISOString().split("T")[0] }.pdf`; + // Memoize the document to prevent unnecessary re-renders + const reportDocument = useMemo(() => { + console.log("Creating report document with:", { + tenantName, + tenantId, + userStats, + standardsData, + organizationData, + brandingSettings, + secureScore: secureScore.isSuccess ? secureScore : null, + licensingData: licenseData.isSuccess ? licenseData?.data : null, + deviceData: deviceData.isSuccess ? deviceData?.data : null, + conditionalAccessData: conditionalAccessData.isSuccess ? conditionalAccessData?.data : null, + standardsCompareData: standardsCompareData.isSuccess ? standardsCompareData?.data : null, + sectionConfig, + }); + + try { + return ( + + ); + } catch (error) { + console.error("Error creating ExecutiveReportDocument:", error); + return ( + + + + Error creating document: {error.message} + + + + ); + } + }, [ + tenantName, + tenantId, + userStats, + standardsData, + organizationData, + brandingSettings, + secureScore, + licenseData, + deviceData, + conditionalAccessData, + standardsCompareData, + sectionConfig, + ]); + + // Handle section toggle + const handleSectionToggle = (sectionKey) => { + setSectionConfig((prev) => { + // Count currently enabled sections + const enabledSections = Object.values(prev).filter(Boolean).length; + + // If trying to disable the last remaining section, prevent it + if (prev[sectionKey] && enabledSections === 1) { + return prev; // Don't change state + } + + return { + ...prev, + [sectionKey]: !prev[sectionKey], + }; + }); + }; + + // Section configuration options + const sectionOptions = [ + { + key: "executiveSummary", + label: "Executive Summary", + description: "High-level overview and statistics", + }, + { + key: "securityStandards", + label: "Security Standards", + description: "Compliance assessment and standards evaluation", + }, + { + key: "secureScore", + label: "Microsoft Secure Score", + description: "Security posture measurement and trends", + }, + { + key: "licenseManagement", + label: "License Management", + description: "License allocation and optimization", + }, + { + key: "deviceManagement", + label: "Device Management", + description: "Device compliance and insights", + }, + { + key: "conditionalAccess", + label: "Conditional Access", + description: "Access control policies and analysis", + }, + { + key: "infographics", + label: "Infographic Pages", + description: "Statistical pages with visual elements between sections", + }, + ]; + // Don't render the button if data is not ready if (!shouldShowButton) { return ( @@ -1960,45 +2147,240 @@ export const ExecutiveReportButton = (props) => { } return ( - - } - fileName={fileName} - > - {({ blob, url, loading, error }) => ( - + <> + {/* Main Executive Summary Button */} + + + + + {/* Combined Preview and Configuration Dialog */} + setPreviewOpen(false)} + maxWidth="xl" + fullWidth + sx={{ + "& .MuiDialog-paper": { + height: "95vh", + maxHeight: "95vh", + }, + }} + > + + + Executive Report - {tenantName} + + setPreviewOpen(false)} size="small"> + + + + + + {/* Left Panel - Section Configuration */} + + + + + Report Sections + + + Configure which sections to include in your executive report. Changes are reflected + in real-time. + + + + {sectionOptions.map((option) => ( + + + { + event.stopPropagation(); + handleSectionToggle(option.key); + }} + color="primary" + size="small" + disabled={ + // Disable if this is the last enabled section + sectionConfig[option.key] && + Object.values(sectionConfig).filter(Boolean).length === 1 + } + /> + } + label={ + handleSectionToggle(option.key)}> + + {option.label} + + + {option.description} + + + } + sx={{ margin: 0, width: "100%" }} + /> + + + ))} + + + + + 💡 Pro Tip + + + Enable only the sections relevant to your audience to create focused, impactful + reports. At least one section must be enabled. + + + + + + {/* Right Panel - PDF Preview */} + + + {reportDocument} + + + + + + + + Sections enabled: {Object.values(sectionConfig).filter(Boolean).length} of{" "} + {sectionOptions.length} + + + - - )} - + + +
    + + ); }; diff --git a/src/data/AuditLogSchema.json b/src/data/AuditLogSchema.json index 4ed181f46654..93122adbc4cf 100644 --- a/src/data/AuditLogSchema.json +++ b/src/data/AuditLogSchema.json @@ -16,7 +16,10 @@ "CIPPGeoLocation": "List:countryList", "CIPPBadRepIP": "String", "CIPPHostedIP": "String", - "CIPPIPDetected": "String" + "CIPPIPDetected": "String", + "CIPPUserId": "String", + "CIPPUserKey": "String", + "CIPPUsername": "String" }, "Audit.Exchange": { "Id": "Combination GUID", @@ -71,40 +74,126 @@ "LogonError": "String" }, "List:Operation": [ - { "value": "UserLoggedIn", "label": "A user logged in" }, - { "value": "mailitemsaccessed", "label": "accessed mailbox items" }, - { "value": "add delegation entry.", "label": "added delegation entry" }, - { "value": "add domain to company.", "label": "added domain to company" }, - { "value": "add group.", "label": "added group" }, - { "value": "add member to group.", "label": "added member to group" }, - { "value": "add-mailboxpermission", "label": "added delegate mailbox permissions" }, - { "value": "add member to role.", "label": "added member to role" }, - { "value": "add partner to company.", "label": "added a partner to the directory" }, - { "value": "add service principal.", "label": "added service principal" }, + { + "value": "UserLoggedIn", + "label": "A user logged in" + }, + { + "value": "mailitemsaccessed", + "label": "accessed mailbox items" + }, + { + "value": "add delegation entry.", + "label": "added delegation entry" + }, + { + "value": "add domain to company.", + "label": "added domain to company" + }, + { + "value": "add group.", + "label": "added group" + }, + { + "value": "add member to group.", + "label": "added member to group" + }, + { + "value": "add-mailboxpermission", + "label": "added delegate mailbox permissions" + }, + { + "value": "add member to role.", + "label": "added member to role" + }, + { + "value": "add partner to company.", + "label": "added a partner to the directory" + }, + { + "value": "add service principal.", + "label": "added service principal" + }, { "value": "add service principal credentials.", "label": "added credentials to a service principal" }, - { "value": "add user.", "label": "added user" }, - { "value": "addfolderpermissions", "label": "added permissions to folder" }, - { "value": "applyrecordlabel", "label": "labeled message as a record" }, - { "value": "change user license.", "label": "changed user license" }, - { "value": "change user password.", "label": "changed user password" }, - { "value": "copy", "label": "copied messages to another folder" }, - { "value": "create", "label": "created mailbox item" }, - { "value": "delete group.", "label": "deleted group" }, - { "value": "delete user.", "label": "deleted user" }, - { "value": "harddelete", "label": "purged messages from the mailbox" }, - { "value": "mailboxlogin", "label": "user signed in to mailbox" }, - { "value": "move", "label": "moved messages to another folder" }, - { "value": "movetodeleteditems", "label": "moved messages to deleted items folder" }, - { "value": "new-inboxrule", "label": "created new inbox rule in outlook web app" }, - { "value": "remove delegation entry.", "label": "removed delegation entry" }, - { "value": "remove domain from company.", "label": "removed domain from company" }, - { "value": "remove member from group.", "label": "removed member from group" }, - { "value": "remove member from a role.", "label": "remove member from a role" }, - { "value": "Disable Strong Authentication.", "label": "Disable Strong Authentication." }, - + { + "value": "add user.", + "label": "added user" + }, + { + "value": "addfolderpermissions", + "label": "added permissions to folder" + }, + { + "value": "applyrecordlabel", + "label": "labeled message as a record" + }, + { + "value": "change user license.", + "label": "changed user license" + }, + { + "value": "change user password.", + "label": "changed user password" + }, + { + "value": "copy", + "label": "copied messages to another folder" + }, + { + "value": "create", + "label": "created mailbox item" + }, + { + "value": "delete group.", + "label": "deleted group" + }, + { + "value": "delete user.", + "label": "deleted user" + }, + { + "value": "harddelete", + "label": "purged messages from the mailbox" + }, + { + "value": "mailboxlogin", + "label": "user signed in to mailbox" + }, + { + "value": "move", + "label": "moved messages to another folder" + }, + { + "value": "movetodeleteditems", + "label": "moved messages to deleted items folder" + }, + { + "value": "new-inboxrule", + "label": "created new inbox rule in outlook web app" + }, + { + "value": "remove delegation entry.", + "label": "removed delegation entry" + }, + { + "value": "remove domain from company.", + "label": "removed domain from company" + }, + { + "value": "remove member from group.", + "label": "removed member from group" + }, + { + "value": "remove member from a role.", + "label": "remove member from a role" + }, + { + "value": "Disable Strong Authentication.", + "label": "Disable Strong Authentication." + }, { "value": "remove service principal.", "label": "removed a service principal from the directory" @@ -113,19 +202,58 @@ "value": "remove service principal credentials.", "label": "removed credentials from a service principal" }, - { "value": "remove-mailboxpermission", "label": "removed delegate mailbox permissions" }, - { "value": "remove member from role.", "label": "removed a user from a directory role" }, - { "value": "remove partner from company.", "label": "removed a partner from the directory" }, - { "value": "removefolderpermissions", "label": "removed permissions from folder" }, - { "value": "reset user password.", "label": "reset user password" }, - { "value": "send", "label": "sent message" }, - { "value": "sendas", "label": "sent message using send as permissions" }, - { "value": "sendonbehalf", "label": "sent message using send on behalf permissions" }, - { "value": "set company contact information.", "label": "set company contact information" }, - { "value": "set company information.", "label": "set company information" }, - { "value": "set delegation entry.", "label": "set delegation entry" }, - { "value": "set dirsyncenabled flag.", "label": "turned on azure ad sync" }, - { "value": "set domain authentication.", "label": "set domain authentication" }, + { + "value": "remove-mailboxpermission", + "label": "removed delegate mailbox permissions" + }, + { + "value": "remove member from role.", + "label": "removed a user from a directory role" + }, + { + "value": "remove partner from company.", + "label": "removed a partner from the directory" + }, + { + "value": "removefolderpermissions", + "label": "removed permissions from folder" + }, + { + "value": "reset user password.", + "label": "reset user password" + }, + { + "value": "send", + "label": "sent message" + }, + { + "value": "sendas", + "label": "sent message using send as permissions" + }, + { + "value": "sendonbehalf", + "label": "sent message using send on behalf permissions" + }, + { + "value": "set company contact information.", + "label": "set company contact information" + }, + { + "value": "set company information.", + "label": "set company information" + }, + { + "value": "set delegation entry.", + "label": "set delegation entry" + }, + { + "value": "set dirsyncenabled flag.", + "label": "turned on azure ad sync" + }, + { + "value": "set domain authentication.", + "label": "set domain authentication" + }, { "value": "set federation settings on domain.", "label": "updated the federation settings for a domain" @@ -134,29 +262,69 @@ "value": "set force change user password.", "label": "set property that forces user to change password" }, - { "value": "set-inboxrule", "label": "modified inbox rule from outlook web app" }, - { "value": "set license properties.", "label": "set license properties" }, - { "value": "set password policy.", "label": "set password policy" }, - { "value": "softdelete", "label": "deleted messages from deleted items folder" }, - { "value": "update", "label": "updated message" }, - { "value": "update user.", "label": "updated user" }, - { "value": "update group.", "label": "updated group" }, - { "value": "update domain.", "label": "updated domain" }, + { + "value": "set-inboxrule", + "label": "modified inbox rule from outlook web app" + }, + { + "value": "set license properties.", + "label": "set license properties" + }, + { + "value": "set password policy.", + "label": "set password policy" + }, + { + "value": "softdelete", + "label": "deleted messages from deleted items folder" + }, + { + "value": "update", + "label": "updated message" + }, + { + "value": "update user.", + "label": "updated user" + }, + { + "value": "update group.", + "label": "updated group" + }, + { + "value": "update domain.", + "label": "updated domain" + }, { "value": "updatecalendardelegation", "label": "added or removed user with delegate access to calendar folder" }, - { "value": "updatefolderpermissions", "label": "modified folder permission" }, - { "value": "updateinboxrules", "label": "updated inbox rules from outlook client" }, - { "value": "verify domain.", "label": "verified domain" }, - { "value": "verify email verified domain.", "label": "verified email verified domain" }, + { + "value": "updatefolderpermissions", + "label": "modified folder permission" + }, + { + "value": "updateinboxrules", + "label": "updated inbox rules from outlook client" + }, + { + "value": "verify domain.", + "label": "verified domain" + }, + { + "value": "verify email verified domain.", + "label": "verified email verified domain" + }, { "value": "Update StsRefreshTokenValidFrom Timestamp.", "label": "Update StsRefreshTokenValidFrom Timestamp." } ], "List:LogonType": [ - { "value": 0, "Membername": "Owner", "label": "The mailbox owner." }, + { + "value": 0, + "Membername": "Owner", + "label": "The mailbox owner." + }, { "value": 1, "Membername": "Admin", @@ -177,19 +345,63 @@ "Membername": "SystemService", "label": "A service account in the Microsoft datacenter" }, - { "value": 5, "Membername": "BestAccess", "label": "Reserved for internal use." }, - { "value": 6, "Membername": "DelegatedAdmin", "label": "A delegated administrator." } + { + "value": 5, + "Membername": "BestAccess", + "label": "Reserved for internal use." + }, + { + "value": 6, + "Membername": "DelegatedAdmin", + "label": "A delegated administrator." + } ], "List:UserType": [ - { "value": 0, "Membername": "Regular", "label": "A regular user." }, - { "value": 1, "Membername": "Reserved", "label": "A reserved user." }, - { "value": 2, "Membername": "Admin", "label": "An administrator." }, - { "value": 3, "Membername": "DcAdmin", "label": "A Microsoft datacenter operator." }, - { "value": 4, "Membername": "System", "label": "A system account." }, - { "value": 5, "Membername": "Application", "label": "An application." }, - { "value": 6, "Membername": "ServicePrincipal", "label": "A service principal." }, - { "value": 7, "Membername": "CustomPolicy", "label": "A custom policy." }, - { "value": 8, "Membername": "SystemPolicy", "label": "A system policy." } + { + "value": 0, + "Membername": "Regular", + "label": "A regular user." + }, + { + "value": 1, + "Membername": "Reserved", + "label": "A reserved user." + }, + { + "value": 2, + "Membername": "Admin", + "label": "An administrator." + }, + { + "value": 3, + "Membername": "DcAdmin", + "label": "A Microsoft datacenter operator." + }, + { + "value": 4, + "Membername": "System", + "label": "A system account." + }, + { + "value": 5, + "Membername": "Application", + "label": "An application." + }, + { + "value": 6, + "Membername": "ServicePrincipal", + "label": "A service principal." + }, + { + "value": 7, + "Membername": "CustomPolicy", + "label": "A custom policy." + }, + { + "value": 8, + "Membername": "SystemPolicy", + "label": "A system policy." + } ], "List:AuditLogRecordType": [ { @@ -207,13 +419,21 @@ "Membername": "ExchangeItemGroup", "label": "Events from an Exchange mailbox audit log for actions that can be performed on multiple items, such as moving or deleted one or more email messages." }, - { "value": 4, "Membername": "SharePoint", "label": "SharePoint events." }, + { + "value": 4, + "Membername": "SharePoint", + "label": "SharePoint events." + }, { "value": 6, "Membername": "SharePointFileOperation", "label": "SharePoint file operation events." }, - { "value": 7, "Membername": "OneDrive", "label": "OneDrive for Business events." }, + { + "value": 7, + "Membername": "OneDrive", + "label": "OneDrive for Business events." + }, { "value": 8, "Membername": "AzureActiveDirectory", @@ -269,9 +489,21 @@ "Membername": "ExchangeAggregatedOperation", "label": "Aggregated Exchange mailbox auditing events." }, - { "value": 20, "Membername": "PowerBIAudit", "label": "Power BI events." }, - { "value": 21, "Membername": "CRM", "label": "Dynamics 365 events." }, - { "value": 22, "Membername": "Yammer", "label": "Yammer events." }, + { + "value": 20, + "Membername": "PowerBIAudit", + "label": "Power BI events." + }, + { + "value": 21, + "Membername": "CRM", + "label": "Dynamics 365 events." + }, + { + "value": 22, + "Membername": "Yammer", + "label": "Yammer events." + }, { "value": 23, "Membername": "SkypeForBusinessCmdlets", @@ -282,7 +514,11 @@ "Membername": "Discovery", "label": "Events for eDiscovery activities performed by running content searches and managing eDiscovery cases in the Security & Compliance Center." }, - { "value": 25, "Membername": "MicrosoftTeams", "label": "Events from Microsoft Teams." }, + { + "value": 25, + "Membername": "MicrosoftTeams", + "label": "Events from Microsoft Teams." + }, { "value": 28, "Membername": "ThreatIntelligence", @@ -298,8 +534,16 @@ "Membername": "MicrosoftFlow", "label": "Microsoft Power Automate (formerly called Microsoft Flow) events." }, - { "value": 31, "Membername": "AeD", "label": "Advanced eDiscovery events." }, - { "value": 32, "Membername": "MicrosoftStream", "label": "Microsoft Stream events." }, + { + "value": 31, + "Membername": "AeD", + "label": "Advanced eDiscovery events." + }, + { + "value": 32, + "Membername": "MicrosoftStream", + "label": "Microsoft Stream events." + }, { "value": 33, "Membername": "ComplianceDLPSharePointClassification", @@ -310,7 +554,11 @@ "Membername": "ThreatFinder", "label": "Campaign-related events from Microsoft Defender for Office 365." }, - { "value": 35, "Membername": "Project", "label": "Microsoft Project events." }, + { + "value": 35, + "Membername": "Project", + "label": "Microsoft Project events." + }, { "value": 36, "Membername": "SharePointListOperation", @@ -326,7 +574,11 @@ "Membername": "DataGovernance", "label": "Events related to retention policies and retention labels in the Security & Compliance Center" }, - { "value": 39, "Membername": "Kaizala", "label": "Kaizala events." }, + { + "value": 39, + "Membername": "Kaizala", + "label": "Kaizala events." + }, { "value": 40, "Membername": "SecurityComplianceAlerts", @@ -352,7 +604,11 @@ "Membername": "WorkplaceAnalytics", "label": "Workplace Analytics events." }, - { "value": 45, "Membername": "PowerAppsApp", "label": "Power Apps events." }, + { + "value": 45, + "Membername": "PowerAppsApp", + "label": "Power Apps events." + }, { "value": 46, "Membername": "PowerAppsPlan", @@ -408,13 +664,21 @@ "Membername": "SharePointFieldOperation", "label": "SharePoint list field events." }, - { "value": 57, "Membername": "MicrosoftTeamsAdmin", "label": "Teams admin events." }, + { + "value": 57, + "Membername": "MicrosoftTeamsAdmin", + "label": "Teams admin events." + }, { "value": 58, "Membername": "HRSignal", "label": "Events related to HR data signals that support the Insider risk management solution." }, - { "value": 59, "Membername": "MicrosoftTeamsDevice", "label": "Teams device events." }, + { + "value": 59, + "Membername": "MicrosoftTeamsDevice", + "label": "Teams device events." + }, { "value": 60, "Membername": "MicrosoftTeamsAnalytics", @@ -430,15 +694,31 @@ "Membername": "Campaign", "label": "Email campaign events from Microsoft Defender for Office 365." }, - { "value": 63, "Membername": "DLPEndpoint", "label": "Endpoint DLP events." }, + { + "value": 63, + "Membername": "DLPEndpoint", + "label": "Endpoint DLP events." + }, { "value": 64, "Membername": "AirInvestigation", "label": "Automated incident response (AIR) events." }, - { "value": 65, "Membername": "Quarantine", "label": "Quarantine events." }, - { "value": 66, "Membername": "MicrosoftForms", "label": "Microsoft Forms events." }, - { "value": 67, "Membername": "ApplicationAudit", "label": "Application audit events." }, + { + "value": 65, + "Membername": "Quarantine", + "label": "Quarantine events." + }, + { + "value": 66, + "Membername": "MicrosoftForms", + "label": "Microsoft Forms events." + }, + { + "value": 67, + "Membername": "ApplicationAudit", + "label": "Application audit events." + }, { "value": 68, "Membername": "ComplianceSupervisionExchange", @@ -464,13 +744,21 @@ "Membername": "MipAutoLabelSharePointPolicyLocation", "label": "Auto-labeling policy events in SharePoint." }, - { "value": 73, "Membername": "MicrosoftTeamsShifts", "label": "Teams Shifts events." }, + { + "value": 73, + "Membername": "MicrosoftTeamsShifts", + "label": "Teams Shifts events." + }, { "value": 75, "Membername": "MipAutoLabelExchangeItem", "label": "Auto-labeling events in Exchange." }, - { "value": 76, "Membername": "CortanaBriefing", "label": "Briefing email events." }, + { + "value": 76, + "Membername": "CortanaBriefing", + "label": "Briefing email events." + }, { "value": 78, "Membername": "WDATPAlerts", @@ -526,15 +814,31 @@ "Membername": "PhysicalBadgingSignal", "label": "Events related to physical badging signals that support the Insider risk management solution." }, - { "value": 93, "Membername": "AipDiscover", "label": "AIP scanner events" }, + { + "value": 93, + "Membername": "AipDiscover", + "label": "AIP scanner events" + }, { "value": 94, "Membername": "AipSensitivityLabelAction", "label": "AIP sensitivity label events" }, - { "value": 95, "Membername": "AipProtectionAction", "label": "AIP protection events" }, - { "value": 96, "Membername": "AipFileDeleted", "label": "AIP file deletion events" }, - { "value": 97, "Membername": "AipHeartBeat", "label": "AIP heartbeat events" }, + { + "value": 95, + "Membername": "AipProtectionAction", + "label": "AIP protection events" + }, + { + "value": 96, + "Membername": "AipFileDeleted", + "label": "AIP file deletion events" + }, + { + "value": 97, + "Membername": "AipHeartBeat", + "label": "AIP heartbeat events" + }, { "value": 98, "Membername": "MCASAlerts", @@ -560,8 +864,16 @@ "Membername": "SharePointSearch", "label": "Events related to searching an organization's SharePoint home site." }, - { "value": 103, "Membername": "PrivacyInsights", "label": "Privacy insight events." }, - { "value": 105, "Membername": "MyAnalyticsSettings", "label": "MyAnalytics events." }, + { + "value": 103, + "Membername": "PrivacyInsights", + "label": "Privacy insight events." + }, + { + "value": 105, + "Membername": "MyAnalyticsSettings", + "label": "MyAnalytics events." + }, { "value": 106, "Membername": "SecurityComplianceUserChange", @@ -617,13 +929,21 @@ "Membername": "PowerPagesSite", "label": "Activities related to Power Pages site." }, - { "value": 188, "Membername": "PlannerPlan", "label": "Microsoft Planner plan events." }, + { + "value": 188, + "Membername": "PlannerPlan", + "label": "Microsoft Planner plan events." + }, { "value": 189, "Membername": "PlannerCopyPlan", "label": "Microsoft Planner copy plan events." }, - { "value": 190, "Membername": "PlannerTask", "label": "Microsoft Planner task events." }, + { + "value": 190, + "Membername": "PlannerTask", + "label": "Microsoft Planner task events." + }, { "value": 191, "Membername": "PlannerRoster", @@ -674,7 +994,11 @@ "Membername": "ProjectForThewebRoadmapSettings", "label": "Microsoft Project for the web roadmap tenant settings events." }, - { "value": 216, "Membername": "Viva Goals", "label": "Viva Goals events." }, + { + "value": 216, + "Membername": "Viva Goals", + "label": "Viva Goals events." + }, { "value": 217, "Membername": "MicrosoftGraphDataConnectConsent", @@ -685,7 +1009,11 @@ "Membername": "AttackSimAdmin", "label": "Events related to admin activities in Attack Simulation & Training in Microsoft Defender for Office 365." }, - { "value": 230, "Membername": "TeamsUpStrings", "label": "Teams UpStrings App Events." }, + { + "value": 230, + "Membername": "TeamsUpStrings", + "label": "Teams UpStrings App Events." + }, { "value": 231, "Membername": "PlannerRosterSensitivityLabel", @@ -718,257 +1046,1013 @@ } ], "List:countryList": [ - { "value": "AF", "label": "Afghanistan" }, - { "value": "AX", "label": "\u00c5land Islands" }, - { "value": "AL", "label": "Albania" }, - { "value": "DZ", "label": "Algeria" }, - { "value": "AS", "label": "American Samoa" }, - { "value": "AD", "label": "Andorra" }, - { "value": "AO", "label": "Angola" }, - { "value": "AI", "label": "Anguilla" }, - { "value": "AQ", "label": "Antarctica" }, - { "value": "AG", "label": "Antigua and Barbuda" }, - { "value": "AR", "label": "Argentina" }, - { "value": "AM", "label": "Armenia" }, - { "value": "AW", "label": "Aruba" }, - { "value": "AC", "label": "Ascension Island" }, - { "value": "AU", "label": "Australia" }, - { "value": "AT", "label": "Austria" }, - { "value": "AZ", "label": "Azerbaijan" }, - { "value": "BS", "label": "Bahamas" }, - { "value": "BH", "label": "Bahrain" }, - { "value": "BD", "label": "Bangladesh" }, - { "value": "BB", "label": "Barbados" }, - { "value": "BY", "label": "Belarus" }, - { "value": "BE", "label": "Belgium" }, - { "value": "BZ", "label": "Belize" }, - { "value": "BJ", "label": "Benin" }, - { "value": "BM", "label": "Bermuda" }, - { "value": "BT", "label": "Bhutan" }, - { "value": "BO", "label": "Bolivia, Plurinational State of" }, - { "value": "BQ", "label": "Bonaire, Sint Eustatius and Saba" }, - { "value": "BA", "label": "Bosnia and Herzegovina" }, - { "value": "BW", "label": "Botswana" }, - { "value": "BV", "label": "Bouvet Island" }, - { "value": "BR", "label": "Brazil" }, - { "value": "IO", "label": "British Indian Ocean Territory" }, - { "value": "BN", "label": "Brunei Darussalam" }, - { "value": "BG", "label": "Bulgaria" }, - { "value": "BF", "label": "Burkina Faso" }, - { "value": "BI", "label": "Burundi" }, - { "value": "KH", "label": "Cambodia" }, - { "value": "CM", "label": "Cameroon" }, - { "value": "CA", "label": "Canada" }, - { "value": "CV", "label": "Cape Verde" }, - { "value": "KY", "label": "Cayman Islands" }, - { "value": "CF", "label": "Central African Republic" }, - { "value": "TD", "label": "Chad" }, - { "value": "CL", "label": "Chile" }, - { "value": "CN", "label": "China" }, - { "value": "CX", "label": "Christmas Island" }, - { "value": "CC", "label": "Cocos (Keeling) Islands" }, - { "value": "CO", "label": "Colombia" }, - { "value": "KM", "label": "Comoros" }, - { "value": "CG", "label": "Congo" }, - { "value": "CD", "label": "Congo, the Democratic Republic of the" }, - { "value": "CK", "label": "Cook Islands" }, - { "value": "CR", "label": "Costa Rica" }, - { "value": "CI", "label": "C\u00f4te d'Ivoire" }, - { "value": "HR", "label": "Croatia" }, - { "value": "CU", "label": "Cuba" }, - { "value": "CW", "label": "Cura\u00e7ao" }, - { "value": "CY", "label": "Cyprus" }, - { "value": "CZ", "label": "Czech Republic" }, - { "value": "DK", "label": "Denmark" }, - { "value": "DG", "label": "Diego Garcia" }, - { "value": "DJ", "label": "Djibouti" }, - { "value": "DM", "label": "Dominica" }, - { "value": "DO", "label": "Dominican Republic" }, - { "value": "EC", "label": "Ecuador" }, - { "value": "EG", "label": "Egypt" }, - { "value": "SV", "label": "El Salvador" }, - { "value": "GQ", "label": "Equatorial Guinea" }, - { "value": "ER", "label": "Eritrea" }, - { "value": "EE", "label": "Estonia" }, - { "value": "ET", "label": "Ethiopia" }, - { "value": "FK", "label": "Falkland Islands (Malvinas)" }, - { "value": "FO", "label": "Faroe Islands" }, - { "value": "FJ", "label": "Fiji" }, - { "value": "FI", "label": "Finland" }, - { "value": "FR", "label": "France" }, - { "value": "GF", "label": "French Guiana" }, - { "value": "PF", "label": "French Polynesia" }, - { "value": "TF", "label": "French Southern Territories" }, - { "value": "GA", "label": "Gabon" }, - { "value": "GM", "label": "Gambia" }, - { "value": "GE", "label": "Georgia" }, - { "value": "DE", "label": "Germany" }, - { "value": "GH", "label": "Ghana" }, - { "value": "GI", "label": "Gibraltar" }, - { "value": "GR", "label": "Greece" }, - { "value": "GL", "label": "Greenland" }, - { "value": "GD", "label": "Grenada" }, - { "value": "GP", "label": "Guadeloupe" }, - { "value": "GU", "label": "Guam" }, - { "value": "GT", "label": "Guatemala" }, - { "value": "GG", "label": "Guernsey" }, - { "value": "GN", "label": "Guinea" }, - { "value": "GW", "label": "Guinea-Bissau" }, - { "value": "GY", "label": "Guyana" }, - { "value": "HT", "label": "Haiti" }, - { "value": "HM", "label": "Heard Island and McDonald Islands" }, - { "value": "VA", "label": "Holy See (Vatican City State)" }, - { "value": "HN", "label": "Honduras" }, - { "value": "HK", "label": "Hong Kong" }, - { "value": "HU", "label": "Hungary" }, - { "value": "IS", "label": "Iceland" }, - { "value": "IN", "label": "India" }, - { "value": "ID", "label": "Indonesia" }, - { "value": "IR", "label": "Iran, Islamic Republic of" }, - { "value": "IQ", "label": "Iraq" }, - { "value": "IE", "label": "Ireland" }, - { "value": "IM", "label": "Isle of Man" }, - { "value": "IL", "label": "Israel" }, - { "value": "IT", "label": "Italy" }, - { "value": "JM", "label": "Jamaica" }, - { "value": "JP", "label": "Japan" }, - { "value": "JE", "label": "Jersey" }, - { "value": "JO", "label": "Jordan" }, - { "value": "KZ", "label": "Kazakhstan" }, - { "value": "KE", "label": "Kenya" }, - { "value": "KI", "label": "Kiribati" }, - { "value": "KP", "label": "Korea, Democratic People's Republic of" }, - { "value": "KR", "label": "Korea, Republic of" }, - { "value": "XK", "label": "Kosovo" }, - { "value": "KW", "label": "Kuwait" }, - { "value": "KG", "label": "Kyrgyzstan" }, - { "value": "LA", "label": "Lao People's Democratic Republic" }, - { "value": "LV", "label": "Latvia" }, - { "value": "LB", "label": "Lebanon" }, - { "value": "LS", "label": "Lesotho" }, - { "value": "LR", "label": "Liberia" }, - { "value": "LY", "label": "Libya" }, - { "value": "LI", "label": "Liechtenstein" }, - { "value": "LT", "label": "Lithuania" }, - { "value": "LU", "label": "Luxembourg" }, - { "value": "MO", "label": "Macao" }, - { "value": "MK", "label": "Macedonia, the Former Yugoslav Republic of" }, - { "value": "MG", "label": "Madagascar" }, - { "value": "MW", "label": "Malawi" }, - { "value": "MY", "label": "Malaysia" }, - { "value": "MV", "label": "Maldives" }, - { "value": "ML", "label": "Mali" }, - { "value": "MT", "label": "Malta" }, - { "value": "MH", "label": "Marshall Islands" }, - { "value": "MQ", "label": "Martinique" }, - { "value": "MR", "label": "Mauritania" }, - { "value": "MU", "label": "Mauritius" }, - { "value": "YT", "label": "Mayotte" }, - { "value": "MX", "label": "Mexico" }, - { "value": "FM", "label": "Micronesia, Federated States of" }, - { "value": "MD", "label": "Moldova, Republic of" }, - { "value": "MC", "label": "Monaco" }, - { "value": "MN", "label": "Mongolia" }, - { "value": "ME", "label": "Montenegro" }, - { "value": "MS", "label": "Montserrat" }, - { "value": "MA", "label": "Morocco" }, - { "value": "MZ", "label": "Mozambique" }, - { "value": "MM", "label": "Myanmar" }, - { "value": "NA", "label": "Namibia" }, - { "value": "NR", "label": "Nauru" }, - { "value": "NP", "label": "Nepal" }, - { "value": "NL", "label": "Netherlands" }, - { "value": "NC", "label": "New Caledonia" }, - { "value": "NZ", "label": "New Zealand" }, - { "value": "NI", "label": "Nicaragua" }, - { "value": "NE", "label": "Niger" }, - { "value": "NG", "label": "Nigeria" }, - { "value": "NU", "label": "Niue" }, - { "value": "NF", "label": "Norfolk Island" }, - { "value": "MP", "label": "Northern Mariana Islands" }, - { "value": "NO", "label": "Norway" }, - { "value": "OM", "label": "Oman" }, - { "value": "PK", "label": "Pakistan" }, - { "value": "PW", "label": "Palau" }, - { "value": "PS", "label": "Palestine, State of" }, - { "value": "PA", "label": "Panama" }, - { "value": "PG", "label": "Papua New Guinea" }, - { "value": "PY", "label": "Paraguay" }, - { "value": "PE", "label": "Peru" }, - { "value": "PH", "label": "Philippines" }, - { "value": "PN", "label": "Pitcairn" }, - { "value": "PL", "label": "Poland" }, - { "value": "PT", "label": "Portugal" }, - { "value": "PR", "label": "Puerto Rico" }, - { "value": "QA", "label": "Qatar" }, - { "value": "RE", "label": "R\u00e9union" }, - { "value": "RO", "label": "Romania" }, - { "value": "RU", "label": "Russian Federation" }, - { "value": "RW", "label": "Rwanda" }, - { "value": "BL", "label": "Saint Barth\u00e9lemy" }, - { "value": "SH", "label": "Saint Helena, Ascension and Tristan da Cunha" }, - { "value": "KN", "label": "Saint Kitts and Nevis" }, - { "value": "LC", "label": "Saint Lucia" }, - { "value": "MF", "label": "Saint Martin (French part)" }, - { "value": "PM", "label": "Saint Pierre and Miquelon" }, - { "value": "VC", "label": "Saint Vincent and the Grenadines" }, - { "value": "WS", "label": "Samoa" }, - { "value": "SM", "label": "San Marino" }, - { "value": "ST", "label": "Sao Tome and Principe" }, - { "value": "SA", "label": "Saudi Arabia" }, - { "value": "SN", "label": "Senegal" }, - { "value": "RS", "label": "Serbia" }, - { "value": "SC", "label": "Seychelles" }, - { "value": "SL", "label": "Sierra Leone" }, - { "value": "SG", "label": "Singapore" }, - { "value": "SX", "label": "Sint Maarten (Dutch part)" }, - { "value": "SK", "label": "Slovakia" }, - { "value": "SI", "label": "Slovenia" }, - { "value": "SB", "label": "Solomon Islands" }, - { "value": "SO", "label": "Somalia" }, - { "value": "ZA", "label": "South Africa" }, - { "value": "GS", "label": "South Georgia and the South Sandwich Islands" }, - { "value": "SS", "label": "South Sudan" }, - { "value": "ES", "label": "Spain" }, - { "value": "LK", "label": "Sri Lanka" }, - { "value": "SD", "label": "Sudan" }, - { "value": "SR", "label": "Suriname" }, - { "value": "SJ", "label": "Svalbard and Jan Mayen" }, - { "value": "SZ", "label": "Swaziland" }, - { "value": "SE", "label": "Sweden" }, - { "value": "CH", "label": "Switzerland" }, - { "value": "SY", "label": "Syrian Arab Republic" }, - { "value": "TW", "label": "Taiwan, Province of China" }, - { "value": "TJ", "label": "Tajikistan" }, - { "value": "TZ", "label": "Tanzania, United Republic of" }, - { "value": "TH", "label": "Thailand" }, - { "value": "TL", "label": "Timor-Leste" }, - { "value": "TG", "label": "Togo" }, - { "value": "TK", "label": "Tokelau" }, - { "value": "TO", "label": "Tonga" }, - { "value": "TT", "label": "Trinidad and Tobago" }, - { "value": "TN", "label": "Tunisia" }, - { "value": "TR", "label": "Turkey" }, - { "value": "TM", "label": "Turkmenistan" }, - { "value": "TC", "label": "Turks and Caicos Islands" }, - { "value": "TV", "label": "Tuvalu" }, - { "value": "UG", "label": "Uganda" }, - { "value": "UA", "label": "Ukraine" }, - { "value": "AE", "label": "United Arab Emirates" }, - { "value": "GB", "label": "United Kingdom" }, - { "value": "US", "label": "United States" }, - { "value": "UM", "label": "United States Minor Outlying Islands" }, - { "value": "UY", "label": "Uruguay" }, - { "value": "UZ", "label": "Uzbekistan" }, - { "value": "VU", "label": "Vanuatu" }, - { "value": "VE", "label": "Venezuela, Bolivarian Republic of" }, - { "value": "VN", "label": "Viet Nam" }, - { "value": "VG", "label": "Virgin Islands, British" }, - { "value": "VI", "label": "Virgin Islands, U.S." }, - { "value": "WF", "label": "Wallis and Futuna" }, - { "value": "EH", "label": "Western Sahara" }, - { "value": "YE", "label": "Yemen" }, - { "value": "ZM", "label": "Zambia" }, - { "value": "ZW", "label": "Zimbabwe" } + { + "value": "AF", + "label": "Afghanistan" + }, + { + "value": "AX", + "label": "\u00c5land Islands" + }, + { + "value": "AL", + "label": "Albania" + }, + { + "value": "DZ", + "label": "Algeria" + }, + { + "value": "AS", + "label": "American Samoa" + }, + { + "value": "AD", + "label": "Andorra" + }, + { + "value": "AO", + "label": "Angola" + }, + { + "value": "AI", + "label": "Anguilla" + }, + { + "value": "AQ", + "label": "Antarctica" + }, + { + "value": "AG", + "label": "Antigua and Barbuda" + }, + { + "value": "AR", + "label": "Argentina" + }, + { + "value": "AM", + "label": "Armenia" + }, + { + "value": "AW", + "label": "Aruba" + }, + { + "value": "AC", + "label": "Ascension Island" + }, + { + "value": "AU", + "label": "Australia" + }, + { + "value": "AT", + "label": "Austria" + }, + { + "value": "AZ", + "label": "Azerbaijan" + }, + { + "value": "BS", + "label": "Bahamas" + }, + { + "value": "BH", + "label": "Bahrain" + }, + { + "value": "BD", + "label": "Bangladesh" + }, + { + "value": "BB", + "label": "Barbados" + }, + { + "value": "BY", + "label": "Belarus" + }, + { + "value": "BE", + "label": "Belgium" + }, + { + "value": "BZ", + "label": "Belize" + }, + { + "value": "BJ", + "label": "Benin" + }, + { + "value": "BM", + "label": "Bermuda" + }, + { + "value": "BT", + "label": "Bhutan" + }, + { + "value": "BO", + "label": "Bolivia, Plurinational State of" + }, + { + "value": "BQ", + "label": "Bonaire, Sint Eustatius and Saba" + }, + { + "value": "BA", + "label": "Bosnia and Herzegovina" + }, + { + "value": "BW", + "label": "Botswana" + }, + { + "value": "BV", + "label": "Bouvet Island" + }, + { + "value": "BR", + "label": "Brazil" + }, + { + "value": "IO", + "label": "British Indian Ocean Territory" + }, + { + "value": "BN", + "label": "Brunei Darussalam" + }, + { + "value": "BG", + "label": "Bulgaria" + }, + { + "value": "BF", + "label": "Burkina Faso" + }, + { + "value": "BI", + "label": "Burundi" + }, + { + "value": "KH", + "label": "Cambodia" + }, + { + "value": "CM", + "label": "Cameroon" + }, + { + "value": "CA", + "label": "Canada" + }, + { + "value": "CV", + "label": "Cape Verde" + }, + { + "value": "KY", + "label": "Cayman Islands" + }, + { + "value": "CF", + "label": "Central African Republic" + }, + { + "value": "TD", + "label": "Chad" + }, + { + "value": "CL", + "label": "Chile" + }, + { + "value": "CN", + "label": "China" + }, + { + "value": "CX", + "label": "Christmas Island" + }, + { + "value": "CC", + "label": "Cocos (Keeling) Islands" + }, + { + "value": "CO", + "label": "Colombia" + }, + { + "value": "KM", + "label": "Comoros" + }, + { + "value": "CG", + "label": "Congo" + }, + { + "value": "CD", + "label": "Congo, the Democratic Republic of the" + }, + { + "value": "CK", + "label": "Cook Islands" + }, + { + "value": "CR", + "label": "Costa Rica" + }, + { + "value": "CI", + "label": "C\u00f4te d'Ivoire" + }, + { + "value": "HR", + "label": "Croatia" + }, + { + "value": "CU", + "label": "Cuba" + }, + { + "value": "CW", + "label": "Cura\u00e7ao" + }, + { + "value": "CY", + "label": "Cyprus" + }, + { + "value": "CZ", + "label": "Czech Republic" + }, + { + "value": "DK", + "label": "Denmark" + }, + { + "value": "DG", + "label": "Diego Garcia" + }, + { + "value": "DJ", + "label": "Djibouti" + }, + { + "value": "DM", + "label": "Dominica" + }, + { + "value": "DO", + "label": "Dominican Republic" + }, + { + "value": "EC", + "label": "Ecuador" + }, + { + "value": "EG", + "label": "Egypt" + }, + { + "value": "SV", + "label": "El Salvador" + }, + { + "value": "GQ", + "label": "Equatorial Guinea" + }, + { + "value": "ER", + "label": "Eritrea" + }, + { + "value": "EE", + "label": "Estonia" + }, + { + "value": "ET", + "label": "Ethiopia" + }, + { + "value": "FK", + "label": "Falkland Islands (Malvinas)" + }, + { + "value": "FO", + "label": "Faroe Islands" + }, + { + "value": "FJ", + "label": "Fiji" + }, + { + "value": "FI", + "label": "Finland" + }, + { + "value": "FR", + "label": "France" + }, + { + "value": "GF", + "label": "French Guiana" + }, + { + "value": "PF", + "label": "French Polynesia" + }, + { + "value": "TF", + "label": "French Southern Territories" + }, + { + "value": "GA", + "label": "Gabon" + }, + { + "value": "GM", + "label": "Gambia" + }, + { + "value": "GE", + "label": "Georgia" + }, + { + "value": "DE", + "label": "Germany" + }, + { + "value": "GH", + "label": "Ghana" + }, + { + "value": "GI", + "label": "Gibraltar" + }, + { + "value": "GR", + "label": "Greece" + }, + { + "value": "GL", + "label": "Greenland" + }, + { + "value": "GD", + "label": "Grenada" + }, + { + "value": "GP", + "label": "Guadeloupe" + }, + { + "value": "GU", + "label": "Guam" + }, + { + "value": "GT", + "label": "Guatemala" + }, + { + "value": "GG", + "label": "Guernsey" + }, + { + "value": "GN", + "label": "Guinea" + }, + { + "value": "GW", + "label": "Guinea-Bissau" + }, + { + "value": "GY", + "label": "Guyana" + }, + { + "value": "HT", + "label": "Haiti" + }, + { + "value": "HM", + "label": "Heard Island and McDonald Islands" + }, + { + "value": "VA", + "label": "Holy See (Vatican City State)" + }, + { + "value": "HN", + "label": "Honduras" + }, + { + "value": "HK", + "label": "Hong Kong" + }, + { + "value": "HU", + "label": "Hungary" + }, + { + "value": "IS", + "label": "Iceland" + }, + { + "value": "IN", + "label": "India" + }, + { + "value": "ID", + "label": "Indonesia" + }, + { + "value": "IR", + "label": "Iran, Islamic Republic of" + }, + { + "value": "IQ", + "label": "Iraq" + }, + { + "value": "IE", + "label": "Ireland" + }, + { + "value": "IM", + "label": "Isle of Man" + }, + { + "value": "IL", + "label": "Israel" + }, + { + "value": "IT", + "label": "Italy" + }, + { + "value": "JM", + "label": "Jamaica" + }, + { + "value": "JP", + "label": "Japan" + }, + { + "value": "JE", + "label": "Jersey" + }, + { + "value": "JO", + "label": "Jordan" + }, + { + "value": "KZ", + "label": "Kazakhstan" + }, + { + "value": "KE", + "label": "Kenya" + }, + { + "value": "KI", + "label": "Kiribati" + }, + { + "value": "KP", + "label": "Korea, Democratic People's Republic of" + }, + { + "value": "KR", + "label": "Korea, Republic of" + }, + { + "value": "XK", + "label": "Kosovo" + }, + { + "value": "KW", + "label": "Kuwait" + }, + { + "value": "KG", + "label": "Kyrgyzstan" + }, + { + "value": "LA", + "label": "Lao People's Democratic Republic" + }, + { + "value": "LV", + "label": "Latvia" + }, + { + "value": "LB", + "label": "Lebanon" + }, + { + "value": "LS", + "label": "Lesotho" + }, + { + "value": "LR", + "label": "Liberia" + }, + { + "value": "LY", + "label": "Libya" + }, + { + "value": "LI", + "label": "Liechtenstein" + }, + { + "value": "LT", + "label": "Lithuania" + }, + { + "value": "LU", + "label": "Luxembourg" + }, + { + "value": "MO", + "label": "Macao" + }, + { + "value": "MK", + "label": "Macedonia, the Former Yugoslav Republic of" + }, + { + "value": "MG", + "label": "Madagascar" + }, + { + "value": "MW", + "label": "Malawi" + }, + { + "value": "MY", + "label": "Malaysia" + }, + { + "value": "MV", + "label": "Maldives" + }, + { + "value": "ML", + "label": "Mali" + }, + { + "value": "MT", + "label": "Malta" + }, + { + "value": "MH", + "label": "Marshall Islands" + }, + { + "value": "MQ", + "label": "Martinique" + }, + { + "value": "MR", + "label": "Mauritania" + }, + { + "value": "MU", + "label": "Mauritius" + }, + { + "value": "YT", + "label": "Mayotte" + }, + { + "value": "MX", + "label": "Mexico" + }, + { + "value": "FM", + "label": "Micronesia, Federated States of" + }, + { + "value": "MD", + "label": "Moldova, Republic of" + }, + { + "value": "MC", + "label": "Monaco" + }, + { + "value": "MN", + "label": "Mongolia" + }, + { + "value": "ME", + "label": "Montenegro" + }, + { + "value": "MS", + "label": "Montserrat" + }, + { + "value": "MA", + "label": "Morocco" + }, + { + "value": "MZ", + "label": "Mozambique" + }, + { + "value": "MM", + "label": "Myanmar" + }, + { + "value": "NA", + "label": "Namibia" + }, + { + "value": "NR", + "label": "Nauru" + }, + { + "value": "NP", + "label": "Nepal" + }, + { + "value": "NL", + "label": "Netherlands" + }, + { + "value": "NC", + "label": "New Caledonia" + }, + { + "value": "NZ", + "label": "New Zealand" + }, + { + "value": "NI", + "label": "Nicaragua" + }, + { + "value": "NE", + "label": "Niger" + }, + { + "value": "NG", + "label": "Nigeria" + }, + { + "value": "NU", + "label": "Niue" + }, + { + "value": "NF", + "label": "Norfolk Island" + }, + { + "value": "MP", + "label": "Northern Mariana Islands" + }, + { + "value": "NO", + "label": "Norway" + }, + { + "value": "OM", + "label": "Oman" + }, + { + "value": "PK", + "label": "Pakistan" + }, + { + "value": "PW", + "label": "Palau" + }, + { + "value": "PS", + "label": "Palestine, State of" + }, + { + "value": "PA", + "label": "Panama" + }, + { + "value": "PG", + "label": "Papua New Guinea" + }, + { + "value": "PY", + "label": "Paraguay" + }, + { + "value": "PE", + "label": "Peru" + }, + { + "value": "PH", + "label": "Philippines" + }, + { + "value": "PN", + "label": "Pitcairn" + }, + { + "value": "PL", + "label": "Poland" + }, + { + "value": "PT", + "label": "Portugal" + }, + { + "value": "PR", + "label": "Puerto Rico" + }, + { + "value": "QA", + "label": "Qatar" + }, + { + "value": "RE", + "label": "R\u00e9union" + }, + { + "value": "RO", + "label": "Romania" + }, + { + "value": "RU", + "label": "Russian Federation" + }, + { + "value": "RW", + "label": "Rwanda" + }, + { + "value": "BL", + "label": "Saint Barth\u00e9lemy" + }, + { + "value": "SH", + "label": "Saint Helena, Ascension and Tristan da Cunha" + }, + { + "value": "KN", + "label": "Saint Kitts and Nevis" + }, + { + "value": "LC", + "label": "Saint Lucia" + }, + { + "value": "MF", + "label": "Saint Martin (French part)" + }, + { + "value": "PM", + "label": "Saint Pierre and Miquelon" + }, + { + "value": "VC", + "label": "Saint Vincent and the Grenadines" + }, + { + "value": "WS", + "label": "Samoa" + }, + { + "value": "SM", + "label": "San Marino" + }, + { + "value": "ST", + "label": "Sao Tome and Principe" + }, + { + "value": "SA", + "label": "Saudi Arabia" + }, + { + "value": "SN", + "label": "Senegal" + }, + { + "value": "RS", + "label": "Serbia" + }, + { + "value": "SC", + "label": "Seychelles" + }, + { + "value": "SL", + "label": "Sierra Leone" + }, + { + "value": "SG", + "label": "Singapore" + }, + { + "value": "SX", + "label": "Sint Maarten (Dutch part)" + }, + { + "value": "SK", + "label": "Slovakia" + }, + { + "value": "SI", + "label": "Slovenia" + }, + { + "value": "SB", + "label": "Solomon Islands" + }, + { + "value": "SO", + "label": "Somalia" + }, + { + "value": "ZA", + "label": "South Africa" + }, + { + "value": "GS", + "label": "South Georgia and the South Sandwich Islands" + }, + { + "value": "SS", + "label": "South Sudan" + }, + { + "value": "ES", + "label": "Spain" + }, + { + "value": "LK", + "label": "Sri Lanka" + }, + { + "value": "SD", + "label": "Sudan" + }, + { + "value": "SR", + "label": "Suriname" + }, + { + "value": "SJ", + "label": "Svalbard and Jan Mayen" + }, + { + "value": "SZ", + "label": "Swaziland" + }, + { + "value": "SE", + "label": "Sweden" + }, + { + "value": "CH", + "label": "Switzerland" + }, + { + "value": "SY", + "label": "Syrian Arab Republic" + }, + { + "value": "TW", + "label": "Taiwan, Province of China" + }, + { + "value": "TJ", + "label": "Tajikistan" + }, + { + "value": "TZ", + "label": "Tanzania, United Republic of" + }, + { + "value": "TH", + "label": "Thailand" + }, + { + "value": "TL", + "label": "Timor-Leste" + }, + { + "value": "TG", + "label": "Togo" + }, + { + "value": "TK", + "label": "Tokelau" + }, + { + "value": "TO", + "label": "Tonga" + }, + { + "value": "TT", + "label": "Trinidad and Tobago" + }, + { + "value": "TN", + "label": "Tunisia" + }, + { + "value": "TR", + "label": "Turkey" + }, + { + "value": "TM", + "label": "Turkmenistan" + }, + { + "value": "TC", + "label": "Turks and Caicos Islands" + }, + { + "value": "TV", + "label": "Tuvalu" + }, + { + "value": "UG", + "label": "Uganda" + }, + { + "value": "UA", + "label": "Ukraine" + }, + { + "value": "AE", + "label": "United Arab Emirates" + }, + { + "value": "GB", + "label": "United Kingdom" + }, + { + "value": "US", + "label": "United States" + }, + { + "value": "UM", + "label": "United States Minor Outlying Islands" + }, + { + "value": "UY", + "label": "Uruguay" + }, + { + "value": "UZ", + "label": "Uzbekistan" + }, + { + "value": "VU", + "label": "Vanuatu" + }, + { + "value": "VE", + "label": "Venezuela, Bolivarian Republic of" + }, + { + "value": "VN", + "label": "Viet Nam" + }, + { + "value": "VG", + "label": "Virgin Islands, British" + }, + { + "value": "VI", + "label": "Virgin Islands, U.S." + }, + { + "value": "WF", + "label": "Wallis and Futuna" + }, + { + "value": "EH", + "label": "Western Sahara" + }, + { + "value": "YE", + "label": "Yemen" + }, + { + "value": "ZM", + "label": "Zambia" + }, + { + "value": "ZW", + "label": "Zimbabwe" + } ] -} +} \ No newline at end of file diff --git a/src/data/M365Licenses.json b/src/data/M365Licenses.json index b493afd304db..098fdfb04710 100644 --- a/src/data/M365Licenses.json +++ b/src/data/M365Licenses.json @@ -1927,6 +1927,30 @@ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318", "Service_Plans_Included_Friendly_Names": "Exchange Foundation" }, + { + "Product_Display_Name": "Dynamics 365 Field Service Contractor", + "String_Id": "D365_FIELD_SERVICE_CONTRACTOR", + "GUID": "23e6e135-e869-4ce4-9ae4-5710cd69ac13", + "Service_Plan_Name": "CDS_FIELD_SERVICE_CONTRACTOR", + "Service_Plan_Id": "f4614a66-d632-443a-bc77-afe92987b322", + "Service_Plans_Included_Friendly_Names": "Common Data Service Field service Part Time Contractors" + }, + { + "Product_Display_Name": "Dynamics 365 Field Service Contractor", + "String_Id": "D365_FIELD_SERVICE_CONTRACTOR", + "GUID": "23e6e135-e869-4ce4-9ae4-5710cd69ac13", + "Service_Plan_Name": "EXCHANGE_S_FOUNDATION", + "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318", + "Service_Plans_Included_Friendly_Names": "Exchange Foundation" + }, + { + "Product_Display_Name": "Dynamics 365 Field Service Contractor", + "String_Id": "D365_FIELD_SERVICE_CONTRACTOR", + "GUID": "23e6e135-e869-4ce4-9ae4-5710cd69ac13", + "Service_Plan_Name": "POWERAPPS_DYN_APPS", + "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b", + "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365" + }, { "Product_Display_Name": "Dynamics 365 Field Service Contractor for Government", "String_Id": "D365_FIELD_SERVICE_CONTRACTOR_GOV", @@ -17975,6 +17999,702 @@ "Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014", "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365" }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MESH_AVATARS_FOR_TEAMS", + "Service_Plan_Id": "dcf9d2f4-772e-4434-b757-77a453cfbc02", + "Service_Plans_Included_Friendly_Names": "Avatars for Teams" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MESH_AVATARS_ADDITIONAL_FOR_TEAMS", + "Service_Plan_Id": "3efbd4ed-8958-4824-8389-1321f8730af8", + "Service_Plans_Included_Friendly_Names": "Avatars for Teams (additional)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "CDS_O365_P3", + "Service_Plan_Id": "afa73018-811e-46e9-988f-f75d2b1b8430", + "Service_Plans_Included_Friendly_Names": "Common Data Service for Teams" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "LOCKBOX_ENTERPRISE", + "Service_Plan_Id": "9f431833-0334-42de-a7dc-70aa40db46db", + "Service_Plans_Included_Friendly_Names": "Customer Lockbox" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "CustomerLockboxA_Enterprise", + "Service_Plan_Id": "3ec18638-bd4c-4d3b-8905-479ed636b83e", + "Service_Plans_Included_Friendly_Names": "Customer Lockbox (A)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MIP_S_Exchange", + "Service_Plan_Id": "cd31b152-6326-4d1b-ae1b-997b625182e6", + "Service_Plans_Included_Friendly_Names": "Data Classification in Microsoft 365" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "EXCHANGE_S_ENTERPRISE", + "Service_Plan_Id": "efb87545-963c-4e0d-99df-69c6916d9eb0", + "Service_Plans_Included_Friendly_Names": "Exchange Online (Plan 2)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "GRAPH_CONNECTORS_SEARCH_INDEX", + "Service_Plan_Id": "a6520331-d7d4-4276-95f5-15c0933bc757", + "Service_Plans_Included_Friendly_Names": "Graph Connectors Search with Index" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MESH_IMMERSIVE_FOR_TEAMS", + "Service_Plan_Id": "f0ff6ac6-297d-49cd-be34-6dfef97f0c28", + "Service_Plans_Included_Friendly_Names": "Immersive spaces for Teams" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "INFORMATION_BARRIERS", + "Service_Plan_Id": "c4801e8a-cb58-4c35-aca6-f2dcc106f287", + "Service_Plans_Included_Friendly_Names": "Information Barriers" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "Content_Explorer", + "Service_Plan_Id": "d9fa6af4-e046-4c89-9226-729a0786685d", + "Service_Plans_Included_Friendly_Names": "Information Protection and Governance Analytics - Premium" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "ContentExplorer_Standard", + "Service_Plan_Id": "2b815d45-56e4-4e3a-b65c-66cb9175b560", + "Service_Plans_Included_Friendly_Names": "Information Protection and Governance Analytics – Standard" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MIP_S_CLP2", + "Service_Plan_Id": "efb0351d-3b08-4503-993d-383af8de41e3", + "Service_Plans_Included_Friendly_Names": "Information Protection for Office 365 - Premium" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MIP_S_CLP1", + "Service_Plan_Id": "5136a095-5cf0-4aff-bec3-e84448b38ea5", + "Service_Plans_Included_Friendly_Names": "Information Protection for Office 365 - Standard" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MYANALYTICS_P2", + "Service_Plan_Id": "33c4f319-9bdd-48d6-9c4d-410b750a4a5a", + "Service_Plans_Included_Friendly_Names": "Insights by MyAnalytics" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "M365_ADVANCED_AUDITING", + "Service_Plan_Id": "2f442157-a11c-46b9-ae5b-6e39ff4e5849", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Advanced Auditing" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "OFFICESUBSCRIPTION", + "Service_Plan_Id": "43de0ff5-c92c-492b-9116-175376d08c38", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for enterprise" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MCOMEETADV", + "Service_Plan_Id": "3e26ee1f-8a5f-4d52-aee2-b81ce45c8f40", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audio Conferencing" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "M365_AUDIT_PLATFORM", + "Service_Plan_Id": "f6de4823-28fa-440b-b886-4783fa86ddba", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audit Platform" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MICROSOFT_COMMUNICATION_COMPLIANCE", + "Service_Plan_Id": "a413a9ff-720c-4822-98ef-2f37c2a21f4c", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Communication Compliance" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MTP", + "Service_Plan_Id": "bf28f719-7844-4079-9c78-c1307898e192", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Defender" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1", + "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MCOEV", + "Service_Plan_Id": "4828c8ec-dc2e-4779-b502-87ac9ce28ab7", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Phone System" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MICROSOFTBOOKINGS", + "Service_Plan_Id": "199a5c09-e0ca-4e37-8f7c-b05d533e1ea2", + "Service_Plans_Included_Friendly_Names": "Microsoft Bookings" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "CLIPCHAMP", + "Service_Plan_Id": "a1ace008-72f3-4ea0-8dac-33b3a23a2472", + "Service_Plans_Included_Friendly_Names": "Microsoft Clipchamp" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "COMMUNICATIONS_DLP", + "Service_Plan_Id": "6dc145d6-95dd-4191-b9c3-185575ee6f6b", + "Service_Plans_Included_Friendly_Names": "Microsoft Communications DLP" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "CUSTOMER_KEY", + "Service_Plan_Id": "6db1f1db-2b46-403f-be40-e39395f08dbb", + "Service_Plans_Included_Friendly_Names": "Microsoft Customer Key" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "ATP_ENTERPRISE", + "Service_Plan_Id": "f20fedf3-f3c3-43c3-8267-2bfdd51c0939", + "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Office 365 (Plan 1)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "THREAT_INTELLIGENCE", + "Service_Plan_Id": "8e0c0a52-6a6c-4d40-8370-dd62790dcd70", + "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Office 365 (Plan 2)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "EXCEL_PREMIUM", + "Service_Plan_Id": "531ee2f8-b1cb-453b-9c21-d2180d014ca5", + "Service_Plans_Included_Friendly_Names": "Microsoft Excel Advanced Analytics" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "FORMS_PLAN_E5", + "Service_Plan_Id": "e212cbc7-0961-4c40-9825-01117710dcb1", + "Service_Plans_Included_Friendly_Names": "Microsoft Forms (Plan E5)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "INFO_GOVERNANCE", + "Service_Plan_Id": "e26c2fcc-ab91-4a61-b35c-03cdc8dddf66", + "Service_Plans_Included_Friendly_Names": "Microsoft Information Governance" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "INSIDER_RISK", + "Service_Plan_Id": "d587c7a3-bda9-4f99-8776-9bcf59c84f75", + "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT", + "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34", + "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management - Exchange" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "KAIZALA_STANDALONE", + "Service_Plan_Id": "0898bdbb-73b0-471a-81e5-20f1fe4dd66e", + "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MICROSOFT_LOOP", + "Service_Plan_Id": "c4b8c31a-fb44-4c65-9837-a21f55fcabda", + "Service_Plans_Included_Friendly_Names": "Microsoft Loop" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "ML_CLASSIFICATION", + "Service_Plan_Id": "d2d51368-76c9-4317-ada2-a12c004c432f", + "Service_Plans_Included_Friendly_Names": "Microsoft ML-Based Classification" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "EXCHANGE_ANALYTICS", + "Service_Plan_Id": "34c0d7a0-a70f-4668-9238-47f9fc208882", + "Service_Plans_Included_Friendly_Names": "Microsoft MyAnalytics (Full)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "PROJECTWORKMANAGEMENT", + "Service_Plan_Id": "b737dad2-2f6c-4c65-90e3-ca563267e8b9", + "Service_Plans_Included_Friendly_Names": "Microsoft Planner" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "RECORDS_MANAGEMENT", + "Service_Plan_Id": "65cc641f-cccd-4643-97e0-a17e3045e541", + "Service_Plans_Included_Friendly_Names": "Microsoft Records Management" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MICROSOFT_SEARCH", + "Service_Plan_Id": "94065c59-bc8e-4e8b-89e5-5138d471eaff", + "Service_Plans_Included_Friendly_Names": "Microsoft Search" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "Deskless", + "Service_Plan_Id": "8c7d2df8-86f0-4902-b2ed-a0458298f3b3", + "Service_Plans_Included_Friendly_Names": "Microsoft StaffHub" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "STREAM_O365_E5", + "Service_Plan_Id": "6c6042f5-6f01-4d67-b8c1-eb99d36eed3e", + "Service_Plans_Included_Friendly_Names": "Microsoft Stream for Office 365 E5" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "INTUNE_O365", + "Service_Plan_Id": "882e1d05-acd1-4ccb-8708-6ee03664b117", + "Service_Plans_Included_Friendly_Names": "Mobile Device Management for Office 365" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "Nucleus", + "Service_Plan_Id": "db4d623d-b514-490b-b7ef-8885eee514de", + "Service_Plans_Included_Friendly_Names": "Nucleus" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "EQUIVIO_ANALYTICS", + "Service_Plan_Id": "4de31727-a228-4ec3-a5bf-8e45b5ca48cc", + "Service_Plans_Included_Friendly_Names": "Office 365 Advanced eDiscovery" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "ADALLOM_S_O365", + "Service_Plan_Id": "8c098270-9dd4-4350-9b30-ba4703f3b36b", + "Service_Plans_Included_Friendly_Names": "Office 365 Cloud App Security" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "PAM_ENTERPRISE", + "Service_Plan_Id": "b1188c4c-1b36-4018-b48b-ee07604f6feb", + "Service_Plans_Included_Friendly_Names": "Office 365 Privileged Access Management" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "SAFEDOCS", + "Service_Plan_Id": "bf6f5520-59e3-4f82-974b-7dbbc4fd27c7", + "Service_Plans_Included_Friendly_Names": "Office 365 SafeDocs" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "SHAREPOINTWAC", + "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014", + "Service_Plans_Included_Friendly_Names": "Office for the Web" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "PEOPLE_SKILLS_FOUNDATION", + "Service_Plan_Id": "13b6da2c-0d84-450e-9f69-a33e221387ca", + "Service_Plans_Included_Friendly_Names": "People Skills - Foundation" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "POWERAPPS_O365_P3", + "Service_Plan_Id": "9c0dab89-a30c-4117-86e7-97bda240acd2", + "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365 (Plan 3)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "BI_AZURE_P2", + "Service_Plan_Id": "70d33638-9c74-4d01-bfd3-562de28bd4ba", + "Service_Plans_Included_Friendly_Names": "Power BI Pro" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "PREMIUM_ENCRYPTION", + "Service_Plan_Id": "617b097b-4b93-4ede-83de-5f075bb5fb2f", + "Service_Plans_Included_Friendly_Names": "Premium Encryption in Office 365" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "PROJECT_O365_P3", + "Service_Plan_Id": "b21a6b06-1988-436e-a07b-51ec6d9f52ad", + "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E5)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "PURVIEW_DISCOVERY", + "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522", + "Service_Plans_Included_Friendly_Names": "Purview Discovery" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "Bing_Chat_Enterprise", + "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba", + "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "COMMUNICATIONS_COMPLIANCE", + "Service_Plan_Id": "41fcdd7d-4733-4863-9cf4-c65b83ce2df4", + "Service_Plans_Included_Friendly_Names": "RETIRED - Microsoft Communications Compliance" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "DATA_INVESTIGATIONS", + "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9", + "Service_Plans_Included_Friendly_Names": "Retired - Microsoft Data Investigations" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "PLACES_CORE", + "Service_Plan_Id": "1fe6227d-3e01-46d0-9510-0acad4ff6e94", + "Service_Plans_Included_Friendly_Names": "RETIRED - Places Core" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "SHAREPOINTENTERPRISE", + "Service_Plan_Id": "5dbe027f-2339-4123-9542-606e4d348a72", + "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MCOSTANDARD", + "Service_Plan_Id": "0feaeb32-d00e-4d66-bd5a-43b5b83db82c", + "Service_Plans_Included_Friendly_Names": "Skype for Business Online (Plan 2)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "SWAY", + "Service_Plan_Id": "a23b959c-7ce8-4e57-9140-b90eb88a9e97", + "Service_Plans_Included_Friendly_Names": "Sway" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "BPOS_S_TODO_3", + "Service_Plan_Id": "3fb82609-8c27-4f7b-bd51-30634711ee67", + "Service_Plans_Included_Friendly_Names": "To-Do (Plan 3)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "VIVAENGAGE_CORE", + "Service_Plan_Id": "a82fbf69-b4d7-49f4-83a6-915b2cf354f4", + "Service_Plans_Included_Friendly_Names": "Viva Engage Core" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "VIVA_LEARNING_SEEDED", + "Service_Plan_Id": "b76fb638-6ba6-402a-b9f9-83d28acb3d86", + "Service_Plans_Included_Friendly_Names": "Viva Learning Seeded" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "WHITEBOARD_PLAN3", + "Service_Plan_Id": "4a51bca5-1eff-43f5-878c-177680f191af", + "Service_Plans_Included_Friendly_Names": "Whiteboard (Plan 3)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "YAMMER_ENTERPRISE", + "Service_Plan_Id": "7547a3fe-08ee-4ccb-b430-5077c5041653", + "Service_Plans_Included_Friendly_Names": "Yammer Enterprise" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "WINDEFATP", + "Service_Plan_Id": "871d91ec-ec1a-452b-a83f-bd76c7d770ef", + "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Endpoint" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MICROSOFTENDPOINTDLP", + "Service_Plan_Id": "64bfac92-2b17-4482-b5e5-a0304429de3e", + "Service_Plans_Included_Friendly_Names": "Microsoft Endpoint DLP" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "UNIVERSAL_PRINT_01", + "Service_Plan_Id": "795f6fe0-cc4d-4773-b050-5dde4dc704c9", + "Service_Plans_Included_Friendly_Names": "Universal Print" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "WIN10_PRO_ENT_SUB", + "Service_Plan_Id": "21b439ba-a0ca-424f-a6cc-52f954a5b111", + "Service_Plans_Included_Friendly_Names": "Windows 10/11 Enterprise (Original)" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "Windows_Autopatch", + "Service_Plan_Id": "9a6eeb79-0b4b-4bf0-9808-39d99a2cd5a3", + "Service_Plans_Included_Friendly_Names": "Windows Autopatch" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "WINDOWSUPDATEFORBUSINESS_DEPLOYMENTSERVICE", + "Service_Plan_Id": "7bf960f6-2cd9-443a-8046-5dbff9558365", + "Service_Plans_Included_Friendly_Names": "Windows Update for Business Deployment Service" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "RMS_S_PREMIUM", + "Service_Plan_Id": "6c57d4b6-3b23-47a5-9bc9-69f17b4947b3", + "Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P1" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "RMS_S_PREMIUM2", + "Service_Plan_Id": "5689bec4-755d-4753-8b61-40975025187c", + "Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P2" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "RMS_S_ENTERPRISE", + "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90", + "Service_Plans_Included_Friendly_Names": "Azure Rights Management" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "DYN365_CDS_O365_P3", + "Service_Plan_Id": "28b0fa46-c39a-4188-89e2-58e979a6b014", + "Service_Plans_Included_Friendly_Names": "Common Data Service" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "Defender_for_Iot_Enterprise", + "Service_Plan_Id": "99cd49a9-0e54-4e07-aea1-d8d9f5f704f5", + "Service_Plans_Included_Friendly_Names": "Defender for IoT - Enterprise IoT Security" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "MFA_PREMIUM", + "Service_Plan_Id": "8a256a2b-b617-496d-b51b-e76466e88db0", + "Service_Plans_Included_Friendly_Names": "Microsoft Azure Multi-Factor Authentication" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "ADALLOM_S_STANDALONE", + "Service_Plan_Id": "2e2ddb96-6af9-4b1d-a3f0-d6ecfd22edb2", + "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Cloud Apps" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "ATA", + "Service_Plan_Id": "14ab5db5-e6c4-4b20-b4bc-13e36fd2227f", + "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Identity" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "AAD_PREMIUM", + "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d", + "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "AAD_PREMIUM_P2", + "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998", + "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "INTUNE_A", + "Service_Plan_Id": "c1ec4a95-1f05-45b3-a911-aa3fa01094f5", + "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "FLOW_O365_P3", + "Service_Plan_Id": "07699545-9485-468e-95b6-2fca3738be01", + "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365" + }, + { + "Product_Display_Name": "Microsoft 365 E5 (no Teams)", + "String_Id": "Microsoft_365_E5_(no_Teams)", + "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e", + "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_O365_P3", + "Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014", + "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365" + }, { "Product_Display_Name": "Microsoft 365 E5 EEA (no Teams) with Calling Minutes", "String_Id": "Microsoft_365_E5_EEA_(no_Teams)_with_Calling_Minutes", @@ -41023,6 +41743,46 @@ "Service_Plan_Id": "dc789ed8-0170-4b65-a415-eb77d5bb350a", "Service_Plans_Included_Friendly_Names": "Power Automate for Power Apps per User Plan" }, + { + "Product_Display_Name": "Power Apps Premium embedded", + "String_Id": "POWERAPPS_PER_USER_ISVEMB", + "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba", + "Service_Plan_Name": "Power_Pages_Internal_User", + "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941", + "Service_Plans_Included_Friendly_Names": "Power Pages Internal User" + }, + { + "Product_Display_Name": "Power Apps Premium embedded", + "String_Id": "POWERAPPS_PER_USER_ISVEMB", + "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba", + "Service_Plan_Name": "EXCHANGE_S_FOUNDATION", + "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318", + "Service_Plans_Included_Friendly_Names": "Exchange Foundation" + }, + { + "Product_Display_Name": "Power Apps Premium embedded", + "String_Id": "POWERAPPS_PER_USER_ISVEMB", + "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba", + "Service_Plan_Name": "DYN365_CDS_P2", + "Service_Plan_Id": "6ea4c1ef-c259-46df-bce2-943342cd3cb2", + "Service_Plans_Included_Friendly_Names": "Common Data Service" + }, + { + "Product_Display_Name": "Power Apps Premium embedded", + "String_Id": "POWERAPPS_PER_USER_ISVEMB", + "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba", + "Service_Plan_Name": "POWERAPPS_PER_USER", + "Service_Plan_Id": "ea2cf03b-ac60-46ae-9c1d-eeaeb63cec86", + "Service_Plans_Included_Friendly_Names": "Power Apps per User Plan" + }, + { + "Product_Display_Name": "Power Apps Premium embedded", + "String_Id": "POWERAPPS_PER_USER_ISVEMB", + "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba", + "Service_Plan_Name": "Flow_PowerApps_PerUser", + "Service_Plan_Id": "dc789ed8-0170-4b65-a415-eb77d5bb350a", + "Service_Plans_Included_Friendly_Names": "Power Automate for Power Apps per User Plan" + }, { "Product_Display_Name": "Power Apps Premium for Government", "String_Id": "POWERAPPS_PER_USER_GCC", diff --git a/src/data/alerts.json b/src/data/alerts.json index 9de9c369f85b..aa5936ca4578 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -9,6 +9,11 @@ "label": "Alert on admins without any form of MFA", "recommendedRunInterval": "1d" }, + { + "name": "LicenseAssignmentErrors", + "label": "Alert on license assignment errors", + "recommendedRunInterval": "1d" + }, { "name": "NoCAConfig", "label": "Alert on tenants without a Conditional Access policy, while having Conditional Access licensing available.", diff --git a/src/data/standards.json b/src/data/standards.json index aee94c9ffbb4..23505da2cd6a 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2803,6 +2803,7 @@ "cat": "Defender Standards", "tag": [], "helpText": "This standard creates a Spam filter policy similar to the default strict policy.", + "docsDescription": "This standard creates a Spam filter policy similar to the default strict policy, the following settings are configured to on by default: IncreaseScoreWithNumericIps, IncreaseScoreWithRedirectToOtherPort, MarkAsSpamEmptyMessages, MarkAsSpamJavaScriptInHtml, MarkAsSpamSpfRecordHardFail, MarkAsSpamFromAddressAuthFail, MarkAsSpamNdrBackscatter, MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled", "addedComponent": [ { "type": "number", @@ -3535,6 +3536,33 @@ "powershellEquivalent": "Update-MgBetaAdminSharePointSetting", "recommendedBy": [] }, + { + "name": "standards.SPFileRequests", + "cat": "SharePoint Standards", + "tag": [], + "helpText": "Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.", + "docsDescription": "File Requests allow users to create secure upload-only share links where uploads are hidden from other people using the link. This creates a secure and private way for people to upload files to a folder. This feature is not enabled by default on new tenants and requires PowerShell configuration. This standard enables or disables this feature and optionally configures link expiration settings for both SharePoint and OneDrive.", + "executiveText": "Enables secure file upload functionality that allows external users to submit files directly to company folders without seeing other submissions or folder contents. This provides a professional and secure way to collect documents from clients, vendors, and partners while maintaining data privacy and security.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.SPFileRequests.state", + "label": "Enable File Requests" + }, + { + "type": "number", + "name": "standards.SPFileRequests.expirationDays", + "label": "Link Expiration 1-730 Days (Optional)", + "required": false + } + ], + "label": "Set SharePoint and OneDrive File Requests", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-07-30", + "powershellEquivalent": "Set-SPOTenant -CoreRequestFilesLinkEnabled $true -OneDriveRequestFilesLinkEnabled $true -CoreRequestFilesLinkExpirationInDays 30 -OneDriveRequestFilesLinkExpirationInDays 30", + "recommendedBy": ["CIPP"] + }, { "name": "standards.TenantDefaultTimezone", "cat": "SharePoint Standards", diff --git a/src/hooks/use-guid-resolver.js b/src/hooks/use-guid-resolver.js new file mode 100644 index 000000000000..1325722fc872 --- /dev/null +++ b/src/hooks/use-guid-resolver.js @@ -0,0 +1,523 @@ +import { useState, useCallback, useRef, useEffect } from "react"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { useSettings } from "/src/hooks/use-settings"; + +// Function to check if a string is a GUID +const isGuid = (str) => { + if (typeof str !== "string") return false; + const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return guidRegex.test(str); +}; + +// Function to extract GUIDs from strings (including embedded GUIDs) +const extractGuidsFromString = (str) => { + if (typeof str !== "string") return []; + const guidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi; + return str.match(guidRegex) || []; +}; + +// Function to extract object IDs from partner tenant UPNs (user_@.onmicrosoft.com) +// Also handles format: TenantName.onmicrosoft.com\tenant: , object: +const extractObjectIdFromPartnerUPN = (str) => { + if (typeof str !== "string") return []; + const matches = []; + + // Format 1: user_@.onmicrosoft.com + const partnerUpnRegex = /user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)/gi; + let match; + + while ((match = partnerUpnRegex.exec(str)) !== null) { + // Convert the 32-character hex string to GUID format + const hexId = match[1]; + const tenantDomain = match[2]; + if (hexId.length === 32) { + const guid = [ + hexId.slice(0, 8), + hexId.slice(8, 12), + hexId.slice(12, 16), + hexId.slice(16, 20), + hexId.slice(20, 32), + ].join("-"); + matches.push({ guid, tenantDomain }); + } + } + + // Format 2: TenantName.onmicrosoft.com\tenant: , object: + // For exchange format, use the partner tenant guid for resolution + const partnerTenantObjectRegex = + /([^\\]+\.onmicrosoft\.com)\\tenant:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}),\s*object:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi; + + while ((match = partnerTenantObjectRegex.exec(str)) !== null) { + const customerTenantDomain = match[1]; // This is the customer tenant domain + const partnerTenantGuid = match[2]; // This is the partner tenant guid - use this for resolution + const objectGuid = match[3]; // This is the object to resolve + + // Use the partner tenant GUID for resolution + matches.push({ guid: objectGuid, tenantDomain: partnerTenantGuid }); + } + + return matches; +}; + +// Function to recursively scan an object for GUIDs +const findGuids = (obj, guidsSet = new Set(), partnerGuidsMap = new Map()) => { + if (!obj) return { guidsSet, partnerGuidsMap }; + + if (typeof obj === "string") { + // First, extract object IDs from partner tenant UPNs to track which GUIDs belong to partners + const partnerObjectIds = extractObjectIdFromPartnerUPN(obj); + const partnerGuids = new Set(); + + partnerObjectIds.forEach(({ guid, tenantDomain }) => { + if (!partnerGuidsMap.has(tenantDomain)) { + partnerGuidsMap.set(tenantDomain, new Set()); + } + partnerGuidsMap.get(tenantDomain).add(guid); + partnerGuids.add(guid); // Track this GUID as belonging to a partner + }); + + // Check if the entire string is a GUID + if (isGuid(obj)) { + // Only add to main guidsSet if it's not a partner GUID + if (!partnerGuids.has(obj)) { + guidsSet.add(obj); + } + } else { + // Extract GUIDs embedded within longer strings + const embeddedGuids = extractGuidsFromString(obj); + embeddedGuids.forEach((guid) => { + // Only add to main guidsSet if it's not a partner GUID + if (!partnerGuids.has(guid)) { + guidsSet.add(guid); + } + }); + } + } else if (Array.isArray(obj)) { + obj.forEach((item) => { + const result = findGuids(item, guidsSet, partnerGuidsMap); + guidsSet = result.guidsSet; + partnerGuidsMap = result.partnerGuidsMap; + }); + } else if (typeof obj === "object") { + Object.values(obj).forEach((value) => { + const result = findGuids(value, guidsSet, partnerGuidsMap); + guidsSet = result.guidsSet; + partnerGuidsMap = result.partnerGuidsMap; + }); + } + + return { guidsSet, partnerGuidsMap }; +}; + +// Helper function to replace GUIDs and special UPNs in a string with resolved names +const replaceGuidsAndUpnsInString = (str, guidMapping, upnMapping, isLoadingGuids) => { + if (typeof str !== "string") return { result: str, hasResolvedNames: false }; + + let result = str; + let hasResolvedNames = false; + + // Replace standard GUIDs + const guidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi; + const guidsInString = str.match(guidRegex) || []; + + guidsInString.forEach((guid) => { + if (guidMapping[guid]) { + result = result.replace(new RegExp(guid, "gi"), guidMapping[guid]); + hasResolvedNames = true; + } + }); + + // Replace partner UPNs (user_@partnertenant.onmicrosoft.com) + const partnerUpnRegex = /user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)/gi; + let match; + + // We need to clone the string to reset the regex lastIndex + const strForMatching = String(str); + + while ((match = partnerUpnRegex.exec(strForMatching)) !== null) { + const fullMatch = match[0]; // The complete UPN + const hexId = match[1]; + + if (hexId.length === 32) { + const guid = [ + hexId.slice(0, 8), + hexId.slice(8, 12), + hexId.slice(12, 16), + hexId.slice(16, 20), + hexId.slice(20, 32), + ].join("-"); + + // For partner UPN format, use the actual UPN if available, otherwise fall back to display name + if (upnMapping[guid]) { + result = result.replace( + new RegExp(fullMatch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), + upnMapping[guid] + ); + hasResolvedNames = true; + } else if (guidMapping[guid]) { + result = result.replace( + new RegExp(fullMatch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), + guidMapping[guid] + ); + hasResolvedNames = true; + } + } + } + + return { result, hasResolvedNames }; +}; + +export const useGuidResolver = (manualTenant = null) => { + const tenantFilter = useSettings().currentTenant; + const activeTenant = manualTenant || tenantFilter; + + // GUID resolution state + const [guidMapping, setGuidMapping] = useState({}); + const [upnMapping, setUpnMapping] = useState({}); // New mapping specifically for UPNs + const [isLoadingGuids, setIsLoadingGuids] = useState(false); + + // Use refs for values that shouldn't trigger re-renders but need to persist + const notFoundGuidsRef = useRef(new Set()); + const pendingGuidsRef = useRef([]); + const pendingPartnerGuidsRef = useRef(new Map()); // Map of tenantDomain -> Set of GUIDs + const lastRequestTimeRef = useRef(0); + const lastPartnerRequestTimeRef = useRef(0); // Separate timing for partner tenant calls + const rateLimitBackoffRef = useRef(2000); // Default backoff time in milliseconds + const rateLimitTimeoutRef = useRef(null); // For tracking retry timeouts + + // Helper function to retry API call with the correct backoff + const retryApiCallWithBackoff = useCallback((apiCall, url, data, retryDelay = null) => { + // Clear any existing timeout + if (rateLimitTimeoutRef.current) { + clearTimeout(rateLimitTimeoutRef.current); + } + + // Use specified delay or current backoff time + const delay = retryDelay || rateLimitBackoffRef.current; + + // Set timeout to retry + rateLimitTimeoutRef.current = setTimeout(() => { + apiCall.mutate({ url, data }); + rateLimitTimeoutRef.current = null; + }, delay); + + // Increase backoff for future retries (up to a reasonable limit) + rateLimitBackoffRef.current = Math.min(rateLimitBackoffRef.current * 1.5, 10000); + }, []); + + // Setup API call for directory objects resolution + const directoryObjectsMutation = ApiPostCall({ + relatedQueryKeys: ["directoryObjects"], + onResult: (data) => { + // Handle rate limit error + if (data && data.statusCode === 429) { + console.log("Rate limit hit on directory objects lookup, retrying..."); + + // Extract retry time from message if available + let retryAfterSeconds = 2; + if (data.message && typeof data.message === "string") { + const match = data.message.match(/Try again in (\d+) seconds/i); + if (match && match[1]) { + retryAfterSeconds = parseInt(match[1], 10) || 2; + } + } + + // Retry with the specified delay (convert to milliseconds) + retryApiCallWithBackoff( + directoryObjectsMutation, + "/api/ListDirectoryObjects", + { + tenantFilter: activeTenant, + ids: pendingGuidsRef.current, + $select: "id,displayName,userPrincipalName,mail", + }, + retryAfterSeconds * 1000 + ); + return; + } + + // Reset backoff time on successful request + rateLimitBackoffRef.current = 2000; + + if (data && Array.isArray(data.value)) { + const newDisplayMapping = {}; + const newUpnMapping = {}; + + // Process the returned results + data.value.forEach((item) => { + if (item.id) { + // For display purposes, prefer displayName > userPrincipalName > mail + if (item.displayName || item.userPrincipalName || item.mail) { + newDisplayMapping[item.id] = item.displayName || item.userPrincipalName || item.mail; + } + + // For UPN replacement, specifically store the UPN when available + if (item.userPrincipalName) { + newUpnMapping[item.id] = item.userPrincipalName; + } + } + }); + + // Find GUIDs that were sent but not returned in the response + const processedGuids = new Set(pendingGuidsRef.current); + const returnedGuids = new Set(data.value.map((item) => item.id)); + const notReturned = [...processedGuids].filter((guid) => !returnedGuids.has(guid)); + + // Add unresolved GUIDs to partner tenant fallback lookup + if (notReturned.length > 0) { + console.log( + `${notReturned.length} GUIDs not resolved by primary tenant, trying partner tenant lookup` + ); + + // Add to partner lookup with the current tenant as fallback + if (!pendingPartnerGuidsRef.current.has(activeTenant)) { + pendingPartnerGuidsRef.current.set(activeTenant, new Set()); + } + notReturned.forEach((guid) => { + pendingPartnerGuidsRef.current.get(activeTenant).add(guid); + }); + + // Trigger partner lookup immediately for fallback + const now = Date.now(); + if (!rateLimitTimeoutRef.current && now - lastPartnerRequestTimeRef.current >= 2000) { + lastPartnerRequestTimeRef.current = now; + + // Use partner tenant API for unresolved GUIDs + console.log( + `Sending partner fallback request for ${notReturned.length} GUIDs in tenant ${activeTenant}` + ); + partnerDirectoryObjectsMutation.mutate({ + url: "/api/ListDirectoryObjects", + data: { + tenantFilter: activeTenant, + ids: notReturned, + $select: "id,displayName,userPrincipalName,mail", + partnerLookup: true, // Flag to indicate this is a partner lookup + }, + }); + } + } + + setGuidMapping((prevMapping) => ({ ...prevMapping, ...newDisplayMapping })); + setUpnMapping((prevMapping) => ({ ...prevMapping, ...newUpnMapping })); + pendingGuidsRef.current = []; + + // Only set loading to false if we don't have pending partner lookups + if (notReturned.length === 0) { + setIsLoadingGuids(false); + } + } + }, + }); + + // Setup API call for partner tenant directory objects resolution + const partnerDirectoryObjectsMutation = ApiPostCall({ + relatedQueryKeys: ["partnerDirectoryObjects"], + onResult: (data) => { + // Handle rate limit error + if (data && data.statusCode === 429) { + console.log("Rate limit hit on partner directory objects lookup, retrying..."); + + // Extract retry time from message if available + let retryAfterSeconds = 2; + if (data.message && typeof data.message === "string") { + const match = data.message.match(/Try again in (\d+) seconds/i); + if (match && match[1]) { + retryAfterSeconds = parseInt(match[1], 10) || 2; + } + } + + // We need to preserve the current tenant domain for retry + const currentTenantEntries = [...pendingPartnerGuidsRef.current.entries()]; + + if (currentTenantEntries.length > 0) { + const [tenantDomain, guidsSet] = currentTenantEntries[0]; + const guidsToRetry = Array.from(guidsSet); + + // Retry with the specified delay (convert to milliseconds) + retryApiCallWithBackoff( + partnerDirectoryObjectsMutation, + "/api/ListDirectoryObjects", + { + tenantFilter: tenantDomain, + ids: guidsToRetry, + $select: "id,displayName,userPrincipalName,mail", + }, + retryAfterSeconds * 1000 + ); + } + return; + } + + // Reset backoff time on successful request + rateLimitBackoffRef.current = 2000; + + if (data && Array.isArray(data.value)) { + const newDisplayMapping = {}; + const newUpnMapping = {}; + + // Process the returned results + data.value.forEach((item) => { + if (item.id) { + // For display purposes, prefer userPrincipalName > mail > DisplayName + if (item.userPrincipalName || item.mail || item.displayName) { + newDisplayMapping[item.id] = item.userPrincipalName || item.mail || item.displayName; + } + + // For UPN replacement, specifically store the UPN when available + if (item.userPrincipalName) { + newUpnMapping[item.id] = item.userPrincipalName; + } + } + }); + + // Find GUIDs that were sent but not returned in the partner lookup + const allPendingPartnerGuids = new Set(); + pendingPartnerGuidsRef.current.forEach((guidsSet) => { + guidsSet.forEach((guid) => allPendingPartnerGuids.add(guid)); + }); + + const returnedGuids = new Set(data.value.map((item) => item.id)); + const stillNotFound = [...allPendingPartnerGuids].filter( + (guid) => !returnedGuids.has(guid) + ); + + // Add truly unresolved GUIDs to notFoundGuids + if (stillNotFound.length > 0) { + stillNotFound.forEach((guid) => notFoundGuidsRef.current.add(guid)); + } + + setGuidMapping((prevMapping) => ({ ...prevMapping, ...newDisplayMapping })); + setUpnMapping((prevMapping) => ({ ...prevMapping, ...newUpnMapping })); + + // Clear processed partner GUIDs + pendingPartnerGuidsRef.current = new Map(); + setIsLoadingGuids(false); + } + }, + }); // Function to handle resolving GUIDs + const resolveGuids = useCallback( + (objectToScan) => { + const { guidsSet, partnerGuidsMap } = findGuids(objectToScan); + + // Handle regular GUIDs (current tenant) - these should NOT include partner tenant GUIDs + if (guidsSet.size > 0) { + const guidsArray = Array.from(guidsSet); + const notResolvedGuids = guidsArray.filter( + (guid) => !guidMapping[guid] && !notFoundGuidsRef.current.has(guid) + ); + + if (notResolvedGuids.length > 0) { + // Merge new GUIDs with existing pending GUIDs without duplicates + const allPendingGuids = [...new Set([...pendingGuidsRef.current, ...notResolvedGuids])]; + pendingGuidsRef.current = allPendingGuids; + setIsLoadingGuids(true); + + // Make API call for primary tenant GUIDs + const now = Date.now(); + if (!rateLimitTimeoutRef.current && now - lastRequestTimeRef.current >= 2000) { + lastRequestTimeRef.current = now; + + // Only send a maximum of 1000 GUIDs per request + const batchSize = 1000; + const guidsToSend = allPendingGuids.slice(0, batchSize); + + if (guidsToSend.length > 0) { + console.log( + `Sending primary tenant request for ${guidsToSend.length} GUIDs in tenant ${activeTenant}` + ); + directoryObjectsMutation.mutate({ + url: "/api/ListDirectoryObjects", + data: { + tenantFilter: activeTenant, + ids: guidsToSend, + $select: "id,displayName,userPrincipalName,mail", + }, + }); + } else { + setIsLoadingGuids(false); + } + } + } + } + + // Handle partner tenant GUIDs separately + if (partnerGuidsMap.size > 0) { + partnerGuidsMap.forEach((guids, tenantDomain) => { + const guidsArray = Array.from(guids); + const notResolvedGuids = guidsArray.filter( + (guid) => !guidMapping[guid] && !notFoundGuidsRef.current.has(guid) + ); + + if (notResolvedGuids.length > 0) { + // Store pending partner GUIDs + if (!pendingPartnerGuidsRef.current.has(tenantDomain)) { + pendingPartnerGuidsRef.current.set(tenantDomain, new Set()); + } + notResolvedGuids.forEach((guid) => + pendingPartnerGuidsRef.current.get(tenantDomain).add(guid) + ); + + setIsLoadingGuids(true); + + // Make API call for partner tenant - with separate timing from primary tenant + const now = Date.now(); + if (!rateLimitTimeoutRef.current && now - lastPartnerRequestTimeRef.current >= 2000) { + lastPartnerRequestTimeRef.current = now; + + // Only send a maximum of 1000 GUIDs per request + const batchSize = 1000; + const guidsToSend = notResolvedGuids.slice(0, batchSize); + + if (guidsToSend.length > 0) { + console.log( + `Sending partner tenant request for ${guidsToSend.length} GUIDs in tenant ${tenantDomain}` + ); + partnerDirectoryObjectsMutation.mutate({ + url: "/api/ListDirectoryObjects", + data: { + tenantFilter: tenantDomain, + ids: guidsToSend, + $select: "id,displayName,userPrincipalName,mail", + }, + }); + } + } + } + }); + } + + // If no GUIDs to process, ensure loading state is false + if (guidsSet.size === 0 && partnerGuidsMap.size === 0) { + setIsLoadingGuids(false); + } + }, + [guidMapping, activeTenant, directoryObjectsMutation, partnerDirectoryObjectsMutation] + ); + + // Create a memoized version of the string replacement function + const replaceGuidsAndUpnsInStringMemoized = useCallback( + (str) => replaceGuidsAndUpnsInString(str, guidMapping, upnMapping, isLoadingGuids), + [guidMapping, upnMapping, isLoadingGuids] + ); + + // Cleanup function to clear any pending timeouts when the component unmounts + useEffect(() => { + return () => { + if (rateLimitTimeoutRef.current) { + clearTimeout(rateLimitTimeoutRef.current); + rateLimitTimeoutRef.current = null; + } + }; + }, []); + + return { + guidMapping, + upnMapping, + isLoadingGuids, + resolveGuids, + isGuid, + extractObjectIdFromPartnerUPN, + replaceGuidsAndUpnsInString: replaceGuidsAndUpnsInStringMemoized, + }; +}; diff --git a/src/layouts/config.js b/src/layouts/config.js index f0991ef3fff5..812c19383a8d 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -196,7 +196,7 @@ export const nativeMenuItems = [ ], }, { - title: "Standards", + title: "Standards & Drift", path: "/tenant/standards", permissions: [ "Tenant.Standards.*", @@ -205,15 +205,10 @@ export const nativeMenuItems = [ ], items: [ { - title: "Standard Templates", + title: "Standards Management", path: "/tenant/standards/list-standards", permissions: ["Tenant.Standards.*"], }, - { - title: "Tenant Alignment", - path: "/tenant/standards/tenant-alignment", - permissions: ["Tenant.Standards.*"], - }, { title: "Best Practice Analyser", path: "/tenant/standards/bpa-report", @@ -256,11 +251,7 @@ export const nativeMenuItems = [ { title: "Reports", path: "/tenant/reports", - permissions: [ - "Tenant.Administration.*", - "Scheduler.Billing.*", - "Tenant.Application.*", - ], + permissions: ["Tenant.Administration.*", "Scheduler.Billing.*", "Tenant.Application.*"], items: [ { title: "Licence Report", @@ -270,9 +261,7 @@ export const nativeMenuItems = [ { title: "Sherweb Licence Report", path: "/tenant/reports/list-csp-licenses", - permissions: [ - "Tenant.Directory.*" - ], + permissions: ["Tenant.Directory.*"], }, { title: "Consented Applications", @@ -478,10 +467,7 @@ export const nativeMenuItems = [ { title: "Reports", path: "/endpoint/reports", - permissions: [ - "Endpoint.Device.*", - "Endpoint.Autopilot.*", - ], + permissions: ["Endpoint.Device.*", "Endpoint.Autopilot.*"], items: [ { title: "Analytics Device Score", @@ -711,6 +697,11 @@ export const nativeMenuItems = [ path: "/email/reports/mailbox-statistics", permissions: ["Exchange.Mailbox.*"], }, + { + title: "Mailbox Activity", + path: "/email/reports/mailbox-activity", + permissions: ["Exchange.Mailbox.*"], + }, { title: "Mailbox Client Access Settings", path: "/email/reports/mailbox-cas-settings", diff --git a/src/layouts/index.js b/src/layouts/index.js index add691f24bf0..5813fc0ee70e 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -137,6 +137,8 @@ export const Layout = (props) => { if (!hasPermission) { return null; } + } else { + return null; } // check sub-items if (item.items && item.items.length > 0) { @@ -148,7 +150,6 @@ export const Layout = (props) => { }) .filter(Boolean); }; - const filteredMenu = filterItemsByRole(nativeMenuItems); setMenuItems(filteredMenu); } else if ( @@ -159,7 +160,14 @@ export const Layout = (props) => { ) { setHideSidebar(true); } - }, [currentRole.isSuccess, swaStatus.data, swaStatus.isLoading]); + }, [ + currentRole.isSuccess, + swaStatus.data, + swaStatus.isLoading, + currentRole.data?.clientPrincipal?.userRoles, + currentRole.data?.permissions, + currentRole.isFetching, + ]); const handleNavPin = useCallback(() => { settings.handleUpdate({ diff --git a/src/pages/cipp/advanced/timers.js b/src/pages/cipp/advanced/timers.js index 711def195cc7..116d27cd836f 100644 --- a/src/pages/cipp/advanced/timers.js +++ b/src/pages/cipp/advanced/timers.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { SvgIcon, Button } from "@mui/material"; -import { Refresh } from "@mui/icons-material"; +import { Refresh, PlayArrow } from "@mui/icons-material"; import { ApiPostCall } from "../../../api/ApiCall"; import { useEffect, useState } from "react"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage"; @@ -81,6 +81,7 @@ const Page = () => { data: { FunctionName: "Command", Parameters: "Parameters" }, confirmText: "Do you want to run this task now?", allowResubmit: true, + icon: , }, ]} /> diff --git a/src/pages/cipp/logs/index.js b/src/pages/cipp/logs/index.js index f2577871fe58..c17f96f15b89 100644 --- a/src/pages/cipp/logs/index.js +++ b/src/pages/cipp/logs/index.js @@ -17,6 +17,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useForm } from "react-hook-form"; import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; import { FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { EyeIcon } from "@heroicons/react/24/outline"; const simpleColumns = [ "DateTime", @@ -34,6 +35,15 @@ const simpleColumns = [ const apiUrl = "/api/Listlogs"; const pageTitle = "Logbook Results"; +const actions = [ + { + label: "View Log Entry", + link: "/cipp/logs/logentry?logentry=[RowKey]", + icon: , + color: "primary", + }, +]; + const Page = () => { const formControl = useForm({ defaultValues: { @@ -180,7 +190,8 @@ const Page = () => { Use the filters below to narrow down your logbook results. You can filter by date range, username, and severity levels. By default, the logbook shows the - current day based on UTC time. Your local time is {new Date().getTimezoneOffset() / -60} hours offset from UTC. + current day based on UTC time. Your local time is{" "} + {new Date().getTimezoneOffset() / -60} hours offset from UTC.
    @@ -303,24 +314,11 @@ const Page = () => { Severity: severity, // Pass severity filter from state Filter: filterEnabled, // Pass filter toggle state }} + actions={actions} /> ); }; -/* Comment to Developer: - - The filter is inside an expandable Accordion. By default, the filter is collapsed. - - The "Apply Filters" button sets the form data for date, username, and severity filters. - - The "Clear Filters" button resets all filters and disables filtering. - - Filters are automatically enabled when any filter parameter is set. - - Form state is managed using react-hook-form, and the filter states are applied to the table. - - Both StartDate and EndDate are passed to the API in 'YYYYMMDD' format. - - The User parameter is passed directly as a string for username filtering. - - The Severity parameter is passed as a comma-separated list of severity levels. - - The Filter toggle is passed as a boolean and is automatically enabled when any filter is set. - - A warning alert is displayed when the selected date range exceeds 10 days instead of enforcing - a strict limit. This helps users understand potential issues with large data sets. -*/ - Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/cipp/logs/logentry.js b/src/pages/cipp/logs/logentry.js new file mode 100644 index 000000000000..3f9c1aef3ae0 --- /dev/null +++ b/src/pages/cipp/logs/logentry.js @@ -0,0 +1,137 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { + Button, + SvgIcon, + Box, + Container, + Chip, +} from "@mui/material"; +import { Stack } from "@mui/system"; +import ArrowLeftIcon from "@mui/icons-material/ArrowLeft"; +import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; +import { CippInfoBar } from "/src/components/CippCards/CippInfoBar"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; + +const Page = () => { + const router = useRouter(); + const { logentry } = router.query; + + const logRequest = ApiGetCall({ + url: `/api/Listlogs`, + data: { + logentryid: logentry, + }, + queryKey: `GetLogEntry-${logentry}`, + waiting: !!logentry, + }); + + const handleBackClick = () => { + router.push("/cipp/logs"); + }; + + // Get the log data from array + const logData = logRequest.data?.[0]; + + // Top info bar data like dashboard + const logInfo = logData ? [ + { name: "Log ID", data: logData.RowKey }, + { name: "Date & Time", data: new Date(logData.DateTime).toLocaleString() }, + { name: "API", data: logData.API }, + { + name: "Severity", + data: ( + + ) + }, + ] : []; + + // Main log properties + const propertyItems = logData ? [ + { label: "Tenant", value: logData.Tenant }, + { label: "User", value: logData.User }, + { label: "Message", value: logData.Message }, + { label: "Tenant ID", value: logData.TenantID }, + { label: "App ID", value: logData.AppId || "None" }, + { label: "IP Address", value: logData.IP || "None" }, + ] : []; + + // LogData properties + const logDataItems = logData?.LogData && typeof logData.LogData === 'object' + ? Object.entries(logData.LogData).map(([key, value]) => ({ + label: key, + value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value), + })) + : []; + + return ( + + + + {/* Back button */} + + + {logRequest.isLoading && } + + {logRequest.isError && ( + + )} + + {logRequest.isSuccess && logData && ( + <> + {/* Top info bar like dashboard */} + + + {/* Main log information */} + + + {/* LogData in separate card */} + {logDataItems.length > 0 && ( + + )} + + )} + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/administration/contacts-template/deploy.jsx b/src/pages/email/administration/contacts-template/deploy.jsx index 1da26d9a5952..6766b209d6ba 100644 --- a/src/pages/email/administration/contacts-template/deploy.jsx +++ b/src/pages/email/administration/contacts-template/deploy.jsx @@ -31,6 +31,7 @@ const Page = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/email/reports/mailbox-activity/index.js b/src/pages/email/reports/mailbox-activity/index.js new file mode 100644 index 000000000000..554f6337b717 --- /dev/null +++ b/src/pages/email/reports/mailbox-activity/index.js @@ -0,0 +1,152 @@ +import { useState } from "react"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { + Button, + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + SvgIcon, + Stack, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { ExpandMore, Sort } from "@mui/icons-material"; +import { FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { useForm } from "react-hook-form"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; + +const Page = () => { + const formControl = useForm({ + defaultValues: { + period: { value: "D30", label: "30 days" }, + }, + }); + + const [expanded, setExpanded] = useState(false); + const [selectedPeriod, setSelectedPeriod] = useState("D30"); + const [selectedPeriodLabel, setSelectedPeriodLabel] = useState("30 days"); + + const periodOptions = [ + { value: "D7", label: "7 days" }, + { value: "D30", label: "30 days" }, + { value: "D90", label: "90 days" }, + { value: "D180", label: "180 days" }, + ]; + + const onSubmit = (data) => { + const periodValue = + typeof data.period === "object" && data.period?.value ? data.period.value : data.period; + const periodLabel = + typeof data.period === "object" && data.period?.label ? data.period.label : data.period; + + setSelectedPeriod(periodValue); + setSelectedPeriodLabel(periodLabel); + setExpanded(false); + }; + + const clearFilters = () => { + formControl.reset({ + period: { value: "D30", label: "30 days" }, + }); + setSelectedPeriod("D30"); + setSelectedPeriodLabel("30 days"); + setExpanded(false); + }; + + const tableFilter = ( + setExpanded(!expanded)}> + }> + + + + + + Report Period + + (Period: {selectedPeriodLabel}) + + + + + +
    + + + + + + + + + + + + +
    +
    +
    + ); + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/spamfilter/list-connectionfilter/add.jsx b/src/pages/email/spamfilter/list-connectionfilter/add.jsx index 2fad50d45b26..a0c30550356d 100644 --- a/src/pages/email/spamfilter/list-connectionfilter/add.jsx +++ b/src/pages/email/spamfilter/list-connectionfilter/add.jsx @@ -41,6 +41,7 @@ const AddPolicy = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/email/spamfilter/list-quarantine-policies/add.jsx b/src/pages/email/spamfilter/list-quarantine-policies/add.jsx index bd2e36fc3764..69def81cf558 100644 --- a/src/pages/email/spamfilter/list-quarantine-policies/add.jsx +++ b/src/pages/email/spamfilter/list-quarantine-policies/add.jsx @@ -47,6 +47,7 @@ const AddPolicy = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> @@ -72,73 +73,67 @@ const AddPolicy = () => { */} - - - - - + + + + + - - - - - - - - - - - + + + + + + + + + + ); diff --git a/src/pages/email/spamfilter/list-spamfilter/add.jsx b/src/pages/email/spamfilter/list-spamfilter/add.jsx index eaa81a6909ec..b25c936e8ecc 100644 --- a/src/pages/email/spamfilter/list-spamfilter/add.jsx +++ b/src/pages/email/spamfilter/list-spamfilter/add.jsx @@ -41,6 +41,7 @@ const AddPolicy = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/email/transport/list-connectors/add.jsx b/src/pages/email/transport/list-connectors/add.jsx index 233101bf3e09..acd4f0c0a50f 100644 --- a/src/pages/email/transport/list-connectors/add.jsx +++ b/src/pages/email/transport/list-connectors/add.jsx @@ -41,6 +41,7 @@ const AddPolicy = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/email/transport/list-rules/add.jsx b/src/pages/email/transport/list-rules/add.jsx index b72592a724a4..e98931c56eda 100644 --- a/src/pages/email/transport/list-rules/add.jsx +++ b/src/pages/email/transport/list-rules/add.jsx @@ -41,6 +41,7 @@ const AddPolicy = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/endpoint/MEM/add-policy/index.js b/src/pages/endpoint/MEM/add-policy/index.js index 158d85c81539..b0ff95a5c89e 100644 --- a/src/pages/endpoint/MEM/add-policy/index.js +++ b/src/pages/endpoint/MEM/add-policy/index.js @@ -10,7 +10,7 @@ const Page = () => { title: "Step 1", description: "Tenant Selection", component: CippTenantStep, - componentProps: { type: "multiple", valueField: "customerId" }, + componentProps: { type: "multiple" }, }, { title: "Step 2", diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 2f80e958a29c..283259e5dec3 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -9,6 +9,7 @@ import { Password, PasswordOutlined, Key, + Edit, Security, FindInPage, Shield, @@ -69,6 +70,25 @@ const Page = () => { ], confirmText: "Select the User to set as the primary user for this device", }, + { + label: "Rename Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "setDeviceName", + }, + confirmText: "Enter the new name for the device", + fields: [ + { + type: "textField", + name: "input", + label: "New Device Name", + required: true, + }, + ], + }, { label: "Sync Device", type: "POST", @@ -180,19 +200,6 @@ const Page = () => { }, confirmText: "Are you sure you want to generate logs and ship these to MEM?", }, - /* - { - label: "Rename device", - type: "POST", - icon: null, - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "setDeviceName", - }, - confirmText: "Enter the new name for the device", - }, - */ { label: "Fresh Start (Remove user data)", type: "POST", @@ -318,7 +325,11 @@ const Page = () => { return ( { const pageTitle = "App Protection & Configuration Policies"; + const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"]; const actions = [ { @@ -58,6 +61,16 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + cardButton={ + } + > + Deploy Policy + + } /> ); }; diff --git a/src/pages/endpoint/MEM/list-compliance-policies/index.js b/src/pages/endpoint/MEM/list-compliance-policies/index.js index a55d76931cc3..a2bb8eb989d1 100644 --- a/src/pages/endpoint/MEM/list-compliance-policies/index.js +++ b/src/pages/endpoint/MEM/list-compliance-policies/index.js @@ -1,10 +1,13 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Book, LaptopChromebook } from "@mui/icons-material"; +import { Book, LaptopChromebook, RocketLaunch } from "@mui/icons-material"; import { GlobeAltIcon, TrashIcon, UserIcon } from "@heroicons/react/24/outline"; +import { PermissionButton } from "/src/utils/permissions.js"; +import Link from "next/link"; const Page = () => { const pageTitle = "Intune Compliance Policies"; + const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"]; const actions = [ { @@ -99,6 +102,16 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + cardButton={ + } + > + Deploy Policy + + } /> ); }; diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js index 79a705984f39..a59547b8cbf7 100644 --- a/src/pages/endpoint/MEM/list-policies/index.js +++ b/src/pages/endpoint/MEM/list-policies/index.js @@ -1,10 +1,13 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Book, LaptopChromebook } from "@mui/icons-material"; +import { Book, LaptopChromebook, RocketLaunch } from "@mui/icons-material"; import { GlobeAltIcon, TrashIcon, UserIcon } from "@heroicons/react/24/outline"; +import { PermissionButton } from "/src/utils/permissions.js"; +import Link from "next/link"; const Page = () => { const pageTitle = "Configuration Policies"; + const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"]; const actions = [ { @@ -98,6 +101,16 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + cardButton={ + } + > + Deploy Policy + + } /> ); }; diff --git a/src/pages/endpoint/applications/list/add.jsx b/src/pages/endpoint/applications/list/add.jsx index f52d33491005..1c431696526c 100644 --- a/src/pages/endpoint/applications/list/add.jsx +++ b/src/pages/endpoint/applications/list/add.jsx @@ -118,6 +118,7 @@ const ApplicationDeploymentForm = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/endpoint/autopilot/add-status-page/index.js b/src/pages/endpoint/autopilot/add-status-page/index.js index 4d109115ce94..3057f7e365d8 100644 --- a/src/pages/endpoint/autopilot/add-status-page/index.js +++ b/src/pages/endpoint/autopilot/add-status-page/index.js @@ -1,6 +1,6 @@ import { Divider } from "@mui/material"; import { Grid } from "@mui/system"; -import { useForm} 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"; @@ -40,6 +40,7 @@ const Page = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/endpoint/autopilot/list-profiles/add.jsx b/src/pages/endpoint/autopilot/list-profiles/add.jsx index 1d6b4cd68a38..6f86da5c0d63 100644 --- a/src/pages/endpoint/autopilot/list-profiles/add.jsx +++ b/src/pages/endpoint/autopilot/list-profiles/add.jsx @@ -45,6 +45,7 @@ const AutopilotProfileForm = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/identity/administration/groups/edit.jsx b/src/pages/identity/administration/groups/edit.jsx index d894dfd18433..a052a7f28b30 100644 --- a/src/pages/identity/administration/groups/edit.jsx +++ b/src/pages/identity/administration/groups/edit.jsx @@ -98,6 +98,7 @@ const EditGroup = () => { } return null; })(), + securityEnabled: group.securityEnabled, // Initialize empty arrays for add/remove actions AddMember: [], RemoveMember: [], @@ -112,6 +113,7 @@ const EditGroup = () => { allowExternal: groupInfo?.data?.allowExternal, sendCopies: groupInfo?.data?.sendCopies, hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, + securityEnabled: group.securityEnabled, }); // Reset the form with all values @@ -125,7 +127,12 @@ const EditGroup = () => { const cleanedData = { ...formData }; // Properties that should only be sent if they've changed from initial values - const changeDetectionProperties = ["allowExternal", "sendCopies", "hideFromOutlookClients"]; + const changeDetectionProperties = [ + "allowExternal", + "sendCopies", + "hideFromOutlookClients", + "securityEnabled", + ]; changeDetectionProperties.forEach((property) => { if (formData[property] === initialValues[property]) { @@ -408,6 +415,18 @@ const EditGroup = () => { /> )} + {groupType === "Microsoft 365" && ( + + + + )}
    )} diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index 7bb042fb9dcf..fd3f8e5ddfe0 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -1,238 +1,240 @@ -import { Box, Divider } from "@mui/material"; -import { Grid } from "@mui/system"; -import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; -import { useForm } from "react-hook-form"; -import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; -import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; -import gdaproles from "/src/data/GDAPRoles.json"; -import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector"; -import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; -const Page = () => { - const formControl = useForm({ Mode: "onChange" }); - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - { - if (!option?.value) { - return "Domain is required"; - } - return true; - }, - }} - /> - - - - - - - - - - - - - - { - if (!value) { - return "Start date is required"; - } - return true; - }, - }} - /> - - - { - const startDate = formControl.getValues("startDate"); - if (!value) { - return "End date is required"; - } - if (new Date(value) < new Date(startDate)) { - return "End date must be after start date"; - } - return true; - }, - }} - /> - - - ({ label: role.Name, value: role.ObjectId }))} - formControl={formControl} - required={true} - validators={{ - validate: (options) => { - if (!options?.length) { - return "At least one role is required"; - } - return true; - }, - }} - /> - - - - - - { - if (!option?.value) { - return "Expiration action is required"; - } - return true; - }, - }} - /> - - - - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { Box, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; +import { useForm } from "react-hook-form"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; +import gdaproles from "/src/data/GDAPRoles.json"; +import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector"; +import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; +const Page = () => { + const formControl = useForm({ Mode: "onChange" }); + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + { + if (!option?.value) { + return "Domain is required"; + } + return true; + }, + }} + /> + + + + + + + + + + + + + + { + if (!value) { + return "Start date is required"; + } + return true; + }, + }} + /> + + + { + const startDate = formControl.getValues("startDate"); + if (!value) { + return "End date is required"; + } + if (new Date(value) < new Date(startDate)) { + return "End date must be after start date"; + } + return true; + }, + }} + /> + + + ({ label: role.Name, value: role.ObjectId }))} + formControl={formControl} + required={true} + validators={{ + validate: (options) => { + if (!options?.length) { + return "At least one role is required"; + } + return true; + }, + }} + /> + + + + + + { + if (!option?.value) { + return "Expiration action is required"; + } + return true; + }, + }} + /> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/users/index.js b/src/pages/identity/administration/users/index.js index d8599a158528..69a290b92615 100644 --- a/src/pages/identity/administration/users/index.js +++ b/src/pages/identity/administration/users/index.js @@ -1,113 +1,113 @@ -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { Send, GroupAdd, PersonAdd } from "@mui/icons-material"; -import Link from "next/link"; -import { useSettings } from "/src/hooks/use-settings.js"; -import { PermissionButton } from "../../../../utils/permissions"; -import { CippUserActions } from "/src/components/CippComponents/CippUserActions.jsx"; - -const Page = () => { - const pageTitle = "Users"; - const tenant = useSettings().currentTenant; - const cardButtonPermissions = ["Identity.User.ReadWrite"]; - - const filters = [ - { - filterName: "Account Enabled", - value: [{ id: "accountEnabled", value: "Yes" }], - type: "column", - }, - { - filterName: "Account Disabled", - value: [{ id: "accountEnabled", value: "No" }], - type: "column", - }, - { - filterName: "Guest Accounts", - value: [{ id: "userType", value: "Guest" }], - type: "column", - }, - ]; - - const offCanvas = { - extendedInfoFields: [ - "createdDateTime", // Created Date (UTC) - "id", // Unique ID - "userPrincipalName", // UPN - "givenName", // Given Name - "surname", // Surname - "jobTitle", // Job Title - "assignedLicenses", // Licenses - "businessPhones", // Business Phone - "mobilePhone", // Mobile Phone - "mail", // Mail - "city", // City - "department", // Department - "onPremisesLastSyncDateTime", // OnPrem Last Sync - "onPremisesDistinguishedName", // OnPrem DN - "otherMails", // Alternate Email Addresses - ], - actions: CippUserActions(), - }; - - return ( - - } - > - Add User - - } - > - Bulk Add Users - - } - > - Invite Guest - - - } - apiData={{ - Endpoint: "users", - manualPagination: true, - $select: - "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", - $count: true, - $orderby: "displayName", - $top: 999, - }} - apiDataKey="Results" - actions={CippUserActions()} - offCanvas={offCanvas} - simpleColumns={[ - "accountEnabled", - "userPrincipalName", - "displayName", - "mail", - "businessPhones", - "proxyAddresses", - "assignedLicenses", - ]} - filters={filters} - /> - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { Send, GroupAdd, PersonAdd } from "@mui/icons-material"; +import Link from "next/link"; +import { useSettings } from "/src/hooks/use-settings.js"; +import { PermissionButton } from "../../../../utils/permissions"; +import { CippUserActions } from "/src/components/CippComponents/CippUserActions.jsx"; + +const Page = () => { + const pageTitle = "Users"; + const tenant = useSettings().currentTenant; + const cardButtonPermissions = ["Identity.User.ReadWrite"]; + + const filters = [ + { + filterName: "Account Enabled", + value: [{ id: "accountEnabled", value: "Yes" }], + type: "column", + }, + { + filterName: "Account Disabled", + value: [{ id: "accountEnabled", value: "No" }], + type: "column", + }, + { + filterName: "Guest Accounts", + value: [{ id: "userType", value: "Guest" }], + type: "column", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "createdDateTime", // Created Date (UTC) + "id", // Unique ID + "userPrincipalName", // UPN + "givenName", // Given Name + "surname", // Surname + "jobTitle", // Job Title + "assignedLicenses", // Licenses + "businessPhones", // Business Phone + "mobilePhone", // Mobile Phone + "mail", // Mail + "city", // City + "department", // Department + "onPremisesLastSyncDateTime", // OnPrem Last Sync + "onPremisesDistinguishedName", // OnPrem DN + "otherMails", // Alternate Email Addresses + ], + actions: CippUserActions(), + }; + + return ( + + } + > + Add User + + } + > + Bulk Add Users + + } + > + Invite Guest + + + } + apiData={{ + Endpoint: "users", + manualPagination: true, + $select: + "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", + $count: true, + $orderby: "displayName", + $top: 999, + }} + apiDataKey="Results" + actions={CippUserActions()} + offCanvas={offCanvas} + simpleColumns={[ + "accountEnabled", + "userPrincipalName", + "displayName", + "mail", + "businessPhones", + "proxyAddresses", + "assignedLicenses", + ]} + filters={filters} + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx new file mode 100644 index 000000000000..8ea240386296 --- /dev/null +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -0,0 +1,611 @@ +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx"; +import { + Stack, + Typography, + Card, + CardContent, + Chip, + Box, + TextField, + Checkbox, + Button, + Switch, + FormControlLabel, + Autocomplete, +} from "@mui/material"; +import { CippWizardStepButtons } from "/src/components/CippWizard/CippWizardStepButtons"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { Delete } from "@mui/icons-material"; + +// User properties that can be patched +const PATCHABLE_PROPERTIES = [ + { + property: "city", + label: "City", + type: "string", + }, + { + property: "companyName", + label: "Company Name", + type: "string", + }, + { + property: "country", + label: "Country", + type: "string", + }, + { + property: "department", + label: "Department", + type: "string", + }, + { + property: "employeeType", + label: "Employee Type", + type: "string", + }, + { + property: "jobTitle", + label: "Job Title", + type: "string", + }, + { + property: "officeLocation", + label: "Office Location", + type: "string", + }, + { + property: "postalCode", + label: "Postal Code", + type: "string", + }, + { + property: "preferredDataLocation", + label: "Preferred Data Location", + type: "string", + }, + { + property: "preferredLanguage", + label: "Preferred Language", + type: "string", + }, + { + property: "showInAddressList", + label: "Show in Address List", + type: "boolean", + }, + { + property: "state", + label: "State/Province", + type: "string", + }, + { + property: "streetAddress", + label: "Street Address", + type: "string", + }, + { + property: "usageLocation", + label: "Usage Location", + type: "string", + }, +]; + +// Step 1: Display users to be updated +const UsersDisplayStep = (props) => { + const { onNextStep, onPreviousStep, formControl, currentStep, users, onUsersChange } = props; + + const handleRemoveUser = (userToRemove) => { + const updatedUsers = users.filter((user) => user.id !== userToRemove.id); + onUsersChange(updatedUsers); + }; + + // Clean user data without circular references + const tableData = + users?.map((user) => ({ + id: user.id, + displayName: user.displayName, + userPrincipalName: user.userPrincipalName, + jobTitle: user.jobTitle, + department: user.department, + // Only include serializable properties + })) || []; + + const columns = ["displayName", "userPrincipalName", "jobTitle", "department"]; + + // Define actions separately to avoid circular references + const rowActions = [ + { + label: "Remove from List", + icon: , + color: "error", + customFunction: (user) => handleRemoveUser(user), + noConfirm: true, + }, + ]; + + return ( + + + Users to be updated + + The following users will be updated with the properties you select in the next step. You + can remove users from this list if needed. + + + + {users && users.length > 0 ? ( + + ) : ( + + + + No users selected. Please go back and select users from the main table. + + + + )} + + 0 ? onNextStep : undefined} + formControl={formControl} + noNextButton={!users || users.length === 0} + /> + + ); +}; + +// Step 2: Property selection and input +const PropertySelectionStep = (props) => { + const { onNextStep, onPreviousStep, formControl, currentStep } = props; + const [selectedProperties, setSelectedProperties] = useState([]); + + // Register form fields + formControl.register("selectedProperties", { required: true }); + formControl.register("propertyValues", { required: false }); + + const handlePropertyValueChange = (property, value) => { + const currentValues = formControl.getValues("propertyValues") || {}; + const newValues = { ...currentValues, [property]: value }; + formControl.setValue("propertyValues", newValues); + formControl.trigger(); + }; + + const renderPropertyInput = (propertyName) => { + const property = PATCHABLE_PROPERTIES.find((p) => p.property === propertyName); + const currentValue = formControl.getValues("propertyValues")?.[propertyName]; + + if (property?.type === "boolean") { + return ( + handlePropertyValueChange(propertyName, e.target.checked)} + /> + } + label={property.label} + key={propertyName} + /> + ); + } + + // Default to text input for string types with consistent styling + return ( + handlePropertyValueChange(propertyName, e.target.value)} + placeholder={`Enter new value for ${property?.label || propertyName}`} + variant="filled" + size="small" + slotProps={{ + inputLabel: { + shrink: true, + sx: { transition: "none" }, + }, + input: { + notched: true, + sx: { + transition: "none", + "& .MuiOutlinedInput-notchedOutline": { + transition: "none", + }, + }, + }, + }} + /> + ); + }; + + return ( + + + Select Properties to update + + Choose which user properties you want to modify and provide the new values. + + + + selectedProperties.includes(prop.property))} + onChange={(event, newValue) => { + // Check if "Select All" was clicked + const selectAllOption = newValue.find((option) => option.isSelectAll); + + if (selectAllOption) { + // If Select All is in the new value, select all properties + const allSelected = selectedProperties.length === PATCHABLE_PROPERTIES.length; + const newProperties = allSelected ? [] : PATCHABLE_PROPERTIES.map((p) => p.property); + setSelectedProperties(newProperties); + formControl.setValue("selectedProperties", newProperties); + + // Reset property values when selection changes + const currentValues = formControl.getValues("propertyValues") || {}; + const newValues = {}; + newProperties.forEach((prop) => { + if (currentValues[prop]) { + newValues[prop] = currentValues[prop]; + } + }); + formControl.setValue("propertyValues", newValues); + formControl.trigger(); + } else { + // Normal property selection + const newProperties = newValue + .filter((prop) => !prop.isSelectAll) + .map((prop) => prop.property); + setSelectedProperties(newProperties); + formControl.setValue("selectedProperties", newProperties); + + // Reset property values when selection changes + const currentValues = formControl.getValues("propertyValues") || {}; + const newValues = {}; + newProperties.forEach((prop) => { + if (currentValues[prop]) { + newValues[prop] = currentValues[prop]; + } + }); + formControl.setValue("propertyValues", newValues); + formControl.trigger(); + } + }} + getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option.property === value.property} + size="small" + renderOption={(props, option, { selected }) => { + const isAllSelected = selectedProperties.length === PATCHABLE_PROPERTIES.length; + const isIndeterminate = + selectedProperties.length > 0 && + selectedProperties.length < PATCHABLE_PROPERTIES.length; + + if (option.isSelectAll) { + return ( +
  • + + {option.label} +
  • + ); + } + + return ( +
  • + + {option.label} +
  • + ); + }} + renderInput={(params) => ( + + )} + renderTags={(value, getTagProps) => + value + .filter((option) => !option.isSelectAll) + .map((option, index) => ( + + )) + } + /> + + {selectedProperties.length > 0 && ( + + + + Properties to update + + {selectedProperties.map(renderPropertyInput)} + + + )} + + + + ); +}; + +// Step 3: Confirmation +const ConfirmationStep = (props) => { + const { lastStep, formControl, onPreviousStep, currentStep, users } = props; + const formValues = formControl.getValues(); + const { selectedProperties = [], propertyValues = {} } = formValues; + + // Create API call handler for bulk patch + const patchUsersApi = ApiPostCall({ + relatedQueryKeys: ["ListUsers"], + }); + + const handleSubmit = () => { + // Validate that we still have users to patch + if (!users || users.length === 0) { + console.error("No users to patch"); + return; + } + + // Create bulk request data + const patchData = users.map((user) => { + const userData = { + id: user.id, + tenantFilter: user.Tenant || user.tenantFilter, + }; + selectedProperties.forEach((propName) => { + if (propertyValues[propName] !== undefined && propertyValues[propName] !== "") { + userData[propName] = propertyValues[propName]; + } + }); + return userData; + }); + + // Submit to API + patchUsersApi.mutate({ + url: "/api/PatchUser", + data: patchData, + }); + }; + + // Clean user data for table display + const tableData = + users?.map((user) => ({ + id: user.id, + displayName: user.displayName, + userPrincipalName: user.userPrincipalName, + jobTitle: user.jobTitle, + department: user.department, + })) || []; + + const columns = ["displayName", "userPrincipalName", "jobTitle", "department"]; + + return ( + + + Confirm User Updates + + Review the users that will be updated with {selectedProperties.length} selected{" "} + {selectedProperties.length === 1 ? "property" : "properties"}, then click Submit to apply + the changes. + + + + {/* Properties to be updated */} + {selectedProperties.length > 0 && ( + + + + Properties to Update + + + {selectedProperties.map((propName) => { + const property = PATCHABLE_PROPERTIES.find((p) => p.property === propName); + const value = propertyValues[propName]; + const displayValue = + property?.type === "boolean" ? (value ? "Yes" : "No") : value || "Not set"; + + return ( + + + {property?.label || propName}: + + + {displayValue} + + + ); + })} + + + + )} + + {users && users.length > 0 ? ( + + ) : ( + + + + No users to update. Please go back and select users. + + + + )} + + + + + {currentStep > 0 && ( + + )} + + + + ); +}; + +const Page = () => { + const router = useRouter(); + const [users, setUsers] = useState([]); + + // Get users from URL parameters or session storage + useEffect(() => { + try { + if (router.query.users) { + const parsedUsers = JSON.parse(decodeURIComponent(router.query.users)); + setUsers(Array.isArray(parsedUsers) ? parsedUsers : [parsedUsers]); + } else { + // Fallback to session storage + const storedUsers = sessionStorage.getItem("patchWizardUsers"); + if (storedUsers) { + const parsedUsers = JSON.parse(storedUsers); + setUsers(Array.isArray(parsedUsers) ? parsedUsers : [parsedUsers]); + // Clear session storage after use + sessionStorage.removeItem("patchWizardUsers"); + } + } + } catch (error) { + console.error("Error parsing users data:", error); + setUsers([]); + } + }, [router.query.users]); + + const steps = [ + { + title: "Step 1", + description: "Review Users", + component: UsersDisplayStep, + componentProps: { + users: users, + onUsersChange: setUsers, + }, + }, + { + title: "Step 2", + description: "Select Properties", + component: PropertySelectionStep, + }, + { + title: "Step 3", + description: "Confirmation", + component: ConfirmationStep, + componentProps: { + users: users, + }, + }, + ]; + + const initialState = { + selectedProperties: [], + propertyValues: {}, + }; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 29ff9301019b..d4f6014ecb4d 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -65,7 +65,7 @@ const Page = () => { waiting: waiting, }); const userRequest = ApiGetCall({ - url: `/api/ListUserMailboxDetails?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}`, + url: `/api/ListUserMailboxDetails?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}&userMail=${graphUserRequest.data?.[0]?.userPrincipalName}`, queryKey: `Mailbox-${userId}`, waiting: waiting, }); diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index d0d81b526bf1..c429c3107e45 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -33,6 +33,7 @@ const DeployDefenderForm = () => { name="selectedTenants" type="multiple" allTenants={true} + preselectedEnabled={true} validators={{ required: "At least one tenant must be selected" }} /> diff --git a/src/pages/security/safelinks/safelinks-template/add.jsx b/src/pages/security/safelinks/safelinks-template/add.jsx index f421d31ecbe0..3e9998b0a63e 100644 --- a/src/pages/security/safelinks/safelinks-template/add.jsx +++ b/src/pages/security/safelinks/safelinks-template/add.jsx @@ -25,25 +25,26 @@ const DeploySafeLinksPolicyTemplate = () => { postUrl="/api/AddSafeLinksPolicyFromTemplate" > - + - + { }; DeploySafeLinksPolicyTemplate.getLayout = (page) => {page}; -export default DeploySafeLinksPolicyTemplate; \ No newline at end of file +export default DeploySafeLinksPolicyTemplate; diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index d2b59fa0fe7c..fc3950a47a4e 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -84,8 +84,6 @@ const AlertWizard = () => { if (alert?.LogType === "Scripted") { setAlertType("script"); - //console.log(alert); - // Create formatted excluded tenants array if it exists const excludedTenantsFormatted = Array.isArray(alert.excludedTenants) ? alert.excludedTenants.map((tenant) => ({ value: tenant, label: tenant })) @@ -176,14 +174,53 @@ const AlertWizard = () => { })) ); - formControl.reset({ + // Format conditions properly for form + const formattedConditions = alert.RawAlert.Conditions.map((condition) => { + const formattedCondition = { + Property: condition.Property, + Operator: condition.Operator, + }; + + // Handle Input based on Property type + if (condition.Property.value === "String") { + // For String type, we need to set both the nested value and the direct value + formattedCondition.Input = { + value: condition.Input.value, + }; + } else { + // For List type, use the full Input object + formattedCondition.Input = condition.Input; + } + + return formattedCondition; + }); + + const resetData = { RowKey: router.query.clone ? undefined : router.query.id ? router.query.id : undefined, tenantFilter: alert.RawAlert.Tenants, - excludedTenants: alert.excludedTenants, + excludedTenants: alert.excludedTenants?.filter((tenant) => tenant !== null) || [], Actions: alert.RawAlert.Actions, - conditions: alert.RawAlert.Conditions, + conditions: formattedConditions, logbook: foundLogbook, - }); + }; + + formControl.reset(resetData); + + // After reset, manually set the Input values to ensure they're properly registered + setTimeout(() => { + formattedConditions.forEach((condition, index) => { + if (condition.Property.value === "String") { + // For String properties, set the nested value path + formControl.setValue(`conditions.${index}.Input.value`, condition.Input.value); + } else { + // For List properties, set the direct Input value + formControl.setValue(`conditions.${index}.Input`, condition.Input); + } + }); + + // Trigger validation to ensure all fields are properly registered + formControl.trigger(); + }, 100); } } }, [existingAlert.isSuccess, router, editAlert]); @@ -216,9 +253,13 @@ const AlertWizard = () => { recommendedOption.label += " (Recommended)"; } setRecurrenceOptions(updatedRecurrenceOptions); - formControl.setValue("recurrence", recommendedOption); + + // Only set the recommended recurrence if we're NOT editing an existing alert + if (!editAlert) { + formControl.setValue("recurrence", recommendedOption); + } } - }, [commandValue]); + }, [commandValue, editAlert]); useEffect(() => { // Logic to handle template-based form updates when a preset is selected @@ -269,7 +310,7 @@ const AlertWizard = () => { }; const handleAuditSubmit = (values) => { - values.conditions = values.conditions.filter((condition) => condition.Property); + values.conditions = values.conditions.filter((condition) => condition?.Property); apiRequest.mutate({ url: "/api/AddAlert", data: values }); }; @@ -470,6 +511,18 @@ const AlertWizard = () => { formControl={formControl} label="Select property" options={getAuditLogSchema(logbookWatcher?.value)} + creatable={true} + onCreateOption={(option) => { + const propertyName = option.label || option; + + // Return the option with String type for immediate use + const newOption = { + label: propertyName, + value: "String", // Always set to String for custom properties + }; + + return newOption; + }} /> diff --git a/src/pages/tenant/administration/audit-logs/index.js b/src/pages/tenant/administration/audit-logs/index.js index 1c0809755fda..9a329abfb73a 100644 --- a/src/pages/tenant/administration/audit-logs/index.js +++ b/src/pages/tenant/administration/audit-logs/index.js @@ -1,19 +1,27 @@ import { useState } from "react"; +import { useRouter } from "next/router"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Button, Accordion, AccordionSummary, AccordionDetails, Typography } from "@mui/material"; +import { + Box, + Button, + Accordion, + AccordionSummary, + AccordionDetails, + Typography, +} from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { EyeIcon } from "@heroicons/react/24/outline"; import { Grid } from "@mui/system"; +import tabOptions from "./tabOptions.json"; -const simpleColumns = ["Timestamp", "Tenant", "Title", "Actions"]; - -const apiUrl = "/api/ListAuditLogs"; -const pageTitle = "Audit Logs"; - -const actions = [ +// Saved Logs Configuration +const savedLogsColumns = ["Timestamp", "Tenant", "Title", "Actions"]; +const savedLogsApiUrl = "/api/ListAuditLogs"; +const savedLogsActions = [ { label: "View Log", link: "/tenant/administration/audit-logs/log?id=[LogId]", @@ -23,22 +31,23 @@ const actions = [ ]; const Page = () => { + const router = useRouter(); + const formControl = useForm({ mode: "onChange", defaultValues: { dateFilter: "relative", - Time: 1, + Time: 7, Interval: { label: "Days", value: "d" }, }, }); - const [expanded, setExpanded] = useState(false); // Accordion state - const [relativeTime, setRelativeTime] = useState("1d"); // Relative time filter - const [startDate, setStartDate] = useState(null); // Start date filter - const [endDate, setEndDate] = useState(null); // End date filter + const [expanded, setExpanded] = useState(false); + const [relativeTime, setRelativeTime] = useState("7d"); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); const onSubmit = (data) => { - // Handle filter application logic if (data.dateFilter === "relative") { setRelativeTime(`${data.Time}${data.Interval.value}`); setStartDate(null); @@ -50,113 +59,121 @@ const Page = () => { } }; - return ( - setExpanded(!expanded)}> - }> - Search Options - - -
    - - {/* Date Filter Type */} - - - + // API parameters for saved logs + const apiParams = { + RelativeTime: relativeTime ? relativeTime : "7d", + ...(startDate && { StartDate: startDate }), + ...(endDate && { EndDate: endDate }), + }; - {/* Relative Time Filter */} - {formControl.watch("dateFilter") === "relative" && ( - <> - - - - - - - - - - - - )} + const searchFilter = ( + setExpanded(!expanded)}> + }> + Search Options + + + + + {/* Date Filter Type */} + + + - {/* Start and End Date Filters */} - {formControl.watch("dateFilter") === "startEnd" && ( - <> - + {/* Relative Time Filter */} + {formControl.watch("dateFilter") === "relative" && ( + <> + + + - + - - )} + + + + )} - {/* Submit Button */} - - + {/* Start and End Date Filters */} + {formControl.watch("dateFilter") === "startEnd" && ( + <> + + + + + - - - - - } - title={pageTitle} - apiUrl={apiUrl} + + )} + + {/* Submit Button */} + + + +
    + +
    + + ); + + return ( + ); }; /* Comment to Developer: + - This page displays saved audit logs with date filtering options. - The filter options are implemented within an Accordion for a collapsible UI. - DateFilter types are supported as 'Relative' and 'Start/End'. - Relative time is calculated based on Time and Interval inputs. @@ -164,6 +181,10 @@ const Page = () => { - Filters are dynamically applied to the table query. */ -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => ( + + {page} + +); export default Page; diff --git a/src/pages/tenant/administration/audit-logs/log.js b/src/pages/tenant/administration/audit-logs/log.js index 162fdd2f6b37..d240889c26e7 100644 --- a/src/pages/tenant/administration/audit-logs/log.js +++ b/src/pages/tenant/administration/audit-logs/log.js @@ -88,7 +88,12 @@ const Page = () => { { label: "Tenant", value: data.Tenant }, { label: "User", - value: data?.Data?.RawData?.UserKey ?? data?.Data?.RawData?.AuditRecord?.userId ?? "N/A", + value: + data?.Data?.RawData?.CIPPUserKey ?? + data?.Data?.RawData?.AuditRecord?.CIPPuserId ?? + data?.Data?.RawData?.AuditRecord?.UserKey ?? + data?.Data?.RawData?.userId ?? + "N/A", }, { label: "IP Address", value: data?.Data?.IP }, { diff --git a/src/pages/tenant/administration/audit-logs/search-results.js b/src/pages/tenant/administration/audit-logs/search-results.js new file mode 100644 index 000000000000..88ab5a7bf26b --- /dev/null +++ b/src/pages/tenant/administration/audit-logs/search-results.js @@ -0,0 +1,121 @@ +import { useRouter } from "next/router"; +import { useState, useEffect } from "react"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; +import { EyeIcon, ArrowLeftIcon } from "@heroicons/react/24/outline"; +import CippAuditLogDetails from "/src/components/CippComponents/CippAuditLogDetails.jsx"; +import { Button, SvgIcon, Box } from "@mui/material"; +import { ManageSearch } from "@mui/icons-material"; +import { useDialog } from "/src/hooks/use-dialog"; + +const searchResultsColumns = [ + "createdDateTime", + "userPrincipalName", + "operation", + "service", + "auditLogRecordType", + "clientIp", + "Actions", +]; + +const Page = () => { + const router = useRouter(); + const [searchId, setSearchId] = useState(null); + const [searchName, setSearchName] = useState(null); + const [isReady, setIsReady] = useState(false); + const processLogsDialog = useDialog(); + + useEffect(() => { + if (router.isReady) { + setSearchId(router.query.id || router.query.searchId); + setSearchName(router.query.name ? decodeURIComponent(router.query.name) : null); + setIsReady(true); + } + }, [router.isReady, router.query.id, router.query.searchId, router.query.name]); + + if (!isReady) { + return
    Loading...
    ; + } + + if (!searchId) { + return
    Search ID is required
    ; + } + + const pageTitle = searchName ? `${searchName}` : `Search Results - ${searchId}`; + + const handleBackClick = () => { + router.push("/tenant/administration/audit-logs/searches"); + }; + + // Process Logs API configuration + const processLogsApi = { + type: "POST", + url: "/api/ExecAuditLogSearch", + confirmText: + "Process these logs? Note: This will only alert on logs that match your Alert Configuration rules.", + relatedQueryKeys: ["AuditLogSearches"], + allowResubmit: true, + data: { + Action: "ProcessLogs", + SearchId: searchId, + }, + }; + + // Define offcanvas configuration with larger size for audit log details + const offcanvas = { + title: "Audit Log Details", + size: "xl", // Make the offcanvas extra large + children: (row) => , + }; + + return ( + <> + + + + + } + apiUrl="/api/ListAuditLogSearches" + apiDataKey="Results" + simpleColumns={searchResultsColumns} + queryKey={`AuditLogSearchResults-${searchId}`} + apiData={{ + Type: "SearchResults", + SearchId: searchId, + }} + offCanvas={offcanvas} + actions={[]} + /> + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/audit-logs/searches.js b/src/pages/tenant/administration/audit-logs/searches.js new file mode 100644 index 000000000000..3c5a92dcfbde --- /dev/null +++ b/src/pages/tenant/administration/audit-logs/searches.js @@ -0,0 +1,508 @@ +import { useState, useEffect } from "react"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; +import { Button, Accordion, AccordionSummary, AccordionDetails, Typography } from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { useForm } from "react-hook-form"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { Grid } from "@mui/system"; +import { Add, ManageSearch } from "@mui/icons-material"; +import { useDialog } from "/src/hooks/use-dialog"; +import tabOptions from "./tabOptions.json"; +import { useSettings } from "/src/hooks/use-settings"; + +const simpleColumns = ["displayName", "status", "filterStartDateTime", "filterEndDateTime"]; + +const apiUrl = "/api/ListAuditLogSearches?Type=Searches"; +const pageTitle = "Log Searches"; + +const actions = [ + { + label: "View Results", + link: "/tenant/administration/audit-logs/search-results?id=[id]&name=[displayName]", + color: "primary", + icon: , + }, + { + label: "Process Logs", + url: "/api/ExecAuditLogSearch", + confirmText: + "Process these logs? Note: This will only alert on logs that match your Alert Configuration rules.", + type: "POST", + data: { + Action: "ProcessLogs", + SearchId: "id", + }, + icon: , + }, +]; + +const Page = () => { + const createSearchDialog = useDialog(); + const currentTenant = useSettings().currentTenant; + + const filterControl = useForm({ + mode: "onChange", + defaultValues: { + StatusFilter: { label: "All", value: "" }, + DateFilter: { label: "All Time", value: "" }, + }, + }); + + const [expanded, setExpanded] = useState(false); + const [apiUrlWithFilters, setApiUrlWithFilters] = useState(apiUrl); + + // Watch for filter changes and update API URL + const statusFilter = filterControl.watch("StatusFilter"); + const dateFilter = filterControl.watch("DateFilter"); + + useEffect(() => { + const params = new URLSearchParams(); + params.set("Type", "Searches"); // Always set Type=Searches for this page + + if (statusFilter?.value) { + params.set("Status", statusFilter.value); + } + + if (dateFilter?.value) { + params.set("Days", dateFilter.value); + } + + setApiUrlWithFilters(`/api/ListAuditLogSearches?${params.toString()}`); + }, [statusFilter, dateFilter]); + + // Create Search Dialog Configuration + const createSearchFields = [ + { + type: "textField", + name: "DisplayName", + label: "Search Name", + }, + { + type: "autoComplete", + name: "TenantFilter", + label: "Tenant", + multiple: false, + creatable: false, + api: { + url: "/api/ListTenants?AllTenantSelector=false", + labelField: (option) => `${option.displayName} (${option.defaultDomainName})`, + valueField: "defaultDomainName", + queryKey: "ListTenants-FormnotAllTenants", + excludeTenantFilter: true, + }, + validators: { validate: (value) => !!value?.value || "Please select a tenant" }, + required: true, + }, + { + type: "datePicker", + name: "StartTime", + label: "Start Date & Time", + dateTimeType: "datetime-local", + validators: { required: "Start time is required" }, + required: true, + }, + { + type: "datePicker", + name: "EndTime", + label: "End Date & Time", + dateTimeType: "datetime-local", + validators: { required: "End time is required" }, + required: true, + }, + { + type: "autoComplete", + name: "ServiceFilters", + label: "Services", + multiple: true, + creatable: false, + options: [ + { label: "Azure Active Directory", value: "AzureActiveDirectory" }, + { label: "Dynamics 365", value: "CRM" }, + { label: "Exchange Online", value: "Exchange" }, + { label: "Microsoft Flow", value: "MicrosoftFlow" }, + { label: "Microsoft Teams", value: "MicrosoftTeams" }, + { label: "OneDrive for Business", value: "OneDrive" }, + { label: "Power BI", value: "PowerBI" }, + { label: "Security & Compliance", value: "ThreatIntelligence" }, + { label: "SharePoint Online", value: "SharePoint" }, + { label: "Yammer", value: "Yammer" }, + ], + validators: { + validate: (values) => values?.length > 0 || "Please select at least one service", + }, + }, + { + type: "autoComplete", + name: "RecordTypeFilters", + label: "Record Types", + multiple: true, + creatable: false, + options: [ + { label: "Azure Active Directory", value: "azureActiveDirectory" }, + { label: "Azure AD Account Logon", value: "azureActiveDirectoryAccountLogon" }, + { label: "Azure AD STS Logon", value: "azureActiveDirectoryStsLogon" }, + { label: "Compliance DLP Endpoint", value: "complianceDLPEndpoint" }, + { label: "Compliance DLP Exchange", value: "complianceDLPExchange" }, + { label: "Compliance DLP SharePoint", value: "complianceDLPSharePoint" }, + { label: "Data Governance", value: "dataGovernance" }, + { label: "Exchange Admin", value: "exchangeAdmin" }, + { label: "Exchange Item", value: "exchangeItem" }, + { label: "Exchange Item Group", value: "exchangeItemGroup" }, + { label: "Information Worker Protection", value: "informationWorkerProtection" }, + { label: "Label Content Explorer", value: "labelContentExplorer" }, + { label: "Microsoft Flow", value: "microsoftFlow" }, + { label: "Microsoft Forms", value: "microsoftForms" }, + { label: "Microsoft Stream", value: "microsoftStream" }, + { label: "Microsoft Teams", value: "microsoftTeams" }, + { label: "Microsoft Teams Admin", value: "microsoftTeamsAdmin" }, + { label: "Microsoft Teams Analytics", value: "microsoftTeamsAnalytics" }, + { label: "Microsoft Teams Device", value: "microsoftTeamsDevice" }, + { label: "Microsoft Teams Shifts", value: "microsoftTeamsShifts" }, + { label: "MIP Label", value: "mipLabel" }, + { label: "OneDrive", value: "oneDrive" }, + { label: "Power Apps App", value: "powerAppsApp" }, + { label: "Power Apps Plan", value: "powerAppsPlan" }, + { label: "Power BI Audit", value: "powerBIAudit" }, + { label: "Power BI DLP", value: "powerBIDlp" }, + { label: "Security & Compliance Alerts", value: "securityComplianceAlerts" }, + { label: "Security & Compliance Insights", value: "securityComplianceInsights" }, + { label: "Security & Compliance RBAC", value: "securityComplianceRBAC" }, + { label: "SharePoint", value: "sharePoint" }, + { label: "SharePoint File Operation", value: "sharePointFileOperation" }, + { label: "SharePoint List Operation", value: "sharePointListOperation" }, + { label: "SharePoint Sharing Operation", value: "sharePointSharingOperation" }, + { label: "Threat Intelligence", value: "threatIntelligence" }, + { label: "Threat Intelligence ATP Content", value: "threatIntelligenceAtpContent" }, + { label: "Threat Intelligence URL", value: "threatIntelligenceUrl" }, + { label: "Workplace Analytics", value: "workplaceAnalytics" }, + ], + }, + { + type: "autoComplete", + name: "KeywordFilter", + label: "Keywords", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter keywords to search for", + options: [], + }, + { + type: "autoComplete", + name: "OperationsFilters", + label: "Operations", + multiple: true, + creatable: true, + placeholder: "Enter or select operations", + options: [ + // Authentication & User Operations + { label: "User Logged In", value: "UserLoggedIn" }, + { label: "Mailbox Login", value: "mailboxlogin" }, + + // User Management Operations + { label: "Add User", value: "add user." }, + { label: "Update User", value: "update user." }, + { label: "Delete User", value: "delete user." }, + { label: "Reset User Password", value: "reset user password." }, + { label: "Change User Password", value: "change user password." }, + { label: "Change User License", value: "change user license." }, + + // Group Management Operations + { label: "Add Group", value: "add group." }, + { label: "Update Group", value: "update group." }, + { label: "Delete Group", value: "delete group." }, + { label: "Add Member to Group", value: "add member to group." }, + { label: "Remove Member from Group", value: "remove member from group." }, + + // Mailbox Operations + { label: "New Mailbox", value: "New-Mailbox" }, + { label: "Set Mailbox", value: "Set-Mailbox" }, + { label: "Add Mailbox Permission", value: "add-mailboxpermission" }, + { label: "Remove Mailbox Permission", value: "remove-mailboxpermission" }, + { label: "Mail Items Accessed", value: "mailitemsaccessed" }, + + // Email Operations + { label: "Send Message", value: "send" }, + { label: "Send As", value: "sendas" }, + { label: "Send On Behalf", value: "sendonbehalf" }, + { label: "Create Item", value: "create" }, + { label: "Update Message", value: "update" }, + { label: "Copy Messages", value: "copy" }, + { label: "Move Messages", value: "move" }, + { label: "Move to Deleted Items", value: "movetodeleteditems" }, + { label: "Soft Delete", value: "softdelete" }, + { label: "Hard Delete", value: "harddelete" }, + + // Inbox Rules + { label: "New Inbox Rule", value: "new-inboxrule" }, + { label: "Set Inbox Rule", value: "set-inboxrule" }, + { label: "Update Inbox Rules", value: "updateinboxrules" }, + + // Folder Operations + { label: "Add Folder Permissions", value: "addfolderpermissions" }, + { label: "Remove Folder Permissions", value: "removefolderpermissions" }, + { label: "Update Folder Permissions", value: "updatefolderpermissions" }, + { label: "Update Calendar Delegation", value: "updatecalendardelegation" }, + + // SharePoint/OneDrive Operations (Common ones) + { label: "File Accessed", value: "FileAccessed" }, + { label: "File Modified", value: "FileModified" }, + { label: "File Deleted", value: "FileDeleted" }, + { label: "File Downloaded", value: "FileDownloaded" }, + { label: "File Uploaded", value: "FileUploaded" }, + { label: "Sharing Set", value: "SharingSet" }, + { label: "Anonymous Link Created", value: "AnonymousLinkCreated" }, + + // Role and Permission Operations + { label: "Add Member to Role", value: "add member to role." }, + { label: "Remove Member from Role", value: "remove member from role." }, + { label: "Add Service Principal", value: "add service principal." }, + { label: "Remove Service Principal", value: "remove service principal." }, + + // Company and Domain Operations + { label: "Add Domain to Company", value: "add domain to company." }, + { label: "Remove Domain from Company", value: "remove domain from company." }, + { label: "Verify Domain", value: "verify domain." }, + { label: "Set Company Information", value: "set company information." }, + + // Security Operations + { label: "Disable Strong Authentication", value: "Disable Strong Authentication." }, + { label: "Apply Record Label", value: "applyrecordlabel" }, + { label: "Update STS Refresh Token", value: "Update StsRefreshTokenValidFrom Timestamp." }, + ], + }, + { + type: "autoComplete", + name: "UserPrincipalNameFilters", + label: "User Principal Names", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter user principal names", + options: [], + }, + { + type: "autoComplete", + name: "IPAddressFilters", + label: "IP Addresses", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter IP addresses", + options: [], + }, + { + type: "autoComplete", + name: "ObjectIdFilters", + label: "Object IDs", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter object IDs", + options: [], + }, + { + type: "autoComplete", + name: "AdministrativeUnitFilters", + label: "Administrative Units", + multiple: true, + creatable: true, + placeholder: "Enter administrative units", + api: { + url: "/api/ListGraphRequest", + queryKey: "AdministrativeUnits", + data: { + Endpoint: "directoryObjects/microsoft.graph.administrativeUnit", + $select: "id,displayName", + }, + dataKey: "Results", + labelField: "displayName", + valueField: "id", + addedField: { + id: "id", + displayName: "displayName", + }, + showRefresh: true, + }, + }, + { + type: "switch", + name: "ProcessLogs", + label: "Process Logs for Alerts", + helperText: "Enable to store this search for alert processing", + }, + ]; + + const createSearchApi = { + type: "POST", + url: "/api/ExecAuditLogSearch", + confirmText: + "Create this audit log search? This may take several minutes to hours to complete.", + relatedQueryKeys: ["AuditLogSearches"], + allowResubmit: true, + customDataformatter: (row, action, data) => { + const formattedData = { ...data }; + console.log("Formatted Data:", formattedData); + // Extract value from TenantFilter autocomplete object + if (formattedData.TenantFilter?.value) { + formattedData.TenantFilter = formattedData.TenantFilter.value; + } + + // Handle KeywordFilter - extract values from array and join with spaces + if (Array.isArray(formattedData.KeywordFilter)) { + const keywords = formattedData.KeywordFilter.map((item) => + typeof item === "object" ? item.value : item + ).filter(Boolean); + formattedData.KeywordFilter = keywords.join(" "); + } + + // Extract values from RecordTypeFilters array + if (Array.isArray(formattedData.RecordTypeFilters)) { + formattedData.RecordTypeFilters = formattedData.RecordTypeFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from ServiceFilters array + if (Array.isArray(formattedData.ServiceFilters)) { + formattedData.ServiceFilters = formattedData.ServiceFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from OperationsFilters array + if (Array.isArray(formattedData.OperationsFilters)) { + formattedData.OperationsFilters = formattedData.OperationsFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from UserPrincipalNameFilters array + if (Array.isArray(formattedData.UserPrincipalNameFilters)) { + formattedData.UserPrincipalNameFilters = formattedData.UserPrincipalNameFilters.map( + (item) => (typeof item === "object" ? item.value : item) + ); + } + + // Extract values from IPAddressFilters array + if (Array.isArray(formattedData.IPAddressFilters)) { + formattedData.IPAddressFilters = formattedData.IPAddressFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from ObjectIdFilters array + if (Array.isArray(formattedData.ObjectIdFilters)) { + formattedData.ObjectIdFilters = formattedData.ObjectIdFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from AdministrativeUnitFilters array + if (Array.isArray(formattedData.AdministrativeUnitFilters)) { + formattedData.AdministrativeUnitFilters = formattedData.AdministrativeUnitFilters.map( + (item) => (typeof item === "object" ? item.value : item) + ); + } + + // Remove empty arrays to avoid sending unnecessary data + Object.keys(formattedData).forEach((key) => { + if (Array.isArray(formattedData[key]) && formattedData[key].length === 0) { + delete formattedData[key]; + } + if ( + formattedData[key] === "" || + formattedData[key] === null || + formattedData[key] === undefined + ) { + delete formattedData[key]; + } + }); + + return formattedData; + }, + }; + + return ( + <> + setExpanded(!expanded)}> + }> + Filter Search List + + + + {/* Status Filter */} + + + + + {/* Date Range Filter */} + + + + + + + } + title={pageTitle} + apiUrl={apiUrlWithFilters} + apiDataKey="Results" + simpleColumns={simpleColumns} + queryKey={`AuditLogSearches-${filterControl.getValues().StatusFilter?.value || "All"}-${ + filterControl.getValues().DateFilter?.value || "AllTime" + }-${currentTenant}`} + actions={actions} + cardButton={ + + } + /> + + + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/audit-logs/tabOptions.json b/src/pages/tenant/administration/audit-logs/tabOptions.json new file mode 100644 index 000000000000..b7f764d5821f --- /dev/null +++ b/src/pages/tenant/administration/audit-logs/tabOptions.json @@ -0,0 +1,10 @@ +[ + { + "label": "Saved Logs", + "path": "/tenant/administration/audit-logs" + }, + { + "label": "Log Searches", + "path": "/tenant/administration/audit-logs/searches" + } +] \ No newline at end of file diff --git a/src/pages/tenant/conditional/list-named-locations/add.jsx b/src/pages/tenant/conditional/list-named-locations/add.jsx index 06180dfdb1ae..906b869ed9d1 100644 --- a/src/pages/tenant/conditional/list-named-locations/add.jsx +++ b/src/pages/tenant/conditional/list-named-locations/add.jsx @@ -38,6 +38,8 @@ const DeployNamedLocationForm = () => { formControl={formControl} name="selectedTenants" type="multiple" + preselectedEnabled={true} + validators={{ required: "At least one tenant must be selected" }} allTenants={true} />
    diff --git a/src/pages/tenant/standards/index.js b/src/pages/tenant/standards/index.js deleted file mode 100644 index 5f8bc65212d6..000000000000 --- a/src/pages/tenant/standards/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Standards"; - - return ( -
    -

    {pageTitle}

    -

    This is a placeholder page for the standards section.

    -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/tenant/standards/list-applied-standards/index.js b/src/pages/tenant/standards/list-applied-standards/index.js deleted file mode 100644 index 6937c0fd745f..000000000000 --- a/src/pages/tenant/standards/list-applied-standards/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Edit Standards"; - - return ( -
    -

    {pageTitle}

    -

    This is a placeholder page for the edit standards section.

    -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/tenant/standards/list-standards/classic-standards/index.js b/src/pages/tenant/standards/list-standards/classic-standards/index.js new file mode 100644 index 000000000000..958502d70541 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js @@ -0,0 +1,198 @@ +import { Alert, Button } from "@mui/material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import Link from "next/link"; +import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub } from "@mui/icons-material"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import { Grid } from "@mui/system"; +import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import tabOptions from "../tabOptions.json"; + +const Page = () => { + const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" }); + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const pageTitle = "Templates"; + const actions = [ + { + label: "View Tenant Report", + link: "/tenant/standards/compare?templateId=[GUID]", + icon: , + color: "info", + target: "_self", + }, + { + label: "Edit Template", + //when using a link it must always be the full path /identity/administration/users/[id] for example. + link: "/tenant/standards/template?id=[GUID]&type=[type]", + icon: , + color: "success", + target: "_self", + }, + { + label: "Clone & Edit Template", + link: "/tenant/standards/template?id=[GUID]&clone=true", + icon: , + color: "success", + target: "_self", + }, + { + label: "Run Template Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: "GUID", + }, + confirmText: "Are you sure you want to force a run of this template?", + multiPost: false, + }, + { + label: "Run Template Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: "GUID", + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this template?", + multiPost: false, + }, + { + 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/RemoveStandardTemplate", + icon: , + data: { + ID: "GUID", + }, + confirmText: "Are you sure you want to delete [templateName]?", + multiPost: false, + }, + ]; + const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" }); + const handleConversion = () => { + conversionApi.mutate({ + url: "/api/execStandardConvert", + data: {}, + }); + }; + const tableFilter = ( +
    + {oldStandards.isSuccess && oldStandards.data.length !== 0 && ( + + + + + 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. + + + + + + + + + + + )} +
    + ); + return ( + + + + + } + actions={actions} + tableFilter={tableFilter} + simpleColumns={[ + "templateName", + "type", + "tenantFilter", + "excludedTenants", + "updatedAt", + "updatedBy", + "runManually", + "standards", + ]} + queryKey="listStandardTemplates" + /> + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/standards/list-standards/drift-alignment/index.js b/src/pages/tenant/standards/list-standards/drift-alignment/index.js new file mode 100644 index 000000000000..561a94daaeb6 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/drift-alignment/index.js @@ -0,0 +1,44 @@ +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import tabOptions from "../tabOptions.json"; + +const Page = () => { + const pageTitle = "Drift Alignment"; + + const actions = [ + { + label: "View Tenant Report", + link: "/tenant/standards/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", + icon: , + color: "info", + target: "_self", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; \ No newline at end of file diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index 0c479d6fa838..c4c95550bd02 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -1,186 +1,70 @@ -import { Alert, Button } from "@mui/material"; +import { Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; import Link from "next/link"; -import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub } from "@mui/icons-material"; -import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall"; -import { Grid } from "@mui/system"; -import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; +import { Delete, Add } from "@mui/icons-material"; import { EyeIcon } from "@heroicons/react/24/outline"; +import tabOptions from "./tabOptions.json"; const Page = () => { - const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" }); - const integrations = ApiGetCall({ - url: "/api/ListExtensionsConfig", - queryKey: "Integrations", - refetchOnMount: false, - refetchOnReconnect: false, - }); - const pageTitle = "Standard Templates"; + const pageTitle = "Standard & Drift Alignment"; + const actions = [ { label: "View Tenant Report", - link: "/tenant/standards/compare?templateId=[GUID]", + link: "/tenant/standards/manage-drift/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , color: "info", target: "_self", }, { - label: "Edit Template", - //when using a link it must always be the full path /identity/administration/users/[id] for example. - link: "/tenant/standards/template?id=[GUID]", - icon: , - color: "success", - target: "_self", - }, - { - label: "Clone & Edit Template", - link: "/tenant/standards/template?id=[GUID]&clone=true", - icon: , - color: "success", + label: "Manage Drift", + link: "/tenant/standards/manage-drift?templateId=[standardId]&tenantFilter=[tenantFilter]", + icon: , + color: "info", target: "_self", + condition: (row) => row.standardType === "drift", }, { - label: "Run Template Now (Currently Selected Tenant only)", - type: "GET", - url: "/api/ExecStandardsRun", - icon: , - data: { - TemplateId: "GUID", - }, - confirmText: "Are you sure you want to force a run of this template?", - multiPost: false, - }, - { - label: "Run Template Now (All Tenants in Template)", - type: "GET", - url: "/api/ExecStandardsRun", - icon: , - data: { - TemplateId: "GUID", - tenantFilter: "allTenants", - }, - confirmText: "Are you sure you want to force a run of this template?", - multiPost: false, - }, - { - label: "Save to GitHub", + label: "Remove Drift Customization", 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/RemoveStandardTemplate", + url: "/api/ExecUpdateDriftDeviation", icon: , data: { - ID: "GUID", + RemoveDriftCustomization: "true", + tenantFilter: "tenantFilter", }, - confirmText: "Are you sure you want to delete [templateName]?", + confirmText: + "Are you sure you want to remove all drift customizations? This resets the Drift Standard to the default template, and will generate alerts for the drifted items.", multiPost: false, + condition: (row) => row.standardType === "drift", }, ]; - const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" }); - const handleConversion = () => { - conversionApi.mutate({ - url: "/api/execStandardConvert", - data: {}, - }); - }; - const tableFilter = ( -
    - {oldStandards.isSuccess && oldStandards.data.length !== 0 && ( - - - - - 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. - - - - - - - - - - - )} -
    - ); + return ( }> - Add Template - - } actions={actions} - tableFilter={tableFilter} simpleColumns={[ - "templateName", "tenantFilter", - "excludedTenants", - "updatedAt", - "updatedBy", - "runManually", - "standards", + "standardName", + "standardType", + "alignmentScore", + "LicenseMissingPercentage", + "combinedAlignmentScore", ]} - queryKey="listStandardTemplates" + queryKey="listTenantAlignment" /> ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => ( + + {page} + +); export default Page; diff --git a/src/pages/tenant/standards/list-standards/tabOptions.json b/src/pages/tenant/standards/list-standards/tabOptions.json new file mode 100644 index 000000000000..1c522e6ca8ca --- /dev/null +++ b/src/pages/tenant/standards/list-standards/tabOptions.json @@ -0,0 +1,10 @@ +[ + { + "label": "Standard & Drift Alignment", + "path": "/tenant/standards/list-standards" + }, + { + "label": "Templates", + "path": "/tenant/standards/list-standards/classic-standards" + } +] diff --git a/src/pages/tenant/standards/compare/index.js b/src/pages/tenant/standards/manage-drift/compare.js similarity index 86% rename from src/pages/tenant/standards/compare/index.js rename to src/pages/tenant/standards/manage-drift/compare.js index 75e7892d81cf..8ddc70251193 100644 --- a/src/pages/tenant/standards/compare/index.js +++ b/src/pages/tenant/standards/manage-drift/compare.js @@ -16,6 +16,7 @@ import { InputAdornment, } from "@mui/material"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; import { CheckCircle, Cancel, @@ -26,6 +27,7 @@ import { Close, Search, FactCheck, + PlayArrow, } from "@mui/icons-material"; import { ArrowLeftIcon } from "@mui/x-date-pickers"; import standards from "/src/data/standards.json"; @@ -40,6 +42,7 @@ import { Grid } from "@mui/system"; import DOMPurify from "dompurify"; import { ClockIcon } from "@heroicons/react/24/outline"; import ReactMarkdown from "react-markdown"; +import tabOptions from "./tabOptions.json"; const Page = () => { const router = useRouter(); @@ -171,6 +174,64 @@ const Page = () => { }); } }); + } else if ( + standardKey === "ConditionalAccessTemplate" && + Array.isArray(standardConfig) + ) { + // Process each ConditionalAccessTemplate item separately + standardConfig.forEach((templateItem, index) => { + const templateId = templateItem.TemplateList?.value; + if (templateId) { + const standardId = `standards.ConditionalAccessTemplate.${templateId}`; + const standardInfo = standards.find( + (s) => s.name === `standards.ConditionalAccessTemplate` + ); + + // Find the tenant's value for this specific template + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + const standardObject = currentTenantObj?.[standardId]; + const directStandardValue = standardObject?.Value; + let isCompliant = false; + + // For ConditionalAccessTemplate, the value is true if compliant, or an object with comparison data if not compliant + if (directStandardValue === true) { + isCompliant = true; + } else { + isCompliant = false; + } + + // Create a standardValue object that contains the template settings + const templateSettings = { + templateId, + Template: templateItem.TemplateList?.label || "Unknown Template", + }; + + allStandards.push({ + standardId, + standardName: `Conditional Access Template: ${ + templateItem.TemplateList?.label || templateId + }`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + } + : currentTenantStandard?.value, + standardValue: templateSettings, // Use the template settings object instead of true + complianceStatus: isCompliant ? "Compliant" : "Non-Compliant", + complianceDetails: + standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate?.templateName || "Standard Template", + templateActions: templateItem.action || [], + }); + } + }); } else { // Regular handling for other standards const standardId = `standards.${standardKey}`; @@ -316,16 +377,19 @@ const Page = () => { const filteredStandards = groupedStandards[category].filter((standard) => { const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; - const hasLicenseMissing = typeof tenantValue === "string" && tenantValue.startsWith("License Missing:"); - + const hasLicenseMissing = + typeof tenantValue === "string" && tenantValue.startsWith("License Missing:"); + const matchesFilter = filter === "all" || (filter === "compliant" && standard.complianceStatus === "Compliant") || (filter === "nonCompliant" && standard.complianceStatus === "Non-Compliant") || (filter === "nonCompliantWithLicense" && - standard.complianceStatus === "Non-Compliant" && !hasLicenseMissing) || + standard.complianceStatus === "Non-Compliant" && + !hasLicenseMissing) || (filter === "nonCompliantWithoutLicense" && - standard.complianceStatus === "Non-Compliant" && hasLicenseMissing); + standard.complianceStatus === "Non-Compliant" && + hasLicenseMissing); const matchesSearch = !searchQuery || @@ -352,74 +416,59 @@ const Page = () => { const reportingDisabledCount = comparisonData?.filter((standard) => standard.complianceStatus === "Reporting Disabled") .length || 0; - + // Calculate license-related metrics - const missingLicenseCount = comparisonData?.filter((standard) => { - const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; - return typeof tenantValue === "string" && tenantValue.startsWith("License Missing:"); - }).length || 0; - - const nonCompliantWithLicenseCount = comparisonData?.filter((standard) => { - const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; - return standard.complianceStatus === "Non-Compliant" && - !(typeof tenantValue === "string" && tenantValue.startsWith("License Missing:")); - }).length || 0; - - const nonCompliantWithoutLicenseCount = comparisonData?.filter((standard) => { - const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; - return standard.complianceStatus === "Non-Compliant" && - (typeof tenantValue === "string" && tenantValue.startsWith("License Missing:")); - }).length || 0; - + const missingLicenseCount = + comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return typeof tenantValue === "string" && tenantValue.startsWith("License Missing:"); + }).length || 0; + + const nonCompliantWithLicenseCount = + comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return ( + standard.complianceStatus === "Non-Compliant" && + !(typeof tenantValue === "string" && tenantValue.startsWith("License Missing:")) + ); + }).length || 0; + + const nonCompliantWithoutLicenseCount = + comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return ( + standard.complianceStatus === "Non-Compliant" && + typeof tenantValue === "string" && + tenantValue.startsWith("License Missing:") + ); + }).length || 0; + const compliancePercentage = allCount > 0 ? Math.round((compliantCount / (allCount - reportingDisabledCount || 1)) * 100) : 0; - + const missingLicensePercentage = allCount > 0 ? Math.round((missingLicenseCount / (allCount - reportingDisabledCount || 1)) * 100) : 0; - + // Combined score: compliance percentage + missing license percentage // This represents the total "addressable" compliance (compliant + could be compliant if licensed) const combinedScore = compliancePercentage + missingLicensePercentage; - return ( - - - - - - - - - { - templateDetails?.data?.filter((template) => template.GUID === templateId)?.[0] - ?.templateName - } - - - comparisonApi.refetch()}> - - - - - - {comparisonApi.data?.find( - (comparison) => comparison.tenantFilter === currentTenant - ) && ( - + // Prepare title and subtitle for HeaderedTabbedLayout + const title = + templateDetails?.data?.filter((template) => template.GUID === templateId)?.[0]?.templateName || + "Tenant Report"; + + const subtitle = [ + // Add compliance badges when template data is available (show even if no comparison data yet) + ...(templateDetails?.data?.filter((template) => template.GUID === templateId)?.[0] + ? [ + { + component: ( + @@ -436,7 +485,6 @@ const Page = () => { ? "warning" : "error" } - sx={{ ml: 2 }} /> { variant="outlined" size="small" color={ - combinedScore >= 80 - ? "success" - : combinedScore >= 60 - ? "warning" - : "error" + combinedScore >= 80 ? "success" : combinedScore >= 60 ? "warning" : "error" } /> - )} - - {templateDetails?.data?.filter((template) => template.GUID === templateId)?.[0] - ?.description && ( - theme.palette.primary.main, - textDecoration: "underline", - }, - color: "text.secondary", - fontSize: "0.875rem", - "& p": { - my: 0, - }, - mt: 2, - }} - dangerouslySetInnerHTML={{ - __html: DOMPurify.sanitize( - templateDetails?.data?.filter((template) => template.GUID === templateId)[0] - .description - ), - }} - /> - )} - + ), + }, + ] + : []), + // Add description if available + ...(templateDetails?.data?.filter((template) => template.GUID === templateId)?.[0]?.description + ? [ + { + component: ( + theme.palette.primary.main, + textDecoration: "underline", + }, + color: "text.secondary", + fontSize: "0.875rem", + "& p": { + my: 0, + }, + mt: 1, + }} + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize( + templateDetails?.data?.filter((template) => template.GUID === templateId)[0] + .description + ), + }} + /> + ), + }, + ] + : []), + ]; + + // Actions for the header + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + comparisonApi.refetch(); + templateDetails.refetch(); + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + + return ( + + {comparisonApi.isFetching && ( <> {[1, 2, 3].map((item) => ( @@ -567,7 +671,6 @@ const Page = () => { )} {!comparisonApi.isFetching && ( <> - { textDecoration: "none", }, }, - fontSize: "0.875rem", - lineHeight: 1.43, + fontSize: "0.875rem", + lineHeight: 1.43, "& p": { my: 0, }, @@ -1158,21 +1261,21 @@ const Page = () => { ))} )} - - - - + + + + ); }; diff --git a/src/pages/tenant/standards/manage-drift/history.js b/src/pages/tenant/standards/manage-drift/history.js new file mode 100644 index 000000000000..d7729022430b --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/history.js @@ -0,0 +1,348 @@ +import { useState, useEffect } from "react"; +import { + Box, + Stack, + Typography, + Button, + Chip, + Card, + CardContent, + CircularProgress, + Alert, + Link, +} from "@mui/material"; +import { + Timeline, + TimelineItem, + TimelineSeparator, + TimelineConnector, + TimelineContent, + TimelineDot, + TimelineOppositeContent, +} from "@mui/lab"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { useRouter } from "next/router"; +import { + Policy, + Sync, + PlayArrow, + Error as ErrorIcon, + Warning as WarningIcon, + Info as InfoIcon, + CheckCircle as SuccessIcon, + ExpandMore, +} from "@mui/icons-material"; +import tabOptions from "./tabOptions.json"; +import { useSettings } from "../../../../hooks/use-settings"; + +const Page = () => { + const router = useRouter(); + const { templateId } = router.query; + const [daysToLoad, setDaysToLoad] = useState(5); + const tenant = useSettings().currentTenant; + const [expandedMessages, setExpandedMessages] = useState(new Set()); + + // Toggle message expansion + const toggleMessageExpansion = (index) => { + const newExpanded = new Set(expandedMessages); + if (newExpanded.has(index)) { + newExpanded.delete(index); + } else { + newExpanded.add(index); + } + setExpandedMessages(newExpanded); + }; + + // Truncate message if too long + const truncateMessage = (message, maxLength = 256) => { + if (!message || message.length <= maxLength) { + return { text: message, isTruncated: false }; + } + return { + text: message.substring(0, maxLength) + "...", + fullText: message, + isTruncated: true, + }; + }; + + // Calculate date range for API call + const getDateRange = (days) => { + const endDate = new Date(); + const startDate = new Date(); + startDate.setDate(endDate.getDate() - days); + + return { + startDate: startDate.toISOString().split("T")[0].replace(/-/g, ""), + endDate: endDate.toISOString().split("T")[0].replace(/-/g, ""), + }; + }; + + const { startDate, endDate } = getDateRange(daysToLoad); + + const logsData = ApiGetCall({ + url: `/api/Listlogs?tenant=${tenant}&StartDate=${startDate}&EndDate=${endDate}&Filter=true`, + queryKey: `Listlogs-${tenant}-${startDate}-${endDate}`, + }); + + // Get severity icon and color + const getSeverityConfig = (severity) => { + const severityLower = severity?.toLowerCase(); + switch (severityLower) { + case "error": + return { icon: , color: "error", chipColor: "error" }; + case "warning": + return { icon: , color: "warning", chipColor: "warning" }; + case "info": + return { icon: , color: "info", chipColor: "info" }; + case "success": + return { icon: , color: "success", chipColor: "success" }; + default: + return { icon: , color: "grey", chipColor: "default" }; + } + }; + + // Format date for display + const formatDate = (dateString) => { + const date = new Date(dateString); + return { + time: date.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), + date: date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }), + }; + }; + + // Load more days + const handleLoadMore = () => { + setDaysToLoad((prev) => prev + 7); + }; + + // Actions for the ActionsMenu + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + logsData.refetch(); + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + // Sort logs by date (newest first) + const sortedLogs = logsData.data + ? [...logsData.data].sort((a, b) => new Date(b.DateTime) - new Date(a.DateTime)) + : []; + + return ( + + + + Activity Timeline + + This timeline shows the history of actions taken on this tenant, by CIPP for the last{" "} + {daysToLoad} days. + + + {logsData.isLoading && ( + + + + )} + + {logsData.isError && ( + Failed to load activity logs. Please try again. + )} + + {logsData.data && sortedLogs.length === 0 && ( + No activity logs found for the selected time period. + )} + + {logsData.data && sortedLogs.length > 0 && ( + + + + {sortedLogs.map((log, index) => { + const { icon, color, chipColor } = getSeverityConfig(log.Severity); + const { time, date } = formatDate(log.DateTime); + const { text, fullText, isTruncated } = truncateMessage(log.Message); + const isExpanded = expandedMessages.has(index); + + return ( + + + + {date} + + + {time} + + + + + + {icon} + + {index < sortedLogs.length - 1 && } + + + + + + + + {log.IP && ( + + )} + + + + + {isExpanded ? fullText : text} + + {isTruncated && ( + toggleMessageExpansion(index)} + sx={{ + mt: 0.5, + display: "block", + textAlign: "left", + fontSize: "0.75rem", + }} + > + {isExpanded ? "Show less" : "Show more"} + + )} + + + {log.User && ( + + User: {log.User} + + )} + + + + ); + })} + + + + + + + + )} + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/standards/manage-drift/index.js b/src/pages/tenant/standards/manage-drift/index.js new file mode 100644 index 000000000000..39a4f03b312a --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/index.js @@ -0,0 +1,968 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useRouter } from "next/router"; +import { + Check, + Warning, + ExpandMore, + CheckCircle, + Sync, + Block, + Science, + CheckBox, + Cancel, + Policy, + Error, + Info, + FactCheck, + PlayArrow, +} from "@mui/icons-material"; +import { Box, Stack, Typography, Button, Menu, MenuItem, Chip, SvgIcon } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useState } from "react"; +import { CippChartCard } from "/src/components/CippCards/CippChartCard"; +import { CippBannerListCard } from "/src/components/CippCards/CippBannerListCard"; +import { CippHead } from "/src/components/CippComponents/CippHead"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { useSettings } from "/src/hooks/use-settings"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; +import { useDialog } from "/src/hooks/use-dialog"; +import tabOptions from "./tabOptions.json"; +import standardsData from "/src/data/standards.json"; + +const ManageDriftPage = () => { + const router = useRouter(); + const { templateId } = router.query; + const userSettingsDefaults = useSettings(); + const tenantFilter = userSettingsDefaults.currentTenant || ""; + const [anchorEl, setAnchorEl] = useState({}); + const [bulkActionsAnchorEl, setBulkActionsAnchorEl] = useState(null); + const createDialog = useDialog(); + const [actionData, setActionData] = useState({ data: {}, ready: false }); + + // API calls for drift data + const driftApi = ApiGetCall({ + url: "/api/listTenantDrift", + data: { + TenantFilter: tenantFilter, + }, + queryKey: `TenantDrift-${tenantFilter}`, + }); + + // API call for available drift templates (for What If dropdown) + const standardsApi = ApiGetCall({ + url: "/api/listStandardTemplates", + data: { + type: "drift", + }, + queryKey: "ListDriftTemplates", + }); + + // API call for standards comparison (when templateId is available) + const comparisonApi = ApiGetCall({ + url: "/api/ListStandardsCompare", + data: { + TemplateId: templateId, + TenantFilter: tenantFilter, + CompareToStandard: true, + }, + queryKey: `StandardsCompare-${templateId}-${tenantFilter}`, + enabled: !!templateId && !!tenantFilter, + }); + + // Process drift data for chart - filter by current tenant and aggregate + const rawDriftData = driftApi.data || []; + const tenantDriftData = Array.isArray(rawDriftData) + ? rawDriftData.filter((item) => item.tenantFilter === tenantFilter) + : []; + + // Aggregate data across all standards for this tenant + const processedDriftData = tenantDriftData.reduce( + (acc, item) => { + acc.acceptedDeviationsCount += item.acceptedDeviationsCount || 0; + acc.currentDeviationsCount += item.currentDeviationsCount || 0; + acc.alignedCount += item.alignedCount || 0; + acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; + acc.deniedDeviationsCount += item.deniedDeviationsCount || 0; + + // Use the API's direct arrays instead of filtering allDeviations + if (item.currentDeviations && Array.isArray(item.currentDeviations)) { + acc.currentDeviations.push(...item.currentDeviations.filter((dev) => dev !== null)); + } + if (item.acceptedDeviations && Array.isArray(item.acceptedDeviations)) { + acc.acceptedDeviations.push(...item.acceptedDeviations.filter((dev) => dev !== null)); + } + if (item.customerSpecificDeviations && Array.isArray(item.customerSpecificDeviations)) { + acc.customerSpecificDeviationsList.push( + ...item.customerSpecificDeviations.filter((dev) => dev !== null) + ); + } + if (item.deniedDeviations && Array.isArray(item.deniedDeviations)) { + acc.deniedDeviationsList.push(...item.deniedDeviations.filter((dev) => dev !== null)); + } + + // Use the latest data collection timestamp + if ( + item.latestDataCollection && + (!acc.latestDataCollection || + new Date(item.latestDataCollection) > new Date(acc.latestDataCollection)) + ) { + acc.latestDataCollection = item.latestDataCollection; + } + + return acc; + }, + { + acceptedDeviationsCount: 0, + currentDeviationsCount: 0, + alignedCount: 0, + customerSpecificDeviations: 0, + deniedDeviationsCount: 0, + currentDeviations: [], + acceptedDeviations: [], + customerSpecificDeviationsList: [], + deniedDeviationsList: [], + latestDataCollection: null, + } + ); + + const chartLabels = [ + "Aligned Policies", + "Accepted Deviations", + "Current Deviations", + "Customer Specific Deviations", + ]; + const chartSeries = [ + processedDriftData.alignedCount || 0, + processedDriftData.acceptedDeviationsCount || 0, + processedDriftData.currentDeviationsCount || 0, + processedDriftData.customerSpecificDeviations || 0, + ]; + + // Transform currentDeviations into deviation items for display + const getDeviationIcon = (state) => { + switch (state?.toLowerCase()) { + case "current": + return ; + case "denied": + return ; + case "denieddelete": + case "denied - delete": + return ; + case "deniedremediate": + case "denied - remediate": + return ; + case "accepted": + return ; + case "customerspecific": + return ; + default: + return ; + } + }; + + const getDeviationColor = (state) => { + switch (state?.toLowerCase()) { + case "current": + return "warning.main"; + case "denied": + return "error.main"; + case "denieddelete": + case "denied - delete": + return "error.main"; + case "deniedremediate": + case "denied - remediate": + return "error.main"; + case "accepted": + return "success.main"; + case "customerspecific": + return "info.main"; + default: + return "warning.main"; + } + }; + + const getDeviationStatusText = (state) => { + switch (state?.toLowerCase()) { + case "current": + return "Current Deviation"; + case "denied": + return "Denied Deviation"; + case "denieddelete": + case "denied - delete": + return "Denied - Delete"; + case "deniedremediate": + case "denied - remediate": + return "Denied - Remediate"; + case "accepted": + return "Accepted Deviation"; + case "customerspecific": + return "Customer Specific"; + default: + return "Deviation"; + } + }; + + // Helper function to get pretty name from standards.json + const getStandardPrettyName = (standardName) => { + if (!standardName) return "Unknown Standard"; + + // Find the standard in standards.json by name + const standard = standardsData.find((s) => s.name === standardName); + if (standard && standard.label) { + return standard.label; + } + + // If not found in standards.json, try using standardDisplayName from the deviation object + // This will be handled in the createDeviationItems function + return null; + }; + + // Helper function to get description from standards.json + const getStandardDescription = (standardName) => { + if (!standardName) return null; + + // Find the standard in standards.json by name + const standard = standardsData.find((s) => s.name === standardName); + if (standard) { + return standard.helpText || standard.docsDescription || standard.executiveText || null; + } + + return null; + }; + + // Helper function to create deviation items + const createDeviationItems = (deviations, statusOverride = null) => { + return (deviations || []).map((deviation, index) => { + // Prioritize standardDisplayName from drift data (which has user-friendly names for templates) + // then fallback to standards.json lookup, then raw name + const prettyName = + deviation.standardDisplayName || + getStandardPrettyName(deviation.standardName) || + deviation.standardName || + "Unknown Standard"; + + // Get description from standards.json first, then fallback to standardDescription from deviation + const description = + getStandardDescription(deviation.standardName) || + deviation.standardDescription || + "No description available"; + + return { + id: index + 1, + cardLabelBox: { + cardLabelBoxHeader: getDeviationIcon( + statusOverride || deviation.Status || deviation.state + ), + }, + text: prettyName, + subtext: description, + statusColor: getDeviationColor(statusOverride || deviation.Status || deviation.state), + statusText: getDeviationStatusText(statusOverride || deviation.Status || deviation.state), + standardName: deviation.standardName, // Store the original standardName for action handlers + receivedValue: deviation.receivedValue, // Store the original receivedValue for action handlers + expectedValue: deviation.expectedValue, // Store the original expectedValue for action handlers + originalDeviation: deviation, // Store the complete original deviation object for reference + propertyItems: [ + { label: "Standard Name", value: prettyName }, + { label: "Description", value: description }, + { label: "Expected Value", value: deviation.expectedValue || "N/A" }, + { label: "Current Value", value: deviation.receivedValue || "N/A" }, + { + label: "Status", + value: getDeviationStatusText(statusOverride || deviation.Status || deviation.state), + }, + { + label: "Reason", + value: deviation.Reason || "N/A", + }, + { + label: "User", + value: deviation.lastChangedByUser || "N/A", + }, + { + label: "Last Updated", + value: processedDriftData.latestDataCollection + ? new Date(processedDriftData.latestDataCollection).toLocaleString() + : "N/A", + }, + ].filter((item) => item.value !== "N/A" && item.value !== "No description available"), // Filter out N/A values and empty descriptions + }; + }); + }; + + const deviationItems = createDeviationItems(processedDriftData.currentDeviations); + const acceptedDeviationItems = createDeviationItems( + processedDriftData.acceptedDeviations, + "accepted" + ); + const customerSpecificDeviationItems = createDeviationItems( + processedDriftData.customerSpecificDeviationsList, + "customerspecific" + ); + const deniedDeviationItems = createDeviationItems( + processedDriftData.deniedDeviationsList, + "denied" + ); + + const handleMenuClick = (event, itemId) => { + setAnchorEl((prev) => ({ ...prev, [itemId]: event.currentTarget })); + }; + + const handleMenuClose = (itemId) => { + setAnchorEl((prev) => ({ ...prev, [itemId]: null })); + }; + + const handleAction = (action, itemId) => { + const deviation = processedDriftData.currentDeviations[itemId - 1]; + if (!deviation) return; + + let status; + let actionText; + switch (action) { + case "accept-customer-specific": + status = "CustomerSpecific"; + actionText = "accept as customer specific"; + break; + case "accept": + status = "Accepted"; + actionText = "accept"; + break; + case "deny-delete": + status = "DeniedDelete"; + actionText = "deny and delete"; + break; + case "deny-remediate": + status = "DeniedRemediate"; + actionText = "deny and remediate to align with template"; + break; + default: + return; + } + + // Set action data for CippApiDialog + setActionData({ + data: { + deviations: [ + { + standardName: deviation.standardName, + status: status, + receivedValue: deviation.receivedValue, + }, + ], + TenantFilter: tenantFilter, + }, + action: { + text: actionText, + type: "single", + }, + ready: true, + }); + + createDialog.handleOpen(); + handleMenuClose(itemId); + }; + + const handleDeviationAction = (action, deviation) => { + if (!deviation) return; + + let status; + let actionText; + switch (action) { + case "accept-customer-specific": + status = "CustomerSpecific"; + actionText = "accept as customer specific"; + break; + case "accept": + status = "Accepted"; + actionText = "accept"; + break; + case "deny": + status = "Denied"; + actionText = "deny"; + break; + case "deny-delete": + status = "DeniedDelete"; + actionText = "deny and delete"; + break; + case "deny-remediate": + status = "DeniedRemediate"; + actionText = "deny and remediate to align with template"; + break; + default: + return; + } + + // Set action data for CippApiDialog + setActionData({ + data: { + deviations: [ + { + standardName: deviation.standardName, // Use the standardName from the original deviation data + status: status, + receivedValue: deviation.receivedValue, + }, + ], + TenantFilter: tenantFilter, + }, + action: { + text: actionText, + type: "single", + }, + ready: true, + }); + + createDialog.handleOpen(); + }; + + const handleBulkAction = (action) => { + if ( + !processedDriftData.currentDeviations || + processedDriftData.currentDeviations.length === 0 + ) { + setBulkActionsAnchorEl(null); + return; + } + + let status; + let actionText; + switch (action) { + case "accept-all-customer-specific": + status = "CustomerSpecific"; + actionText = "accept all deviations as customer specific"; + break; + case "accept-all": + status = "Accepted"; + actionText = "accept all deviations"; + break; + case "deny-all": + status = "Denied"; + actionText = "deny all deviations"; + break; + case "deny-all-delete": + status = "DeniedDelete"; + actionText = "deny all deviations and delete"; + break; + case "deny-all-remediate": + status = "DeniedRemediate"; + actionText = "deny all deviations and remediate to align with template"; + break; + default: + setBulkActionsAnchorEl(null); + return; + } + + const deviations = processedDriftData.currentDeviations.map((deviation) => ({ + standardName: deviation.standardName, + status: status, + receivedValue: deviation.receivedValue, + })); + + // Set action data for CippApiDialog + setActionData({ + data: { + deviations: deviations, + TenantFilter: tenantFilter, + receivedValues: deviations.map((d) => d.receivedValue), + }, + action: { + text: actionText, + type: "bulk", + count: deviations.length, + }, + ready: true, + }); + + createDialog.handleOpen(); + setBulkActionsAnchorEl(null); + }; + + const handleRemoveDriftCustomization = () => { + // Set action data for CippApiDialog + setActionData({ + data: { + RemoveDriftCustomization: true, + TenantFilter: tenantFilter, + }, + action: { + text: "remove all drift customizations", + type: "reset", + }, + ready: true, + }); + + createDialog.handleOpen(); + setBulkActionsAnchorEl(null); + }; + + // Actions for the ActionsMenu + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + driftApi.refetch(); + standardsApi.refetch(); + if (templateId) { + comparisonApi.refetch(); + } + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + + // Add action buttons to each deviation item + const deviationItemsWithActions = deviationItems.map((item) => { + // Check if this is a template that supports delete action + const supportsDelete = + item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate"); + + return { + ...item, + actionButton: ( + <> + + handleMenuClose(item.id)} + > + handleAction("accept-customer-specific", item.id)}> + + Accept Deviation - Customer Specific + + handleAction("accept", item.id)}> + + Accept Deviation + + {supportsDelete && ( + handleAction("deny-delete", item.id)}> + + Deny Deviation - Delete Policy + + )} + handleAction("deny-remediate", item.id)}> + + Deny Deviation - Remediate to align with template + + + + ), + }; + }); + + // Add action buttons to accepted deviation items + const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => { + // Check if this is a template that supports delete action + const supportsDelete = + item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate"); + + return { + ...item, + actionButton: ( + <> + + handleMenuClose(`accepted-${item.id}`)} + > + {supportsDelete && ( + handleDeviationAction("deny-delete", item)}> + + Deny - Delete Policy + + )} + handleDeviationAction("deny-remediate", item)}> + + Deny - Remediate to align with template + + handleDeviationAction("accept-customer-specific", item)}> + + Accept - Customer Specific + + + + ), + }; + }); + + // Add action buttons to customer specific deviation items + const customerSpecificDeviationItemsWithActions = customerSpecificDeviationItems.map((item) => { + // Check if this is a template that supports delete action + const supportsDelete = + item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate"); + + return { + ...item, + actionButton: ( + <> + + handleMenuClose(`customer-${item.id}`)} + > + {supportsDelete && ( + handleDeviationAction("deny-delete", item)}> + + Deny - Delete + + )} + handleDeviationAction("deny-remediate", item)}> + + Deny - Remediate to align with template + + handleDeviationAction("accept", item)}> + + Accept + + + + ), + }; + }); + + // Add action buttons to denied deviation items + const deniedDeviationItemsWithActions = deniedDeviationItems.map((item) => ({ + ...item, + actionButton: ( + <> + + handleMenuClose(`denied-${item.id}`)} + > + handleDeviationAction("accept", item)}> + + Accept + + handleDeviationAction("accept-customer-specific", item)}> + + Accept - Customer Specific + + + + ), + })); + + // Calculate compliance metrics for badges + const totalPolicies = + processedDriftData.alignedCount + + processedDriftData.currentDeviationsCount + + processedDriftData.acceptedDeviationsCount + + processedDriftData.customerSpecificDeviations; + + const compliancePercentage = + totalPolicies > 0 ? Math.round((processedDriftData.alignedCount / totalPolicies) * 100) : 0; + + const missingLicensePercentage = 0; // This would need to be calculated from actual license data + const combinedScore = compliancePercentage + missingLicensePercentage; + + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + // Add compliance badges when data is available + ...(totalPolicies > 0 + ? [ + { + component: ( + + + + + } + label={`${compliancePercentage}% Compliant`} + variant="outlined" + size="small" + color={ + compliancePercentage === 100 + ? "success" + : compliancePercentage >= 50 + ? "warning" + : "error" + } + /> + = 80 ? "success" : combinedScore >= 60 ? "warning" : "error" + } + /> + + ), + }, + ] + : []), + ]; + + return ( + + + + {/* Check if there's no drift data */} + {!driftApi.isFetching && + (!rawDriftData || rawDriftData.length === 0 || tenantDriftData.length === 0) ? ( + + + No Drift Data Available + + + This standard does not have any drift entries, or it is not a drift compatible + standard. + + + To enable drift monitoring for this tenant, please ensure: + + + + A drift template has been created and assigned to this tenant + + + The standard is configured for drift monitoring + + + Drift data collection has been completed for this tenant + + + + ) : ( + + {/* Left side - Chart */} + + + + + {/* Right side - Deviation Management */} + + + {/* Current Deviations Section */} + + {/* Header with bulk actions */} + + Current Deviations + + {/* Bulk Actions Dropdown */} + + setBulkActionsAnchorEl(null)} + > + handleBulkAction("accept-all-customer-specific")}> + + Accept All Deviations - Customer Specific + + handleBulkAction("accept-all")}> + + Accept All Deviations + + {/* Only show delete option if there are template deviations that support deletion */} + {processedDriftData.currentDeviations.some( + (deviation) => + deviation.standardName?.includes("ConditionalAccessTemplate") || + deviation.standardName?.includes("IntuneTemplate") + ) && ( + handleBulkAction("deny-all-delete")}> + + Deny All Deviations - Delete + + )} + handleBulkAction("deny-all-remediate")}> + + Deny All Deviations - Remediate to align with template + + + + Remove Drift Customization + + + + + + + + {/* Accepted Deviations Section */} + {acceptedDeviationItemsWithActions.length > 0 && ( + + + Accepted Deviations + + + + )} + + {/* Customer Specific Deviations Section */} + {customerSpecificDeviationItemsWithActions.length > 0 && ( + + + Accepted Deviations - Customer Specific + + + + )} + + {/* Denied Deviations Section */} + {deniedDeviationItemsWithActions.length > 0 && ( + + + Denied Deviations + + + + )} + + + + )} + + {actionData.ready && ( + + )} + + ); +}; + +ManageDriftPage.getLayout = (page) => {page}; + +export default ManageDriftPage; diff --git a/src/pages/tenant/standards/manage-drift/policies-deployed.js b/src/pages/tenant/standards/manage-drift/policies-deployed.js new file mode 100644 index 000000000000..909b19a733f4 --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/policies-deployed.js @@ -0,0 +1,376 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import { + Policy, + Security, + AdminPanelSettings, + Devices, + ExpandMore, + Sync, + PlayArrow, +} from "@mui/icons-material"; +import { + Box, + Stack, + Typography, + Accordion, + AccordionSummary, + AccordionDetails, + Chip, +} from "@mui/material"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import tabOptions from "./tabOptions.json"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { CippHead } from "/src/components/CippComponents/CippHead"; +import { ApiGetCall } from "/src/api/ApiCall"; +import standardsData from "/src/data/standards.json"; + +const PoliciesDeployedPage = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { templateId } = router.query; + const tenantFilter = router.query.tenantFilter || userSettingsDefaults.tenantFilter; + + // API call to get standards template data + const standardsApi = ApiGetCall({ + url: "/api/listStandardTemplates", + queryKey: "ListStandardsTemplates-Drift", + }); + + // API call to get standards comparison data + const comparisonApi = ApiGetCall({ + url: "/api/ListStandardsCompare", + data: { + TemplateId: templateId, + TenantFilter: tenantFilter, + CompareToStandard: true, + }, + queryKey: `StandardsCompare-${templateId}-${tenantFilter}`, + enabled: !!templateId && !!tenantFilter, + }); + + // API call to get drift data for deviation statuses + const driftApi = ApiGetCall({ + url: "/api/listTenantDrift", + data: { + tenantFilter: tenantFilter, + standardsId: templateId, + }, + queryKey: `TenantDrift-${templateId}-${tenantFilter}`, + enabled: !!templateId && !!tenantFilter, + }); + + // Find the current template from standards data + const currentTemplate = (standardsApi.data || []).find( + (template) => template.GUID === templateId + ); + const templateStandards = currentTemplate?.standards || {}; + const comparisonData = comparisonApi.data?.[0] || {}; + + // Helper function to get status from comparison data with deviation status + const getStatus = (standardKey, templateValue = null, templateType = null) => { + const comparisonKey = `standards.${standardKey}`; + const value = comparisonData[comparisonKey]?.Value; + + if (value === true) { + return "Deployed"; + } else { + // Check if there's drift data for this standard to get the deviation status + const driftData = driftApi.data || []; + + // For templates, we need to match against the full template path + let searchKeys = [ + standardKey, + `standards.${standardKey}`, + ]; + + // Add template-specific search keys + if (templateValue && templateType) { + searchKeys.push( + `standards.${templateType}.${templateValue}`, + `${templateType}.${templateValue}`, + templateValue + ); + } + + const deviation = driftData.find(item => + searchKeys.some(key => + item.standardName === key || + item.policyName === key || + item.standardName?.includes(key) || + item.policyName?.includes(key) + ) + ); + + if (deviation && deviation.Status) { + return `Deviation - ${deviation.Status}`; + } + + return "Deviation - New"; + } + }; + + // Helper function to get display name from drift data + const getDisplayNameFromDrift = (standardKey, templateValue = null, templateType = null) => { + const driftData = driftApi.data || []; + + // For templates, we need to match against the full template path + let searchKeys = [ + standardKey, + `standards.${standardKey}`, + ]; + + // Add template-specific search keys + if (templateValue && templateType) { + searchKeys.push( + `standards.${templateType}.${templateValue}`, + `${templateType}.${templateValue}`, + templateValue + ); + } + + const deviation = driftData.find(item => + searchKeys.some(key => + item.standardName === key || + item.policyName === key || + item.standardName?.includes(key) || + item.policyName?.includes(key) + ) + ); + + return deviation?.standardDisplayName || null; + }; + + // Helper function to get last refresh date + const getLastRefresh = (standardKey) => { + const comparisonKey = `standards.${standardKey}`; + const lastRefresh = comparisonData[comparisonKey]?.LastRefresh; + return lastRefresh ? new Date(lastRefresh).toLocaleDateString() : "N/A"; + }; + + // Helper function to get standard name from standards.json + const getStandardName = (standardKey) => { + const standardName = `standards.${standardKey}`; + const standard = standardsData.find(s => s.name === standardName); + return standard?.label || standardKey.replace(/([A-Z])/g, " $1").trim(); + }; + + // Helper function to get template label from standards API data + const getTemplateLabel = (templateValue, templateType) => { + if (!templateValue || !currentTemplate) return "Unknown Template"; + + // Search through all templates in the current template data + const allTemplates = currentTemplate.standards || {}; + + // Look for the template in the specific type array + if (allTemplates[templateType] && Array.isArray(allTemplates[templateType])) { + const template = allTemplates[templateType].find(t => t.TemplateList?.value === templateValue); + if (template?.TemplateList?.label) { + return template.TemplateList.label; + } + } + + // If not found in the specific type, search through all template types + for (const [key, templates] of Object.entries(allTemplates)) { + if (Array.isArray(templates)) { + const template = templates.find(t => t.TemplateList?.value === templateValue); + if (template?.TemplateList?.label) { + return template.TemplateList.label; + } + } + } + + return "Unknown Template"; + }; + + // Process Security Standards (everything NOT IntuneTemplates or ConditionalAccessTemplates) + const deployedStandards = Object.entries(templateStandards) + .filter(([key]) => key !== "IntuneTemplate" && key !== "ConditionalAccessTemplate") + .map(([key, value], index) => ({ + id: index + 1, + name: getStandardName(key), + category: "Security Standard", + status: getStatus(key), + lastModified: getLastRefresh(key), + standardKey: key, + })); + + // Process Intune Templates + const intunePolices = (templateStandards.IntuneTemplate || []).map((template, index) => { + const standardKey = `IntuneTemplate.${template.TemplateList?.value}`; + const driftDisplayName = getDisplayNameFromDrift(standardKey, template.TemplateList?.value, "IntuneTemplate"); + const templateLabel = getTemplateLabel(template.TemplateList?.value, "IntuneTemplate"); + + return { + id: index + 1, + name: driftDisplayName || `Intune - ${templateLabel}`, + category: "Intune Template", + platform: "Multi-Platform", + status: getStatus(standardKey, template.TemplateList?.value, "IntuneTemplate"), + lastModified: getLastRefresh(standardKey), + assignedGroups: template.AssignTo || "N/A", + templateValue: template.TemplateList?.value, + }; + }); + + // Process Conditional Access Templates + const conditionalAccessPolicies = (templateStandards.ConditionalAccessTemplate || []).map( + (template, index) => { + const standardKey = `ConditionalAccessTemplate.${template.TemplateList?.value}`; + const driftDisplayName = getDisplayNameFromDrift(standardKey, template.TemplateList?.value, "ConditionalAccessTemplate"); + const templateLabel = getTemplateLabel(template.TemplateList?.value, "ConditionalAccessTemplate"); + + return { + id: index + 1, + name: driftDisplayName || `Conditional Access - ${templateLabel}`, + state: template.state || "Unknown", + conditions: "Conditional Access Policy", + controls: "Access Control", + lastModified: getLastRefresh(standardKey), + status: getStatus(standardKey, template.TemplateList?.value, "ConditionalAccessTemplate"), + templateValue: template.TemplateList?.value, + }; + } + ); + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + standardsApi.refetch(); + comparisonApi.refetch(); + driftApi.refetch(); + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + return ( + + + + + {/* Standards Section */} + + }> + + + Security Standards + + + + + + + + + {/* Intune Policies Section */} + + }> + + + Intune Policies + + + + + + + + + {/* Conditional Access Policies Section */} + + }> + + + Conditional Access Policies + + + + + + + + + + + ); +}; + +PoliciesDeployedPage.getLayout = (page) => {page}; + +export default PoliciesDeployedPage; diff --git a/src/pages/tenant/standards/manage-drift/recover-policies.js b/src/pages/tenant/standards/manage-drift/recover-policies.js new file mode 100644 index 000000000000..32b2a059a474 --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/recover-policies.js @@ -0,0 +1,227 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import { Policy, Restore, ExpandMore, Sync, PlayArrow } from "@mui/icons-material"; +import { + Box, + Stack, + Typography, + Accordion, + AccordionSummary, + AccordionDetails, + Chip, + Button, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import tabOptions from "./tabOptions.json"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { CippHead } from "/src/components/CippComponents/CippHead"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; + +const RecoverPoliciesPage = () => { + const router = useRouter(); + const { templateId } = router.query; + const [selectedPolicies, setSelectedPolicies] = useState([]); + + const formControl = useForm({ mode: "onChange" }); + + const selectedBackup = formControl.watch("backupDateTime"); + + // Mock data for policies in selected backup - replace with actual API call + const backupPolicies = [ + { + id: 1, + name: "Multi-Factor Authentication Policy", + type: "Conditional Access", + lastModified: "2024-01-15", + settings: "Require MFA for all users", + }, + { + id: 2, + name: "Password Policy Standard", + type: "Security Standard", + lastModified: "2024-01-10", + settings: "14 character minimum, complexity required", + }, + { + id: 3, + name: "Device Compliance Policy", + type: "Intune Policy", + lastModified: "2024-01-08", + settings: "Require encryption, PIN/Password", + }, + ]; + + // Recovery API call + const recoverApi = ApiPostCall({ + relatedQueryKeys: ["ListBackupPolicies", "ListPolicyBackups"], + }); + + const handleRecover = () => { + if (selectedPolicies.length === 0 || !selectedBackup) { + return; + } + + recoverApi.mutate({ + url: "/api/RecoverPolicies", + data: { + templateId, + backupDateTime: selectedBackup, + policyIds: selectedPolicies.map((policy) => policy.id), + }, + }); + }; + + // Actions for the ActionsMenu + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + // Refresh any relevant data here + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + return ( + + + + + {/* Backup Date Selection */} + + }> + + + Select Backup Date & Time + + + + + + { + const date = new Date(option.dateTime); + return `${date.toLocaleDateString()} @ ${date.toLocaleTimeString()} (${ + option.policyCount + } policies)`; + }, + valueField: "dateTime", + }} + required={true} + validators={{ + validate: (value) => !!value || "Please select a backup date & time", + }} + /> + + + + + + {/* Recovery Results */} + + + {/* Backup Policies Section */} + {selectedBackup && ( + + }> + + + Policies in Selected Backup + + + + + + + + Select policies to recover from backup:{" "} + {new Date(selectedBackup).toLocaleString()} + + + + setSelectedPolicies(selectedRows)} + /> + + + + )} + + + + ); +}; + +RecoverPoliciesPage.getLayout = (page) => {page}; + +export default RecoverPoliciesPage; diff --git a/src/pages/tenant/standards/manage-drift/tabOptions.json b/src/pages/tenant/standards/manage-drift/tabOptions.json new file mode 100644 index 000000000000..50b3adfd16dc --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/tabOptions.json @@ -0,0 +1,18 @@ +[ + { + "label": "Manage Drift", + "path": "/tenant/standards/manage-drift" + }, + { + "label": "Policies and Settings Deployed", + "path": "/tenant/standards/manage-drift/policies-deployed" + }, + { + "label": "History", + "path": "/tenant/standards/manage-drift/history" + }, + { + "label": "Tenant Report", + "path": "/tenant/standards/manage-drift/compare" + } +] diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index c5f32f98e8f0..0262b6a6e1ad 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -29,7 +29,18 @@ const Page = () => { const [updatedAt, setUpdatedAt] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [currentStep, setCurrentStep] = useState(0); + const [hasDriftConflict, setHasDriftConflict] = useState(false); const initialStandardsRef = useRef({}); + + // Check if this is drift mode + const isDriftMode = router.query.type === "drift"; + + // Set drift mode flag in form when in drift mode + useEffect(() => { + if (isDriftMode) { + formControl.setValue("isDriftTemplate", true); + } + }, [isDriftMode, formControl]); // Watch form values to check valid configuration const watchForm = useWatch({ control: formControl.control }); @@ -45,7 +56,7 @@ const Page = () => { useEffect(() => { const stepsStatus = { step1: !!_.get(watchForm, "templateName"), - step2: _.get(watchForm, "tenantFilter", []).length > 0, + step2: isDriftMode || _.get(watchForm, "tenantFilter", []).length > 0, // Skip tenant requirement for drift mode step3: Object.keys(selectedStandards).length > 0, step4: _.get(watchForm, "standards") && @@ -60,7 +71,7 @@ const Page = () => { const completedSteps = Object.values(stepsStatus).filter(Boolean).length; setCurrentStep(completedSteps); - }, [selectedStandards, watchForm]); + }, [selectedStandards, watchForm, isDriftMode]); // Handle route change events const handleRouteChange = useCallback( @@ -251,10 +262,11 @@ const Page = () => { }; // Determine if save button should be disabled based on configuration - const isSaveDisabled = - !_.get(watchForm, "tenantFilter") || - !_.get(watchForm, "tenantFilter").length || - currentStep < 3; + const isSaveDisabled = isDriftMode + ? currentStep < 3 || hasDriftConflict // For drift mode, only require steps 1, 3, and 4 (skip tenant requirement) and no drift conflicts + : (!_.get(watchForm, "tenantFilter") || + !_.get(watchForm, "tenantFilter").length || + currentStep < 3); const actions = []; @@ -309,7 +321,10 @@ const Page = () => { sx={{ mb: 3 }} > - {editMode ? "Edit Standards Template" : "Add Standards Template"} + {editMode + ? (isDriftMode ? "Edit Drift Template" : "Edit Standards Template") + : (isDriftMode ? "Add Drift Template" : "Add Standards Template") + } - - } - /> + <> + + + + } + /> + + ); }; diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 832201a4898d..2b3a9bb7f4e7 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -196,6 +196,15 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return isText ? data : data; } + // Handle log message field + const messageFields = ["Message"]; + if (messageFields.includes(cellName)) { + if (typeof data === "string" && data.length > 120) { + return isText ? data : `${data.substring(0, 120)}...`; + } + return isText ? data : data; + } + if (cellName === "alignmentScore" || cellName === "combinedAlignmentScore") { // Handle alignment score, return a percentage with a label return isText ? ( @@ -296,7 +305,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr ? data.join(", ") : renderChipList( data.map((item, key) => { - const itemText = item?.label ? item.label : item; + const itemText = item?.label !== undefined ? item.label : item; let icon = null; if (item?.type === "Group") { @@ -321,7 +330,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr }) ); } else { - const itemText = data?.label ? data.label : data; + const itemText = data?.label !== undefined ? data.label : data; let icon = null; if (data?.type === "Group") { @@ -337,7 +346,6 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr ); } - return isText ? itemText : ; } } @@ -359,6 +367,26 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr )); } } + if (cellName === "standardType") { + return isText ? ( + data + ) : ( + + ); + } + + if (cellName === "type" && data === "drift") { + return isText ? ( + "Drift Standard" + ) : ( + + ); + } if (cellName === "ClientId" || cellName === "role") { return isText ? data : ;