From aa6aae8b546c0ebcbb94c397955f00ef8b8567ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 17 Jul 2025 00:08:32 +0200 Subject: [PATCH 01/69] Feat:: Add Mailbox Activity report page --- src/layouts/config.js | 20 +-- .../email/reports/mailbox-activity/index.js | 152 ++++++++++++++++++ 2 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 src/pages/email/reports/mailbox-activity/index.js diff --git a/src/layouts/config.js b/src/layouts/config.js index f0991ef3fff5..3b2582e27387 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -256,11 +256,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 +266,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 +472,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 +702,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/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; From a9cbbdabd2269508af8411e58f91f1bd552373f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 17 Jul 2025 00:33:26 +0200 Subject: [PATCH 02/69] fix copy paste bug --- src/components/CippComponents/CippExchangeActions.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx index 5e01fc542588..f660349e690e 100644 --- a/src/components/CippComponents/CippExchangeActions.jsx +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -50,7 +50,7 @@ export const CippExchangeActions = () => { type: "POST", icon: , url: "/api/ExecConvertMailbox", - data: { ID: "userPrincipalName" }, + data: { ID: "UPN" }, fields: [ { type: "radio", From eae248d38249f492a356d1a8d47469b764606606 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 17 Jul 2025 21:52:21 +0200 Subject: [PATCH 03/69] extra handling for empty labels --- src/utils/get-cipp-formatting.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 832201a4898d..8f0e8ce2632f 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -296,7 +296,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 +321,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 +337,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr ); } - + console.log("itemText", itemText, "icon", icon); return isText ? itemText : ; } } From b040d6dabc6fae4e05a6b832a479011dfa3709f1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:54:40 +0200 Subject: [PATCH 04/69] fix minor formatting bug --- src/utils/get-cipp-formatting.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 8f0e8ce2632f..74084df57263 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -337,7 +337,6 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr ); } - console.log("itemText", itemText, "icon", icon); return isText ? itemText : ; } } From d620ea16a2e660a1a731015eeb8d033809b235fb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 22 Jul 2025 12:42:36 -0400 Subject: [PATCH 05/69] auditlog updates --- .../CippComponents/CippAuditLogDetails.jsx | 294 ++++++++++++++++++ .../CippComponents/CippGeoLocation.jsx | 31 +- src/hooks/use-guid-resolver.js | 136 ++++++++ .../tenant/administration/audit-logs/index.js | 247 +++++++++------ .../tenant/administration/audit-logs/log.js | 8 +- .../audit-logs/search-results.js | 74 +++++ .../administration/audit-logs/searches.js | 267 ++++++++++++++++ .../administration/audit-logs/tabOptions.json | 10 + 8 files changed, 959 insertions(+), 108 deletions(-) create mode 100644 src/components/CippComponents/CippAuditLogDetails.jsx create mode 100644 src/hooks/use-guid-resolver.js create mode 100644 src/pages/tenant/administration/audit-logs/search-results.js create mode 100644 src/pages/tenant/administration/audit-logs/searches.js create mode 100644 src/pages/tenant/administration/audit-logs/tabOptions.json diff --git a/src/components/CippComponents/CippAuditLogDetails.jsx b/src/components/CippComponents/CippAuditLogDetails.jsx new file mode 100644 index 000000000000..c440b33b2150 --- /dev/null +++ b/src/components/CippComponents/CippAuditLogDetails.jsx @@ -0,0 +1,294 @@ +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, isLoadingGuids, resolveGuids, isGuid } = useGuidResolver(); + + useEffect(() => { + if (row) { + resolveGuids(row); + if (row.auditData) { + resolveGuids(row.auditData); + } + } + }, [row?.id, resolveGuids]); // Dependencies for when to resolve GUIDs + + // Function to replace GUIDs in strings with resolved names + const replaceGuidsInString = (str) => { + if (typeof str !== "string") return str; + + 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) || []; + + if (guidsInString.length === 0) return str; + + let result = str; + let hasResolvedGuids = false; + + guidsInString.forEach((guid) => { + if (guidMapping[guid]) { + result = result.replace(new RegExp(guid, "gi"), guidMapping[guid]); + hasResolvedGuids = true; + } + }); + + // If we have resolved GUIDs, return a tooltip showing original and resolved + if (hasResolvedGuids) { + return ( + + {result} + + ); + } + + // If we have unresolved GUIDs and currently loading + if (guidsInString.length > 0 && 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 ( + 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) => { + if (guidMapping[guidValue]) { + return ( + + {guidMapping[guidValue]} + + ); + } else if (isLoadingGuids) { + return ( +
+ + {guidValue} +
+ ); + } else { + 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/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/hooks/use-guid-resolver.js b/src/hooks/use-guid-resolver.js new file mode 100644 index 000000000000..ebb8722f3e3b --- /dev/null +++ b/src/hooks/use-guid-resolver.js @@ -0,0 +1,136 @@ +import { useState, useCallback, useRef } 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 recursively scan an object for GUIDs +const findGuids = (obj, guidsSet = new Set()) => { + if (!obj) return guidsSet; + + if (typeof obj === "string") { + // Check if the entire string is a GUID + if (isGuid(obj)) { + guidsSet.add(obj); + } else { + // Extract GUIDs embedded within longer strings + const embeddedGuids = extractGuidsFromString(obj); + embeddedGuids.forEach((guid) => guidsSet.add(guid)); + } + } else if (Array.isArray(obj)) { + obj.forEach((item) => findGuids(item, guidsSet)); + } else if (typeof obj === "object") { + Object.values(obj).forEach((value) => findGuids(value, guidsSet)); + } + + return guidsSet; +}; + +export const useGuidResolver = () => { + const tenantFilter = useSettings().currentTenant; + + // GUID resolution state + const [guidMapping, setGuidMapping] = useState({}); + 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 lastRequestTimeRef = useRef(0); + + // Setup API call for directory objects resolution + const directoryObjectsMutation = ApiPostCall({ + relatedQueryKeys: ["directoryObjects"], + onResult: (data) => { + if (data && Array.isArray(data.value)) { + const newMapping = {}; + + // Process the returned results + data.value.forEach((item) => { + if (item.id && (item.displayName || item.userPrincipalName || item.mail)) { + newMapping[item.id] = item.displayName || item.userPrincipalName || item.mail; + } + }); + + // 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 them to the notFoundGuids set + if (notReturned.length > 0) { + notReturned.forEach((guid) => notFoundGuidsRef.current.add(guid)); + } + + setGuidMapping((prevMapping) => ({ ...prevMapping, ...newMapping })); + pendingGuidsRef.current = []; + setIsLoadingGuids(false); + } + }, + }); + + // Function to handle resolving GUIDs + const resolveGuids = useCallback( + (objectToScan) => { + const guidsSet = findGuids(objectToScan); + + if (guidsSet.size === 0) return; + + const guidsArray = Array.from(guidsSet); + const notResolvedGuids = guidsArray.filter( + (guid) => !guidMapping[guid] && !notFoundGuidsRef.current.has(guid) + ); + + if (notResolvedGuids.length === 0) return; + + const allPendingGuids = [...new Set([...pendingGuidsRef.current, ...notResolvedGuids])]; + pendingGuidsRef.current = allPendingGuids; + setIsLoadingGuids(true); + + // Implement throttling - only send a new request every 2 seconds + const now = Date.now(); + if (now - lastRequestTimeRef.current < 2000) { + return; + } + + 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) { + directoryObjectsMutation.mutate({ + url: "/api/ListDirectoryObjects", + data: { + tenantFilter: tenantFilter, + ids: guidsToSend, + $select: "id,displayName,userPrincipalName,mail", + }, + }); + } else { + setIsLoadingGuids(false); + } + }, + [guidMapping, tenantFilter] // Only depend on guidMapping and tenantFilter + ); + + return { + guidMapping, + isLoadingGuids, + resolveGuids, + isGuid, + }; +}; diff --git a/src/pages/tenant/administration/audit-logs/index.js b/src/pages/tenant/administration/audit-logs/index.js index 1c0809755fda..faba994f0571 100644 --- a/src/pages/tenant/administration/audit-logs/index.js +++ b/src/pages/tenant/administration/audit-logs/index.js @@ -1,19 +1,20 @@ 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 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 { EyeIcon, MagnifyingGlassIcon } 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 Alerts Tab Configuration +const savedAlertsColumns = ["Timestamp", "Tenant", "Title", "Actions"]; +const savedAlertsApiUrl = "/api/ListAuditLogSearches"; +const savedAlertsActions = [ { label: "View Log", link: "/tenant/administration/audit-logs/log?id=[LogId]", @@ -22,7 +23,30 @@ const actions = [ }, ]; +// Log Searches Tab Configuration +const logSearchesColumns = [ + "SearchId", + "StartTime", + "EndTime", + "TotalLogs", + "MatchedLogs", + "CippStatus", + "Actions", +]; +const logSearchesApiUrl = "/api/ListAuditLogSearches"; +const logSearchesActions = [ + { + label: "View Results", + link: "/tenant/administration/audit-logs/search-results?searchId=[SearchId]", + color: "primary", + icon: , + }, +]; + const Page = () => { + const router = useRouter(); + const { tab = "saved-alerts" } = router.query; + const formControl = useForm({ mode: "onChange", defaultValues: { @@ -32,13 +56,12 @@ const Page = () => { }, }); - 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("1d"); + 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,108 +73,122 @@ const Page = () => { } }; - return ( - setExpanded(!expanded)}> - }> - Search Options - - -
- - {/* Date Filter Type */} - - - + // Determine current tab configuration + const isLogSearches = tab === "log-searches"; + const currentColumns = isLogSearches ? logSearchesColumns : savedAlertsColumns; + const currentApiUrl = isLogSearches ? logSearchesApiUrl : savedAlertsApiUrl; + const currentActions = isLogSearches ? logSearchesActions : savedAlertsActions; + const currentTitle = isLogSearches ? "Log Searches" : "Saved Alerts"; - {/* Relative Time Filter */} - {formControl.watch("dateFilter") === "relative" && ( - <> - - - - - - - - - - - - )} + // API parameters based on tab + const apiParams = isLogSearches + ? { Type: "Searches" } + : { + Days: relativeTime ? parseInt(relativeTime) : 1, + ...(startDate && { StartDate: startDate }), + ...(endDate && { EndDate: endDate }), + }; + + 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 ( + ); }; @@ -164,6 +201,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..b466b84217db 100644 --- a/src/pages/tenant/administration/audit-logs/log.js +++ b/src/pages/tenant/administration/audit-logs/log.js @@ -1,6 +1,7 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { ApiGetCall } from "/src/api/ApiCall"; import { Box, @@ -17,6 +18,7 @@ import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; import { getCippTranslation } from "../../../../utils/get-cipp-translation"; +import tabOptions from "./tabOptions.json"; import CippGeoLocation from "../../../../components/CippComponents/CippGeoLocation"; import { Grid } from "@mui/system"; import { OpenInNew } from "@mui/icons-material"; @@ -203,6 +205,10 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => ( + + {page} + +); export default Page; 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..f837b8bf3544 --- /dev/null +++ b/src/pages/tenant/administration/audit-logs/search-results.js @@ -0,0 +1,74 @@ +import { useRouter } from "next/router"; +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 { EyeIcon } from "@heroicons/react/24/outline"; +import CippAuditLogDetails from "/src/components/CippComponents/CippAuditLogDetails.jsx"; +import tabOptions from "./tabOptions.json"; + +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); + + 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}`; + + // 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 ( + + ); +}; + +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..0721101e81ed --- /dev/null +++ b/src/pages/tenant/administration/audit-logs/searches.js @@ -0,0 +1,267 @@ +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, MagnifyingGlassIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { Grid } from "@mui/system"; +import { Add } from "@mui/icons-material"; +import { useDialog } from "/src/hooks/use-dialog"; +import tabOptions from "./tabOptions.json"; + +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: "Delete Search", + type: "POST", + url: "/api/ExecAuditLogSearch", + data: { + Action: "Delete", + SearchId: "Id", + }, + confirmText: "Are you sure you want to delete this audit log search?", + color: "danger", + icon: , + }, +]; + +const Page = () => { + const createSearchDialog = useDialog(); + + 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: "autoComplete", + name: "TenantFilter", + label: "Tenant", + multiple: false, + api: { + url: "/api/ListTenants?AllTenantSelector=false", + dataKey: "Results", + labelField: "displayName", + valueField: "defaultDomainName", + }, + validators: { required: "Please select a tenant" }, + }, + { + type: "datePicker", + name: "StartTime", + label: "Start Date & Time", + dateTimeType: "datetime-local", + validators: { required: "Start time is required" }, + }, + { + type: "datePicker", + name: "EndTime", + label: "End Date & Time", + dateTimeType: "datetime-local", + validators: { required: "End time is required" }, + }, + { + type: "autoComplete", + name: "RecordTypeFilters", + label: "Record Types", + multiple: true, + options: [ + { label: "Exchange Admin", value: "exchangeAdmin" }, + { label: "Exchange Item", value: "exchangeItem" }, + { label: "SharePoint", value: "sharePoint" }, + { label: "OneDrive", value: "oneDrive" }, + { label: "Azure Active Directory", value: "azureActiveDirectory" }, + { label: "Azure AD Account Logon", value: "azureActiveDirectoryAccountLogon" }, + { label: "Microsoft Teams", value: "microsoftTeams" }, + { label: "Microsoft Teams Admin", value: "microsoftTeamsAdmin" }, + { label: "Power BI", value: "powerBIAudit" }, + { label: "Microsoft Flow", value: "microsoftFlow" }, + { label: "Threat Intelligence", value: "threatIntelligence" }, + { label: "Security & Compliance", value: "securityComplianceAlerts" }, + ], + }, + { + type: "autoComplete", + name: "ServiceFilters", + label: "Services", + multiple: true, + options: [ + { label: "Exchange Online", value: "Exchange" }, + { label: "SharePoint Online", value: "SharePoint" }, + { label: "OneDrive for Business", value: "OneDrive" }, + { label: "Azure Active Directory", value: "AzureActiveDirectory" }, + { label: "Microsoft Teams", value: "MicrosoftTeams" }, + { label: "Power BI", value: "PowerBI" }, + { label: "Microsoft Flow", value: "MicrosoftFlow" }, + { label: "Dynamics 365", value: "CRM" }, + { label: "Yammer", value: "Yammer" }, + { label: "Security & Compliance", value: "ThreatIntelligence" }, + ], + }, + { + type: "textField", + name: "KeywordFilters", + label: "Keywords", + placeholder: "Enter keywords to search for", + }, + { + type: "textField", + name: "OperationsFilters", + label: "Operations", + placeholder: "Enter operations (comma-separated)", + }, + { + type: "textField", + name: "UserPrincipalNameFilters", + label: "User Principal Names", + placeholder: "Enter UPNs (comma-separated)", + }, + { + type: "textField", + name: "IPAddressFilters", + label: "IP Addresses", + placeholder: "Enter IP addresses (comma-separated)", + }, + { + type: "textField", + name: "ObjectIdFilters", + label: "Object IDs", + placeholder: "Enter object IDs (comma-separated)", + }, + { + type: "textField", + name: "AdministrativeUnitFilters", + label: "Administrative Units", + placeholder: "Enter administrative units (comma-separated)", + }, + ]; + + const createSearchApi = { + type: "POST", + url: "/api/ExecAuditLogSearch", + confirmText: "Create this audit log search? This may take several minutes to complete.", + relatedQueryKeys: ["AuditLogSearches"], + }; + + return ( + <> + setExpanded(!expanded)}> + }> + Filter Search List + + + + {/* Status Filter */} + + + + + {/* Date Range Filter */} + + + + + + + } + title={pageTitle} + apiUrl={apiUrlWithFilters} + apiDataKey="Results" + simpleColumns={simpleColumns} + queryKey="AuditLogSearches" + 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 From 0da6145dc470006008bd91cf2ea8f8e44e6ffdd6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 22 Jul 2025 12:42:50 -0400 Subject: [PATCH 06/69] fix form conditions --- .../CippComponents/CippFormCondition.jsx | 25 +++++++++++++++---- .../CippCustomDataMappingForm.jsx | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx index fc121753b9ab..811d7d24fabe 100644 --- a/src/components/CippComponents/CippFormCondition.jsx +++ b/src/components/CippComponents/CippFormCondition.jsx @@ -45,10 +45,15 @@ 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: ", { + console.log("CippFormCondition: ", { watcher, watchedValue, compareTargetValue, @@ -57,7 +62,7 @@ export const CippFormCondition = (props) => { action, field, propertyName, - });*/ + }); // Function to recursively extract field names from child components const extractFieldNames = (children) => { @@ -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/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", }, }, From 357d0f5ae5c52e54308f8a720ee79622f38e0720 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:23:56 +0200 Subject: [PATCH 07/69] fixes height bug --- .../CippStandards/CippStandardDialog.jsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) 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" ? ( - + ) : ( - + )} - + From 9e06d57a1b653b58a47b800527e91a875b727e19 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:01:18 +0200 Subject: [PATCH 08/69] fix breach check exec ution --- .../CippComponents/BreachSearchDialog.jsx | 67 +++++++++++++++++++ src/pages/tools/tenantbreachlookup/index.js | 60 +++++++---------- 2 files changed, 91 insertions(+), 36 deletions(-) create mode 100644 src/components/CippComponents/BreachSearchDialog.jsx 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/pages/tools/tenantbreachlookup/index.js b/src/pages/tools/tenantbreachlookup/index.js index 37df8d84d648..50b351e0e091 100644 --- a/src/pages/tools/tenantbreachlookup/index.js +++ b/src/pages/tools/tenantbreachlookup/index.js @@ -1,20 +1,16 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { ExclamationTriangleIcon, EyeIcon } from "@heroicons/react/24/outline"; -import { ApiGetCall } from "../../../api/ApiCall"; -import { Button, CircularProgress, SvgIcon } from "@mui/material"; -import { useSettings } from "../../../hooks/use-settings"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { Button } from "@mui/material"; +import { Search } from "@mui/icons-material"; +import { BreachSearchDialog } from "../../../components/CippComponents/BreachSearchDialog"; +import { useDialog } from "../../../hooks/use-dialog"; const Page = () => { - const tenantFilter = useSettings()?.currentTenant; - const ApiCall = ApiGetCall({ - url: "/api/ExecBreachSearch", - data: { tenantFilter: tenantFilter }, - waiting: false, - }); - const pageTitle = "Potential Breached passwords and information"; const apiUrl = "/api/ListBreachesTenant"; + const breachSearchDialog = useDialog(); + const actions = [ { label: "View User", @@ -26,31 +22,23 @@ const Page = () => { ]; return ( - - - - } - /> + <> + + + + } + /> + + ); }; From 5f292d1e0993ddfaaf05767d056984a9f1332711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 23 Jul 2025 14:33:10 +0200 Subject: [PATCH 09/69] fix rounding logic for time selection to always round down to the previous 15-minute mark --- src/components/CippComponents/CippFormComponent.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index 7ddc2965b0b0..182fb24cca71 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -495,9 +495,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); From babba2d1946148abad7274fe79439260ad766043 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:45:08 +0200 Subject: [PATCH 10/69] CA policy handling --- src/pages/tenant/standards/compare/index.js | 127 +++++++++++++++----- 1 file changed, 96 insertions(+), 31 deletions(-) diff --git a/src/pages/tenant/standards/compare/index.js b/src/pages/tenant/standards/compare/index.js index 75e7892d81cf..533ee5aec32f 100644 --- a/src/pages/tenant/standards/compare/index.js +++ b/src/pages/tenant/standards/compare/index.js @@ -171,6 +171,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 +374,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,35 +413,43 @@ 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; @@ -455,11 +524,7 @@ const Page = () => { variant="outlined" size="small" color={ - combinedScore >= 80 - ? "success" - : combinedScore >= 60 - ? "warning" - : "error" + combinedScore >= 80 ? "success" : combinedScore >= 60 ? "warning" : "error" } /> @@ -1120,8 +1185,8 @@ const Page = () => { textDecoration: "none", }, }, - fontSize: "0.875rem", - lineHeight: 1.43, + fontSize: "0.875rem", + lineHeight: 1.43, "& p": { my: 0, }, From 33fef690649066d656f1e435ef9ecd5e59b6d7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 23 Jul 2025 16:16:29 +0200 Subject: [PATCH 11/69] send email to the backend too --- .../CippCards/CippExchangeInfoCard.jsx | 31 +++++++++++-------- .../administration/users/user/exchange.jsx | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/components/CippCards/CippExchangeInfoCard.jsx b/src/components/CippCards/CippExchangeInfoCard.jsx index cfb08497dcf9..1af6e6043772 100644 --- a/src/components/CippCards/CippExchangeInfoCard.jsx +++ b/src/components/CippCards/CippExchangeInfoCard.jsx @@ -81,7 +81,10 @@ export const CippExchangeInfoCard = (props) => { Hidden from GAL: - {getCippFormatting(exchangeData?.HiddenFromAddressLists, "HiddenFromAddressLists")} + {getCippFormatting( + exchangeData?.HiddenFromAddressLists, + "HiddenFromAddressLists" + )}
@@ -127,14 +130,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 +148,7 @@ export const CippExchangeInfoCard = (props) => { cleanAddress = forwardingAddress; } } - + return ( @@ -152,10 +156,9 @@ export const CippExchangeInfoCard = (props) => { Forwarding Status: - {forwardingType === "None" + {forwardingType === "None" ? getCippFormatting(false, "ForwardingStatus") - : `${forwardingType} Forwarding` - } + : `${forwardingType} Forwarding`} {forwardingType !== "None" && ( @@ -172,18 +175,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/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, }); From 103e8bc55a37cd63a227540bf321a80114b4f7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 23 Jul 2025 16:41:19 +0200 Subject: [PATCH 12/69] Add alert if the mailbox is blocked for spam --- src/components/CippCards/CippExchangeInfoCard.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/CippCards/CippExchangeInfoCard.jsx b/src/components/CippCards/CippExchangeInfoCard.jsx index 1af6e6043772..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} Date: Wed, 23 Jul 2025 17:37:16 -0400 Subject: [PATCH 13/69] guid resolver tweaks --- src/hooks/use-guid-resolver.js | 190 ++++++++++--- .../administration/audit-logs/searches.js | 259 ++++++++++++++++-- 2 files changed, 400 insertions(+), 49 deletions(-) diff --git a/src/hooks/use-guid-resolver.js b/src/hooks/use-guid-resolver.js index ebb8722f3e3b..bef74ce65316 100644 --- a/src/hooks/use-guid-resolver.js +++ b/src/hooks/use-guid-resolver.js @@ -16,9 +16,52 @@ const extractGuidsFromString = (str) => { 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()) => { - if (!obj) return guidsSet; +const findGuids = (obj, guidsSet = new Set(), partnerGuidsMap = new Map()) => { + if (!obj) return { guidsSet, partnerGuidsMap }; if (typeof obj === "string") { // Check if the entire string is a GUID @@ -28,14 +71,31 @@ const findGuids = (obj, guidsSet = new Set()) => { // Extract GUIDs embedded within longer strings const embeddedGuids = extractGuidsFromString(obj); embeddedGuids.forEach((guid) => guidsSet.add(guid)); + + // Extract object IDs from partner tenant UPNs + const partnerObjectIds = extractObjectIdFromPartnerUPN(obj); + partnerObjectIds.forEach(({ guid, tenantDomain }) => { + if (!partnerGuidsMap.has(tenantDomain)) { + partnerGuidsMap.set(tenantDomain, new Set()); + } + partnerGuidsMap.get(tenantDomain).add(guid); + }); } } else if (Array.isArray(obj)) { - obj.forEach((item) => findGuids(item, guidsSet)); + 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) => findGuids(value, guidsSet)); + Object.values(obj).forEach((value) => { + const result = findGuids(value, guidsSet, partnerGuidsMap); + guidsSet = result.guidsSet; + partnerGuidsMap = result.partnerGuidsMap; + }); } - return guidsSet; + return { guidsSet, partnerGuidsMap }; }; export const useGuidResolver = () => { @@ -48,6 +108,7 @@ export const useGuidResolver = () => { // 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); // Setup API call for directory objects resolution @@ -81,46 +142,110 @@ export const useGuidResolver = () => { }, }); + // Setup API call for partner tenant directory objects resolution + const partnerDirectoryObjectsMutation = ApiPostCall({ + relatedQueryKeys: ["partnerDirectoryObjects"], + onResult: (data) => { + if (data && Array.isArray(data.value)) { + const newMapping = {}; + + // Process the returned results + data.value.forEach((item) => { + if (item.id && (item.displayName || item.userPrincipalName || item.mail)) { + newMapping[item.id] = item.displayName || item.userPrincipalName || item.mail; + } + }); + + setGuidMapping((prevMapping) => ({ ...prevMapping, ...newMapping })); + + // Clear processed partner GUIDs + pendingPartnerGuidsRef.current = new Map(); + setIsLoadingGuids(false); + } + }, + }); + // Function to handle resolving GUIDs const resolveGuids = useCallback( (objectToScan) => { - const guidsSet = findGuids(objectToScan); + const { guidsSet, partnerGuidsMap } = findGuids(objectToScan); - if (guidsSet.size === 0) return; + // Handle regular GUIDs (current tenant) + if (guidsSet.size > 0) { + const guidsArray = Array.from(guidsSet); + const notResolvedGuids = guidsArray.filter( + (guid) => !guidMapping[guid] && !notFoundGuidsRef.current.has(guid) + ); - const guidsArray = Array.from(guidsSet); - const notResolvedGuids = guidsArray.filter( - (guid) => !guidMapping[guid] && !notFoundGuidsRef.current.has(guid) - ); + if (notResolvedGuids.length > 0) { + const allPendingGuids = [...new Set([...pendingGuidsRef.current, ...notResolvedGuids])]; + pendingGuidsRef.current = allPendingGuids; + setIsLoadingGuids(true); - if (notResolvedGuids.length === 0) return; + // Implement throttling - only send a new request every 2 seconds + const now = Date.now(); + if (now - lastRequestTimeRef.current >= 2000) { + lastRequestTimeRef.current = now; - const allPendingGuids = [...new Set([...pendingGuidsRef.current, ...notResolvedGuids])]; - pendingGuidsRef.current = allPendingGuids; - setIsLoadingGuids(true); + // Only send a maximum of 1000 GUIDs per request + const batchSize = 1000; + const guidsToSend = allPendingGuids.slice(0, batchSize); - // Implement throttling - only send a new request every 2 seconds - const now = Date.now(); - if (now - lastRequestTimeRef.current < 2000) { - return; + if (guidsToSend.length > 0) { + directoryObjectsMutation.mutate({ + url: "/api/ListDirectoryObjects", + data: { + tenantFilter: tenantFilter, + ids: guidsToSend, + $select: "id,displayName,userPrincipalName,mail", + }, + }); + } else { + setIsLoadingGuids(false); + } + } + } } - lastRequestTimeRef.current = now; + // Handle partner tenant GUIDs + 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) + ); - // Only send a maximum of 1000 GUIDs per request - const batchSize = 1000; - const guidsToSend = allPendingGuids.slice(0, batchSize); + setIsLoadingGuids(true); - if (guidsToSend.length > 0) { - directoryObjectsMutation.mutate({ - url: "/api/ListDirectoryObjects", - data: { - tenantFilter: tenantFilter, - ids: guidsToSend, - $select: "id,displayName,userPrincipalName,mail", - }, + // Make API call for partner tenant + const batchSize = 1000; + const guidsToSend = notResolvedGuids.slice(0, batchSize); + + if (guidsToSend.length > 0) { + partnerDirectoryObjectsMutation.mutate({ + url: "/api/ListDirectoryObjects", + data: { + tenantFilter: tenantDomain, + ids: guidsToSend, + $select: "id,displayName,userPrincipalName,mail", + }, + }); + } + } }); - } else { + } + + // If no GUIDs to process, ensure loading state is false + if (guidsSet.size === 0 && partnerGuidsMap.size === 0) { setIsLoadingGuids(false); } }, @@ -132,5 +257,6 @@ export const useGuidResolver = () => { isLoadingGuids, resolveGuids, isGuid, + extractObjectIdFromPartnerUPN, }; }; diff --git a/src/pages/tenant/administration/audit-logs/searches.js b/src/pages/tenant/administration/audit-logs/searches.js index 0721101e81ed..6f384bd13487 100644 --- a/src/pages/tenant/administration/audit-logs/searches.js +++ b/src/pages/tenant/administration/audit-logs/searches.js @@ -82,10 +82,10 @@ const Page = () => { api: { url: "/api/ListTenants?AllTenantSelector=false", dataKey: "Results", - labelField: "displayName", + labelField: (option) => `${option.displayName} (${option.defaultDomainName})`, valueField: "defaultDomainName", + validators: { required: "Please select a tenant" }, }, - validators: { required: "Please select a tenant" }, }, { type: "datePicker", @@ -109,16 +109,41 @@ const Page = () => { options: [ { label: "Exchange Admin", value: "exchangeAdmin" }, { label: "Exchange Item", value: "exchangeItem" }, + { label: "Exchange Item Group", value: "exchangeItemGroup" }, { label: "SharePoint", value: "sharePoint" }, + { label: "SharePoint File Operation", value: "sharePointFileOperation" }, + { label: "SharePoint Sharing Operation", value: "sharePointSharingOperation" }, + { label: "SharePoint List Operation", value: "sharePointListOperation" }, { label: "OneDrive", value: "oneDrive" }, { label: "Azure Active Directory", value: "azureActiveDirectory" }, { label: "Azure AD Account Logon", value: "azureActiveDirectoryAccountLogon" }, + { label: "Azure AD STS Logon", value: "azureActiveDirectoryStsLogon" }, { label: "Microsoft Teams", value: "microsoftTeams" }, { label: "Microsoft Teams Admin", value: "microsoftTeamsAdmin" }, - { label: "Power BI", value: "powerBIAudit" }, + { label: "Microsoft Teams Analytics", value: "microsoftTeamsAnalytics" }, + { label: "Microsoft Teams Device", value: "microsoftTeamsDevice" }, + { label: "Microsoft Teams Shifts", value: "microsoftTeamsShifts" }, + { label: "Power BI Audit", value: "powerBIAudit" }, + { label: "Power BI DLP", value: "powerBIDlp" }, { label: "Microsoft Flow", value: "microsoftFlow" }, + { label: "Microsoft Forms", value: "microsoftForms" }, + { label: "Microsoft Stream", value: "microsoftStream" }, { label: "Threat Intelligence", value: "threatIntelligence" }, - { label: "Security & Compliance", value: "securityComplianceAlerts" }, + { label: "Threat Intelligence URL", value: "threatIntelligenceUrl" }, + { label: "Threat Intelligence ATP Content", value: "threatIntelligenceAtpContent" }, + { label: "Security & Compliance Alerts", value: "securityComplianceAlerts" }, + { label: "Security & Compliance Insights", value: "securityComplianceInsights" }, + { label: "Security & Compliance RBAC", value: "securityComplianceRBAC" }, + { label: "Compliance DLP SharePoint", value: "complianceDLPSharePoint" }, + { label: "Compliance DLP Exchange", value: "complianceDLPExchange" }, + { label: "Compliance DLP Endpoint", value: "complianceDLPEndpoint" }, + { label: "Data Governance", value: "dataGovernance" }, + { label: "MIP Label", value: "mipLabel" }, + { label: "Label Content Explorer", value: "labelContentExplorer" }, + { label: "Information Worker Protection", value: "informationWorkerProtection" }, + { label: "Workplace Analytics", value: "workplaceAnalytics" }, + { label: "Power Apps App", value: "powerAppsApp" }, + { label: "Power Apps Plan", value: "powerAppsPlan" }, ], }, { @@ -140,48 +165,248 @@ const Page = () => { ], }, { - type: "textField", - name: "KeywordFilters", + type: "autoComplete", + name: "KeywordFilter", label: "Keywords", + multiple: true, + creatable: true, + freeSolo: true, placeholder: "Enter keywords to search for", + options: [], }, { - type: "textField", + type: "autoComplete", name: "OperationsFilters", label: "Operations", - placeholder: "Enter operations (comma-separated)", + 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: "textField", + type: "autoComplete", name: "UserPrincipalNameFilters", label: "User Principal Names", - placeholder: "Enter UPNs (comma-separated)", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter user principal names", + options: [], }, { - type: "textField", + type: "autoComplete", name: "IPAddressFilters", label: "IP Addresses", - placeholder: "Enter IP addresses (comma-separated)", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter IP addresses", + options: [], }, { - type: "textField", + type: "autoComplete", name: "ObjectIdFilters", label: "Object IDs", - placeholder: "Enter object IDs (comma-separated)", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter object IDs", + options: [], }, { - type: "textField", + type: "autoComplete", name: "AdministrativeUnitFilters", label: "Administrative Units", - placeholder: "Enter administrative units (comma-separated)", + 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 complete.", + confirmText: + "Create this audit log search? This may take several minutes to hours to complete.", relatedQueryKeys: ["AuditLogSearches"], + customDataformatter: (data) => { + const formattedData = { ...data }; + + // 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 ( From fbd6820bfb5fb8feb962e214ce01678c1393edb5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 23 Jul 2025 23:55:15 -0400 Subject: [PATCH 14/69] fix query key tidy up audit log searches page --- src/hooks/use-guid-resolver.js | 7 +++-- .../audit-logs/search-results.js | 29 ++++++++++++++----- .../administration/audit-logs/searches.js | 6 +++- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/hooks/use-guid-resolver.js b/src/hooks/use-guid-resolver.js index bef74ce65316..a091a5132e7a 100644 --- a/src/hooks/use-guid-resolver.js +++ b/src/hooks/use-guid-resolver.js @@ -98,8 +98,9 @@ const findGuids = (obj, guidsSet = new Set(), partnerGuidsMap = new Map()) => { return { guidsSet, partnerGuidsMap }; }; -export const useGuidResolver = () => { +export const useGuidResolver = (manualTenant = null) => { const tenantFilter = useSettings().currentTenant; + const activeTenant = manualTenant || tenantFilter; // GUID resolution state const [guidMapping, setGuidMapping] = useState({}); @@ -195,7 +196,7 @@ export const useGuidResolver = () => { directoryObjectsMutation.mutate({ url: "/api/ListDirectoryObjects", data: { - tenantFilter: tenantFilter, + tenantFilter: activeTenant, ids: guidsToSend, $select: "id,displayName,userPrincipalName,mail", }, @@ -249,7 +250,7 @@ export const useGuidResolver = () => { setIsLoadingGuids(false); } }, - [guidMapping, tenantFilter] // Only depend on guidMapping and tenantFilter + [guidMapping, activeTenant] // Only depend on guidMapping and activeTenant ); return { diff --git a/src/pages/tenant/administration/audit-logs/search-results.js b/src/pages/tenant/administration/audit-logs/search-results.js index f837b8bf3544..57ac4e32ade3 100644 --- a/src/pages/tenant/administration/audit-logs/search-results.js +++ b/src/pages/tenant/administration/audit-logs/search-results.js @@ -1,11 +1,10 @@ import { useRouter } from "next/router"; 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 { EyeIcon } from "@heroicons/react/24/outline"; +import { EyeIcon, ArrowLeftIcon } from "@heroicons/react/24/outline"; import CippAuditLogDetails from "/src/components/CippComponents/CippAuditLogDetails.jsx"; -import tabOptions from "./tabOptions.json"; +import { Button, SvgIcon } from "@mui/material"; const searchResultsColumns = [ "createdDateTime", @@ -41,6 +40,10 @@ const Page = () => { const pageTitle = searchName ? `${searchName}` : `Search Results - ${searchId}`; + const handleBackClick = () => { + router.push("/tenant/administration/audit-logs/searches"); + }; + // Define offcanvas configuration with larger size for audit log details const offcanvas = { title: "Audit Log Details", @@ -51,6 +54,20 @@ const Page = () => { return ( + + + } + > + Back to Searches + + } apiUrl="/api/ListAuditLogSearches" apiDataKey="Results" simpleColumns={searchResultsColumns} @@ -65,10 +82,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => ( - - {page} - -); +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 index 6f384bd13487..88c43500cb2a 100644 --- a/src/pages/tenant/administration/audit-logs/searches.js +++ b/src/pages/tenant/administration/audit-logs/searches.js @@ -12,6 +12,7 @@ import { Grid } from "@mui/system"; import { Add } 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"]; @@ -41,6 +42,7 @@ const actions = [ const Page = () => { const createSearchDialog = useDialog(); + const currentTenant = useSettings().currentTenant; const filterControl = useForm({ mode: "onChange", @@ -460,7 +462,9 @@ const Page = () => { apiUrl={apiUrlWithFilters} apiDataKey="Results" simpleColumns={simpleColumns} - queryKey="AuditLogSearches" + queryKey={`AuditLogSearches-${filterControl.getValues().StatusFilter?.value || "All"}-${ + filterControl.getValues().DateFilter?.value || "AllTime" + }-${currentTenant}`} actions={actions} cardButton={ } From 545334df867196d1220f656feae4b6fb46f0013c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 24 Jul 2025 15:36:38 -0400 Subject: [PATCH 17/69] guid resolver tweaks --- .../CippComponents/CippAuditLogDetails.jsx | 109 +++++++++++++----- src/hooks/use-guid-resolver.js | 103 +++++++++++++++-- 2 files changed, 176 insertions(+), 36 deletions(-) diff --git a/src/components/CippComponents/CippAuditLogDetails.jsx b/src/components/CippComponents/CippAuditLogDetails.jsx index c440b33b2150..5333ef9e3b5c 100644 --- a/src/components/CippComponents/CippAuditLogDetails.jsx +++ b/src/components/CippComponents/CippAuditLogDetails.jsx @@ -7,38 +7,37 @@ import { useGuidResolver } from "/src/hooks/use-guid-resolver"; import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; const CippAuditLogDetails = ({ row }) => { - const { guidMapping, isLoadingGuids, resolveGuids, isGuid } = useGuidResolver(); - + 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 in strings with resolved names + // Function to replace GUIDs and special UPNs in strings with resolved names const replaceGuidsInString = (str) => { if (typeof str !== "string") return str; - 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) || []; - - if (guidsInString.length === 0) return str; - - let result = str; - let hasResolvedGuids = false; - - guidsInString.forEach((guid) => { - if (guidMapping[guid]) { - result = result.replace(new RegExp(guid, "gi"), guidMapping[guid]); - hasResolvedGuids = true; - } - }); + // Use the hook's helper function to replace both GUIDs and special UPNs + const { result, hasResolvedNames } = replaceGuidsAndUpnsInString(str); - // If we have resolved GUIDs, return a tooltip showing original and resolved - if (hasResolvedGuids) { + // If we have resolved names, return a tooltip showing original and resolved + if (hasResolvedNames) { return ( {result} @@ -46,8 +45,16 @@ const CippAuditLogDetails = ({ row }) => { ); } - // If we have unresolved GUIDs and currently loading - if (guidsInString.length > 0 && isLoadingGuids) { + // 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; + + const hasGuids = guidRegex.test(str); + partnerUpnRegex.lastIndex = 0; // Reset regex state + const hasUpns = partnerUpnRegex.test(str); + + // If we have unresolved GUIDs or UPNs and are currently loading + if ((hasGuids || hasUpns) && isLoadingGuids) { return (
@@ -77,6 +84,12 @@ const CippAuditLogDetails = ({ row }) => { 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 && @@ -122,26 +135,66 @@ const CippAuditLogDetails = ({ row }) => { // Render GUID values with proper resolution states const renderGuidValue = (guidValue) => { + // Handle standard GUIDs directly if (guidMapping[guidValue]) { return ( {guidMapping[guidValue]} ); - } else if (isLoadingGuids) { + } + + // 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}
); - } else { - return ( - - {guidValue} - - ); } + + // Fallback for unresolved values + return ( + + {guidValue} + + ); }; // Recursively render nested objects and arrays with GUID expansion diff --git a/src/hooks/use-guid-resolver.js b/src/hooks/use-guid-resolver.js index a091a5132e7a..217149d67eda 100644 --- a/src/hooks/use-guid-resolver.js +++ b/src/hooks/use-guid-resolver.js @@ -98,12 +98,71 @@ const findGuids = (obj, guidsSet = new Set(), partnerGuidsMap = new Map()) => { 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 @@ -117,12 +176,21 @@ export const useGuidResolver = (manualTenant = null) => { relatedQueryKeys: ["directoryObjects"], onResult: (data) => { if (data && Array.isArray(data.value)) { - const newMapping = {}; + const newDisplayMapping = {}; + const newUpnMapping = {}; // Process the returned results data.value.forEach((item) => { - if (item.id && (item.displayName || item.userPrincipalName || item.mail)) { - newMapping[item.id] = item.displayName || item.userPrincipalName || item.mail; + 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; + } } }); @@ -136,7 +204,8 @@ export const useGuidResolver = (manualTenant = null) => { notReturned.forEach((guid) => notFoundGuidsRef.current.add(guid)); } - setGuidMapping((prevMapping) => ({ ...prevMapping, ...newMapping })); + setGuidMapping((prevMapping) => ({ ...prevMapping, ...newDisplayMapping })); + setUpnMapping((prevMapping) => ({ ...prevMapping, ...newUpnMapping })); pendingGuidsRef.current = []; setIsLoadingGuids(false); } @@ -148,16 +217,26 @@ export const useGuidResolver = (manualTenant = null) => { relatedQueryKeys: ["partnerDirectoryObjects"], onResult: (data) => { if (data && Array.isArray(data.value)) { - const newMapping = {}; + const newDisplayMapping = {}; + const newUpnMapping = {}; // Process the returned results data.value.forEach((item) => { - if (item.id && (item.displayName || item.userPrincipalName || item.mail)) { - newMapping[item.id] = item.displayName || item.userPrincipalName || item.mail; + 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; + } } }); - setGuidMapping((prevMapping) => ({ ...prevMapping, ...newMapping })); + setGuidMapping((prevMapping) => ({ ...prevMapping, ...newDisplayMapping })); + setUpnMapping((prevMapping) => ({ ...prevMapping, ...newUpnMapping })); // Clear processed partner GUIDs pendingPartnerGuidsRef.current = new Map(); @@ -253,11 +332,19 @@ export const useGuidResolver = (manualTenant = null) => { [guidMapping, activeTenant] // Only depend on guidMapping and activeTenant ); + // Create a memoized version of the string replacement function + const replaceGuidsAndUpnsInStringMemoized = useCallback( + (str) => replaceGuidsAndUpnsInString(str, guidMapping, upnMapping, isLoadingGuids), + [guidMapping, upnMapping, isLoadingGuids] + ); + return { guidMapping, + upnMapping, isLoadingGuids, resolveGuids, isGuid, extractObjectIdFromPartnerUPN, + replaceGuidsAndUpnsInString: replaceGuidsAndUpnsInStringMemoized, }; }; From cd24bdd22fcb0af47356f7b33c85d8c89b6129cd Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 24 Jul 2025 16:12:34 -0400 Subject: [PATCH 18/69] better object resolution add rate limiting, upns, etc --- .../CippComponents/CippAuditLogDetails.jsx | 20 +- src/hooks/use-guid-resolver.js | 185 +++++++++++++++--- 2 files changed, 170 insertions(+), 35 deletions(-) diff --git a/src/components/CippComponents/CippAuditLogDetails.jsx b/src/components/CippComponents/CippAuditLogDetails.jsx index 5333ef9e3b5c..b5a3077a0da6 100644 --- a/src/components/CippComponents/CippAuditLogDetails.jsx +++ b/src/components/CippComponents/CippAuditLogDetails.jsx @@ -49,9 +49,21 @@ const CippAuditLogDetails = ({ row }) => { 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; - const hasGuids = guidRegex.test(str); - partnerUpnRegex.lastIndex = 0; // Reset regex state - const hasUpns = partnerUpnRegex.test(str); + 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) { @@ -322,7 +334,7 @@ const CippAuditLogDetails = ({ row }) => { const auditDataItems = row?.auditData ? convertToPropertyItems(row.auditData) : []; return ( - + { 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)) { - guidsSet.add(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) => guidsSet.add(guid)); - - // Extract object IDs from partner tenant UPNs - const partnerObjectIds = extractObjectIdFromPartnerUPN(obj); - partnerObjectIds.forEach(({ guid, tenantDomain }) => { - if (!partnerGuidsMap.has(tenantDomain)) { - partnerGuidsMap.set(tenantDomain, new Set()); + embeddedGuids.forEach((guid) => { + // Only add to main guidsSet if it's not a partner GUID + if (!partnerGuids.has(guid)) { + guidsSet.add(guid); } - partnerGuidsMap.get(tenantDomain).add(guid); }); } } else if (Array.isArray(obj)) { @@ -170,11 +181,64 @@ export const useGuidResolver = (manualTenant = null) => { 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 = {}; @@ -216,6 +280,44 @@ export const useGuidResolver = (manualTenant = null) => { 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 = {}; @@ -243,14 +345,12 @@ export const useGuidResolver = (manualTenant = null) => { setIsLoadingGuids(false); } }, - }); - - // Function to handle resolving GUIDs + }); // Function to handle resolving GUIDs const resolveGuids = useCallback( (objectToScan) => { const { guidsSet, partnerGuidsMap } = findGuids(objectToScan); - // Handle regular GUIDs (current tenant) + // 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( @@ -258,13 +358,14 @@ export const useGuidResolver = (manualTenant = null) => { ); 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); - // Implement throttling - only send a new request every 2 seconds + // Make API call for primary tenant GUIDs const now = Date.now(); - if (now - lastRequestTimeRef.current >= 2000) { + if (!rateLimitTimeoutRef.current && now - lastRequestTimeRef.current >= 2000) { lastRequestTimeRef.current = now; // Only send a maximum of 1000 GUIDs per request @@ -272,6 +373,9 @@ export const useGuidResolver = (manualTenant = null) => { 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: { @@ -287,7 +391,7 @@ export const useGuidResolver = (manualTenant = null) => { } } - // Handle partner tenant GUIDs + // Handle partner tenant GUIDs separately if (partnerGuidsMap.size > 0) { partnerGuidsMap.forEach((guids, tenantDomain) => { const guidsArray = Array.from(guids); @@ -306,19 +410,28 @@ export const useGuidResolver = (manualTenant = null) => { setIsLoadingGuids(true); - // Make API call for partner tenant - const batchSize = 1000; - const guidsToSend = notResolvedGuids.slice(0, batchSize); - - if (guidsToSend.length > 0) { - partnerDirectoryObjectsMutation.mutate({ - url: "/api/ListDirectoryObjects", - data: { - tenantFilter: tenantDomain, - ids: guidsToSend, - $select: "id,displayName,userPrincipalName,mail", - }, - }); + // 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", + }, + }); + } } } }); @@ -329,7 +442,7 @@ export const useGuidResolver = (manualTenant = null) => { setIsLoadingGuids(false); } }, - [guidMapping, activeTenant] // Only depend on guidMapping and activeTenant + [guidMapping, activeTenant, directoryObjectsMutation, partnerDirectoryObjectsMutation] ); // Create a memoized version of the string replacement function @@ -338,6 +451,16 @@ export const useGuidResolver = (manualTenant = null) => { [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, From a572a9545bf134a6b2df17abd29dbc722a1ce0c6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 24 Jul 2025 16:48:00 -0400 Subject: [PATCH 19/69] cleanup saved logs page --- .../tenant/administration/audit-logs/index.js | 84 +++++++------------ 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/src/pages/tenant/administration/audit-logs/index.js b/src/pages/tenant/administration/audit-logs/index.js index faba994f0571..109733afa197 100644 --- a/src/pages/tenant/administration/audit-logs/index.js +++ b/src/pages/tenant/administration/audit-logs/index.js @@ -3,18 +3,25 @@ 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, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { EyeIcon } from "@heroicons/react/24/outline"; import { Grid } from "@mui/system"; import tabOptions from "./tabOptions.json"; -// Saved Alerts Tab Configuration -const savedAlertsColumns = ["Timestamp", "Tenant", "Title", "Actions"]; -const savedAlertsApiUrl = "/api/ListAuditLogSearches"; -const savedAlertsActions = [ +// Saved Logs Configuration +const savedLogsColumns = ["Timestamp", "Tenant", "Title", "Actions"]; +const savedLogsApiUrl = "/api/ListAuditLogSearches"; +const savedLogsActions = [ { label: "View Log", link: "/tenant/administration/audit-logs/log?id=[LogId]", @@ -23,41 +30,20 @@ const savedAlertsActions = [ }, ]; -// Log Searches Tab Configuration -const logSearchesColumns = [ - "SearchId", - "StartTime", - "EndTime", - "TotalLogs", - "MatchedLogs", - "CippStatus", - "Actions", -]; -const logSearchesApiUrl = "/api/ListAuditLogSearches"; -const logSearchesActions = [ - { - label: "View Results", - link: "/tenant/administration/audit-logs/search-results?searchId=[SearchId]", - color: "primary", - icon: , - }, -]; - const Page = () => { const router = useRouter(); - const { tab = "saved-alerts" } = router.query; const formControl = useForm({ mode: "onChange", defaultValues: { dateFilter: "relative", - Time: 1, + Time: 7, Interval: { label: "Days", value: "d" }, }, }); const [expanded, setExpanded] = useState(false); - const [relativeTime, setRelativeTime] = useState("1d"); + const [relativeTime, setRelativeTime] = useState("7d"); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); @@ -73,21 +59,12 @@ const Page = () => { } }; - // Determine current tab configuration - const isLogSearches = tab === "log-searches"; - const currentColumns = isLogSearches ? logSearchesColumns : savedAlertsColumns; - const currentApiUrl = isLogSearches ? logSearchesApiUrl : savedAlertsApiUrl; - const currentActions = isLogSearches ? logSearchesActions : savedAlertsActions; - const currentTitle = isLogSearches ? "Log Searches" : "Saved Alerts"; - - // API parameters based on tab - const apiParams = isLogSearches - ? { Type: "Searches" } - : { - Days: relativeTime ? parseInt(relativeTime) : 1, - ...(startDate && { StartDate: startDate }), - ...(endDate && { EndDate: endDate }), - }; + // API parameters for saved logs + const apiParams = { + Days: relativeTime ? parseInt(relativeTime) : 7, + ...(startDate && { StartDate: startDate }), + ...(endDate && { EndDate: endDate }), + }; const searchFilter = ( setExpanded(!expanded)}> @@ -117,16 +94,18 @@ const Page = () => { <> - + - + { 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. From 81bd56b45afd828d8b890627fba32f3571c4b7ce Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 24 Jul 2025 17:14:21 -0400 Subject: [PATCH 20/69] fix saved logs page --- src/pages/tenant/administration/audit-logs/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/administration/audit-logs/index.js b/src/pages/tenant/administration/audit-logs/index.js index 109733afa197..eea348d867c8 100644 --- a/src/pages/tenant/administration/audit-logs/index.js +++ b/src/pages/tenant/administration/audit-logs/index.js @@ -20,7 +20,7 @@ import tabOptions from "./tabOptions.json"; // Saved Logs Configuration const savedLogsColumns = ["Timestamp", "Tenant", "Title", "Actions"]; -const savedLogsApiUrl = "/api/ListAuditLogSearches"; +const savedLogsApiUrl = "/api/ListAuditLogs"; const savedLogsActions = [ { label: "View Log", @@ -48,6 +48,7 @@ const Page = () => { const [endDate, setEndDate] = useState(null); const onSubmit = (data) => { + console.log("Form Data:", data); if (data.dateFilter === "relative") { setRelativeTime(`${data.Time}${data.Interval.value}`); setStartDate(null); @@ -61,7 +62,7 @@ const Page = () => { // API parameters for saved logs const apiParams = { - Days: relativeTime ? parseInt(relativeTime) : 7, + RelativeTime: relativeTime ? relativeTime : "7d", ...(startDate && { StartDate: startDate }), ...(endDate && { EndDate: endDate }), }; From d2391d071b69008486edee306f927d18eb627557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 24 Jul 2025 23:51:13 +0200 Subject: [PATCH 21/69] remove valueField prop from componentProps to fix current tenant autoselect --- src/pages/endpoint/MEM/add-policy/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 9c1235e9a86b04cc385e0a167d1c02bdc5d3360f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 24 Jul 2025 23:51:43 +0200 Subject: [PATCH 22/69] Add Deploy Policy button with permissions to pages Add preselected option to tenant selector in JIT Admin form --- .../MEM/list-appprotection-policies/index.js | 15 +- .../MEM/list-compliance-policies/index.js | 15 +- src/pages/endpoint/MEM/list-policies/index.js | 15 +- .../identity/administration/jit-admin/add.jsx | 477 +++++++++--------- 4 files changed, 281 insertions(+), 241 deletions(-) diff --git a/src/pages/endpoint/MEM/list-appprotection-policies/index.js b/src/pages/endpoint/MEM/list-appprotection-policies/index.js index 0e889eaee6a7..b637aa950730 100644 --- a/src/pages/endpoint/MEM/list-appprotection-policies/index.js +++ b/src/pages/endpoint/MEM/list-appprotection-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 } from "@mui/icons-material"; +import { Book, RocketLaunch } from "@mui/icons-material"; import { TrashIcon } from "@heroicons/react/24/outline"; +import { PermissionButton } from "/src/utils/permissions.js"; +import Link from "next/link"; const Page = () => { 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/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index 7bb042fb9dcf..7f16c5c2ba0e 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -1,238 +1,239 @@ -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; From 2598bee8291e37ed1fa639ef13eab68fd7311f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 25 Jul 2025 00:02:56 +0200 Subject: [PATCH 23/69] Add preselected option for tenant selection across multiple forms --- .../contacts-template/deploy.jsx | 1 + .../spamfilter/list-connectionfilter/add.jsx | 1 + .../list-quarantine-policies/add.jsx | 127 +++++++++--------- .../email/spamfilter/list-spamfilter/add.jsx | 1 + .../email/transport/list-connectors/add.jsx | 1 + src/pages/email/transport/list-rules/add.jsx | 1 + src/pages/endpoint/applications/list/add.jsx | 1 + .../autopilot/add-status-page/index.js | 3 +- .../endpoint/autopilot/list-profiles/add.jsx | 1 + .../identity/administration/jit-admin/add.jsx | 1 + .../security/defender/deployment/index.js | 1 + .../safelinks/safelinks-template/add.jsx | 9 +- .../conditional/list-named-locations/add.jsx | 2 + 13 files changed, 79 insertions(+), 71 deletions(-) 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/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/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/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index 7f16c5c2ba0e..fd3f8e5ddfe0 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -29,6 +29,7 @@ const Page = () => { type="single" allTenants={false} preselectedEnabled={true} + validators={{ required: "A tenant must be selected" }} /> 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/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} /> From a994f908fc75c4f0ba805de0b8498e7427e56549 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 24 Jul 2025 19:23:59 -0400 Subject: [PATCH 24/69] Update index.js --- src/pages/tenant/administration/audit-logs/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/tenant/administration/audit-logs/index.js b/src/pages/tenant/administration/audit-logs/index.js index eea348d867c8..9a329abfb73a 100644 --- a/src/pages/tenant/administration/audit-logs/index.js +++ b/src/pages/tenant/administration/audit-logs/index.js @@ -48,7 +48,6 @@ const Page = () => { const [endDate, setEndDate] = useState(null); const onSubmit = (data) => { - console.log("Form Data:", data); if (data.dateFilter === "relative") { setRelativeTime(`${data.Time}${data.Interval.value}`); setStartDate(null); From fa72deb20e284281552a57545144e38cb627b334 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 25 Jul 2025 15:20:23 +0200 Subject: [PATCH 25/69] Bulk user property wizard #1 --- .../CippComponents/CippUserActions.jsx | 23 + .../identity/administration/users/index.js | 226 ++++---- .../administration/users/patch-wizard.jsx | 512 ++++++++++++++++++ 3 files changed, 648 insertions(+), 113 deletions(-) create mode 100644 src/pages/identity/administration/users/patch-wizard.jsx diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 46c7ec1f4bb6..09e8b93bffdd 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: "Patch Users", + 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/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..8769a43fc093 --- /dev/null +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -0,0 +1,512 @@ +import React, { 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, + FormControl, + InputLabel, + Select, + MenuItem, + OutlinedInput, + Box, + TextField, + Checkbox, + ListItemText, + Button, + Switch, + FormControlLabel +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { CippWizardStepButtons } from "/src/components/CippWizard/CippWizardStepButtons"; +import { PropertyList } from "/src/components/property-list"; +import { PropertyListItem } from "/src/components/property-list-item"; +import { getCippTranslation } from "/src/utils/get-cipp-translation"; +import { getCippFormatting } from "/src/utils/get-cipp-formatting"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; + +// 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 patched +const UsersDisplayStep = (props) => { + const { onNextStep, onPreviousStep, formControl, currentStep, users } = props; + + return ( + + + Users to be updated + + The following users will be updated with the properties you select in the next step. + + + + + + + Selected Users ({users?.length || 0}) + + {users?.map((user, index) => ( + + + + ))} + + + + + + + + ); +}; + +// 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 handlePropertyChange = (event) => { + const value = event.target.value; + setSelectedProperties(typeof value === 'string' ? value.split(',') : value); + formControl.setValue("selectedProperties", value); + + // Reset property values when selection changes + const currentValues = formControl.getValues("propertyValues") || {}; + const newValues = {}; + value.forEach(prop => { + if (currentValues[prop]) { + newValues[prop] = currentValues[prop]; + } + }); + formControl.setValue("propertyValues", newValues); + formControl.trigger(); + }; + + const handleSelectAll = (event) => { + // Prevent the Select component from treating this as a regular selection + event.stopPropagation(); + + const allSelected = selectedProperties.length === PATCHABLE_PROPERTIES.length; + const newSelection = allSelected ? [] : PATCHABLE_PROPERTIES.map(p => p.property); + setSelectedProperties(newSelection); + formControl.setValue("selectedProperties", newSelection); + + // Reset property values when selection changes + const currentValues = formControl.getValues("propertyValues") || {}; + const newValues = {}; + newSelection.forEach(prop => { + if (currentValues[prop]) { + newValues[prop] = currentValues[prop]; + } + }); + formControl.setValue("propertyValues", newValues); + formControl.trigger(); + }; + + 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 + return ( + handlePropertyValueChange(propertyName, e.target.value)} + placeholder={`Enter new value for ${property?.label || propertyName}`} + variant="filled" + /> + ); + }; + + const isAllSelected = selectedProperties.length === PATCHABLE_PROPERTIES.length; + const isIndeterminate = selectedProperties.length > 0 && selectedProperties.length < PATCHABLE_PROPERTIES.length; + + return ( + + + Select Properties to update + + Choose which user properties you want to modify and provide the new values. + + + + + Properties to update + + + + {selectedProperties.length > 0 && ( + + + Property Values + + {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 = () => { + // 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, + bulkRequest: true + }); + }; + + return ( + + + Confirm Patch Operation + + Review the users and properties that will be updated, then click Submit to apply the changes. + + + + + + + + + + + + + + {selectedProperties.map(propName => { + const property = PATCHABLE_PROPERTIES.find(p => p.property === propName); + return ( + + ); + })} + + + + + + + + Users to be Modified + + {users?.map((user, index) => ( + + + + ))} + + + + + + + + {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, + }, + }, + { + 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; From 87995fc9dd6117a0a45c87273e0f83b42fee29c0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 25 Jul 2025 17:10:39 -0400 Subject: [PATCH 26/69] audit log tweaks --- .../audit-logs/search-results.js | 89 +++++++++++++------ .../administration/audit-logs/searches.js | 16 +++- 2 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/pages/tenant/administration/audit-logs/search-results.js b/src/pages/tenant/administration/audit-logs/search-results.js index ffc27759416e..88ab5a7bf26b 100644 --- a/src/pages/tenant/administration/audit-logs/search-results.js +++ b/src/pages/tenant/administration/audit-logs/search-results.js @@ -2,9 +2,12 @@ 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 } from "@mui/material"; +import { Button, SvgIcon, Box } from "@mui/material"; +import { ManageSearch } from "@mui/icons-material"; +import { useDialog } from "/src/hooks/use-dialog"; const searchResultsColumns = [ "createdDateTime", @@ -21,6 +24,7 @@ const Page = () => { const [searchId, setSearchId] = useState(null); const [searchName, setSearchName] = useState(null); const [isReady, setIsReady] = useState(false); + const processLogsDialog = useDialog(); useEffect(() => { if (router.isReady) { @@ -44,6 +48,20 @@ const Page = () => { 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", @@ -52,32 +70,49 @@ const Page = () => { }; return ( - - - - } - > - Back to Searches - - } - apiUrl="/api/ListAuditLogSearches" - apiDataKey="Results" - simpleColumns={searchResultsColumns} - queryKey={`AuditLogSearchResults-${searchId}`} - apiData={{ - Type: "SearchResults", - SearchId: searchId, - }} - offCanvas={offcanvas} - actions={[]} - /> + <> + + + + + } + apiUrl="/api/ListAuditLogSearches" + apiDataKey="Results" + simpleColumns={searchResultsColumns} + queryKey={`AuditLogSearchResults-${searchId}`} + apiData={{ + Type: "SearchResults", + SearchId: searchId, + }} + offCanvas={offcanvas} + actions={[]} + /> + + + ); }; diff --git a/src/pages/tenant/administration/audit-logs/searches.js b/src/pages/tenant/administration/audit-logs/searches.js index fa4ea43be7db..3c5a92dcfbde 100644 --- a/src/pages/tenant/administration/audit-logs/searches.js +++ b/src/pages/tenant/administration/audit-logs/searches.js @@ -7,9 +7,9 @@ import { Button, Accordion, AccordionSummary, AccordionDetails, Typography } fro import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { EyeIcon, MagnifyingGlassIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { EyeIcon } from "@heroicons/react/24/outline"; import { Grid } from "@mui/system"; -import { Add } from "@mui/icons-material"; +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"; @@ -26,6 +26,18 @@ const actions = [ 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 = () => { From 4331c7f9e865ff35964b948cbf920eea3a650a18 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 25 Jul 2025 17:10:43 -0400 Subject: [PATCH 27/69] Update timers.js --- src/pages/cipp/advanced/timers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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: , }, ]} /> From 8ab9ca69a941cf00c42551bd7b1596fdc68c2280 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 25 Jul 2025 19:21:48 -0400 Subject: [PATCH 28/69] add alert fixes fix custom vars not adding text field fix onCreateOption for autocomplete --- src/components/CippComponents/CippAutocomplete.jsx | 8 +++++--- .../administration/alert-configuration/alert.jsx | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 4fb18221e0b0..d9ae500cb256 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -300,7 +300,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 +315,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 +336,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 || ""; diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index d2b59fa0fe7c..bcc300a85238 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -470,6 +470,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; + }} /> From 7c7c77826d4bfe12620dc1ad4cae4ce3d811ead8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 25 Jul 2025 19:25:25 -0400 Subject: [PATCH 29/69] add common schema props --- src/data/AuditLogSchema.json | 1784 +++++++++++++++++++++++++++------- 1 file changed, 1434 insertions(+), 350 deletions(-) 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 From 001992bdd306128ea6d6852858b4e18ea4099a9b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 25 Jul 2025 19:26:56 -0400 Subject: [PATCH 30/69] disable console logging --- src/components/CippComponents/CippFormCondition.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx index 811d7d24fabe..b3630acd7098 100644 --- a/src/components/CippComponents/CippFormCondition.jsx +++ b/src/components/CippComponents/CippFormCondition.jsx @@ -53,7 +53,7 @@ export const CippFormCondition = (props) => { } } - console.log("CippFormCondition: ", { + /*console.log("CippFormCondition: ", { watcher, watchedValue, compareTargetValue, @@ -62,7 +62,7 @@ export const CippFormCondition = (props) => { action, field, propertyName, - }); + });*/ // Function to recursively extract field names from child components const extractFieldNames = (children) => { From 1385a13e0a890e3987af7a73cf7469d1732ad54e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 27 Jul 2025 12:52:04 -0400 Subject: [PATCH 31/69] use CIPP variants if available --- src/pages/tenant/administration/audit-logs/log.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/administration/audit-logs/log.js b/src/pages/tenant/administration/audit-logs/log.js index b466b84217db..4e8c768b3f65 100644 --- a/src/pages/tenant/administration/audit-logs/log.js +++ b/src/pages/tenant/administration/audit-logs/log.js @@ -90,7 +90,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 }, { From b9271fee46f8ba70477b901f1f19168219f524a8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 27 Jul 2025 12:53:23 -0400 Subject: [PATCH 32/69] executive summary preview --- src/components/ExecutiveReportButton.js | 1796 ++++++++++++++--------- 1 file changed, 1089 insertions(+), 707 deletions(-) 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} + + + - - )} - + + + + + ); }; From fdc9f26fb7535f1f9ec73ea26a4e20458f63733d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 27 Jul 2025 14:23:13 -0400 Subject: [PATCH 33/69] update resolver to also look for unmatched guids in partner tenant prefer UPN to displayname --- src/hooks/use-guid-resolver.js | 62 ++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/src/hooks/use-guid-resolver.js b/src/hooks/use-guid-resolver.js index 44a6e51d880e..1325722fc872 100644 --- a/src/hooks/use-guid-resolver.js +++ b/src/hooks/use-guid-resolver.js @@ -263,15 +263,49 @@ export const useGuidResolver = (manualTenant = null) => { const returnedGuids = new Set(data.value.map((item) => item.id)); const notReturned = [...processedGuids].filter((guid) => !returnedGuids.has(guid)); - // Add them to the notFoundGuids set + // Add unresolved GUIDs to partner tenant fallback lookup if (notReturned.length > 0) { - notReturned.forEach((guid) => notFoundGuidsRef.current.add(guid)); + 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 = []; - setIsLoadingGuids(false); + + // Only set loading to false if we don't have pending partner lookups + if (notReturned.length === 0) { + setIsLoadingGuids(false); + } } }, }); @@ -325,9 +359,9 @@ export const useGuidResolver = (manualTenant = null) => { // 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 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 @@ -337,6 +371,22 @@ export const useGuidResolver = (manualTenant = null) => { } }); + // 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 })); From f113477f33e85c9be296508d32a2f1975a5c9af8 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 28 Jul 2025 10:22:55 +0200 Subject: [PATCH 34/69] Bulk user property wizard better formatting and bulk sending to API --- .../administration/users/patch-wizard.jsx | 209 ++++++++++++------ 1 file changed, 139 insertions(+), 70 deletions(-) diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx index 8769a43fc093..839dbaebd7df 100644 --- a/src/pages/identity/administration/users/patch-wizard.jsx +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -19,7 +19,9 @@ import { ListItemText, Button, Switch, - FormControlLabel + FormControlLabel, + IconButton, + Tooltip } from "@mui/material"; import { Grid } from "@mui/system"; import { CippWizardStepButtons } from "/src/components/CippWizard/CippWizardStepButtons"; @@ -29,6 +31,8 @@ import { getCippTranslation } from "/src/utils/get-cipp-translation"; import { getCippFormatting } from "/src/utils/get-cipp-formatting"; 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 = [ @@ -104,43 +108,82 @@ const PATCHABLE_PROPERTIES = [ } ]; -// Step 1: Display users to be patched +// Step 1: Display users to be updated const UsersDisplayStep = (props) => { - const { onNextStep, onPreviousStep, formControl, currentStep, users } = 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. + The following users will be updated with the properties you select in the next step. You can remove users from this list if needed. - - - - Selected Users ({users?.length || 0}) - - {users?.map((user, index) => ( - - - - ))} - - - - + {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} /> ); @@ -304,7 +347,7 @@ const PropertySelectionStep = (props) => { {selectedProperties.length > 0 && ( - Property Values + Properties to update {selectedProperties.map(renderPropertyInput)} @@ -334,6 +377,12 @@ const ConfirmationStep = (props) => { }); 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 = { @@ -352,66 +401,85 @@ const ConfirmationStep = (props) => { patchUsersApi.mutate({ url: "/api/PatchUser", data: patchData, - bulkRequest: true }); }; + // 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 Patch Operation + Confirm User Updates - Review the users and properties that will be updated, then click Submit to apply the changes. + 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 to be Modified - - {users?.map((user, index) => ( - - - - ))} - - - + + + + )} + + {users && users.length > 0 ? ( + + ) : ( + + + + No users to update. Please go back and select users. + + + + )} @@ -431,7 +499,7 @@ const ConfirmationStep = (props) => { size="large" type="button" variant="contained" - disabled={patchUsersApi.isPending || selectedProperties.length === 0} + disabled={patchUsersApi.isPending || selectedProperties.length === 0 || !users || users.length === 0} onClick={handleSubmit} > {patchUsersApi.isSuccess ? "Resubmit" : "Submit"} @@ -474,6 +542,7 @@ const Page = () => { component: UsersDisplayStep, componentProps: { users: users, + onUsersChange: setUsers, }, }, { From 3365c7375ffd6c9d61abdb8ddf11dafbdf9bb4a5 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 28 Jul 2025 15:45:54 +0200 Subject: [PATCH 35/69] Change action name --- src/components/CippComponents/CippUserActions.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 09e8b93bffdd..59312d5401f4 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -458,7 +458,7 @@ export const CippUserActions = () => { condition: () => canWriteUser, }, { - label: "Patch Users", + label: "Edit Properties", icon: , multiPost: true, noConfirm: true, From cc18d37f960ede0875c3b312f753e7cf1fef58ac Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:26:21 +0200 Subject: [PATCH 36/69] drift --- .../CippStandards/CippStandardAccordion.jsx | 146 +++++- .../CippStandards/CippStandardsSideBar.jsx | 140 +++--- src/layouts/config.js | 9 +- src/pages/tenant/standards/index.js | 17 - .../standards/list-applied-standards/index.js | 17 - .../CippDriftNotificationSettings.jsx | 97 ++++ .../CippDriftTemplateSelection.jsx | 120 +++++ .../apply-drift-template/index.js | 49 ++ .../classic-standard-alignment/index.js | 44 ++ .../list-standards/classic-standards/index.js | 192 ++++++++ .../list-standards/drift-alignment/index.js | 44 ++ .../list-standards/drift-templates/index.js | 114 +++++ .../tenant/standards/list-standards/index.js | 188 ++------ .../standards/list-standards/tabOptions.json | 18 + .../tenant/standards/manageDrift/history.js | 114 +++++ .../tenant/standards/manageDrift/index.js | 456 ++++++++++++++++++ .../manageDrift/policies-deployed.js | 229 +++++++++ .../standards/manageDrift/recover-policies.js | 185 +++++++ .../standards/manageDrift/tabOptions.json | 18 + src/pages/tenant/standards/template.jsx | 32 +- 20 files changed, 1951 insertions(+), 278 deletions(-) delete mode 100644 src/pages/tenant/standards/index.js delete mode 100644 src/pages/tenant/standards/list-applied-standards/index.js create mode 100644 src/pages/tenant/standards/list-standards/apply-drift-template/CippDriftNotificationSettings.jsx create mode 100644 src/pages/tenant/standards/list-standards/apply-drift-template/CippDriftTemplateSelection.jsx create mode 100644 src/pages/tenant/standards/list-standards/apply-drift-template/index.js create mode 100644 src/pages/tenant/standards/list-standards/classic-standard-alignment/index.js create mode 100644 src/pages/tenant/standards/list-standards/classic-standards/index.js create mode 100644 src/pages/tenant/standards/list-standards/drift-alignment/index.js create mode 100644 src/pages/tenant/standards/list-standards/drift-templates/index.js create mode 100644 src/pages/tenant/standards/list-standards/tabOptions.json create mode 100644 src/pages/tenant/standards/manageDrift/history.js create mode 100644 src/pages/tenant/standards/manageDrift/index.js create mode 100644 src/pages/tenant/standards/manageDrift/policies-deployed.js create mode 100644 src/pages/tenant/standards/manageDrift/recover-policies.js create mode 100644 src/pages/tenant/standards/manageDrift/tabOptions.json diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index 8e1532560be3..988e72308111 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,38 @@ const CippStandardAccordion = ({ control: formControl.control, }); + // 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`, true); + formControl.setValue(`${standardName}.action`, [ + { label: "Report", value: "Report" }, + { label: "Remediate", value: "Remediate" }, + ]); + } + }); + } + }, [isDriftMode, selectedStandards, formControl]); + // Check if a standard is configured based on its values const isStandardConfigured = (standardName, standard, values) => { if (!values) return false; @@ -257,6 +290,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 @@ -646,7 +692,8 @@ const CippStandardAccordion = ({ {accordionTitle} - {selectedActions && selectedActions?.length > 0 && ( + {/* Hide action chips in drift mode */} + {!isDriftMode && selectedActions && selectedActions?.length > 0 && ( <> {selectedActions?.map((action, index) => ( @@ -696,12 +743,7 @@ const CippStandardAccordion = ({ components={{ // Make links open in new tab with security attributes a: ({ href, children, ...props }) => ( - + {children} ), @@ -751,23 +793,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/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 601ebe1e7aa3..814a44da9725 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -65,6 +65,7 @@ const CippStandardsSideBar = ({ createDialog, edit, onSaveSuccess, + isDriftMode = false, }) => { const [currentStep, setCurrentStep] = useState(0); const [savedItem, setSavedItem] = useState(null); @@ -136,6 +137,15 @@ const CippStandardsSideBar = ({ + {/* Hidden field to mark drift templates */} + {isDriftMode && ( + + )} - - - {watchForm.tenantFilter?.some( - (tenant) => tenant.value === "AllTenants" || tenant.type === "Group" - ) && ( + {/* Hide tenant selector and schedule options in drift mode */} + {!isDriftMode && ( <> - - )} - {updatedAt.date && ( - <> + {watchForm.tenantFilter?.some( + (tenant) => tenant.value === "AllTenants" || tenant.type === "Group" + ) && ( + <> + + + + )} + {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) => ( @@ -247,7 +267,9 @@ const CippStandardsSideBar = ({ createDialog={createDialog} title="Add Standard" api={{ - confirmText: watchForm.runManually + confirmText: isDriftMode + ? "This template will only run after assigning it through the Drift Template setup wizard" + : 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", @@ -262,6 +284,8 @@ const CippStandardsSideBar = ({ ...(edit ? { GUID: "GUID" } : {}), ...(savedItem ? { GUID: savedItem } : {}), runManually: "runManually", + isDriftTemplate: "isDriftTemplate", + ...(isDriftMode ? { type: "drift" } : {}), }, }} row={formControl.getValues()} diff --git a/src/layouts/config.js b/src/layouts/config.js index 3b2582e27387..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", 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/apply-drift-template/CippDriftNotificationSettings.jsx b/src/pages/tenant/standards/list-standards/apply-drift-template/CippDriftNotificationSettings.jsx new file mode 100644 index 000000000000..1cedee2ec468 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/apply-drift-template/CippDriftNotificationSettings.jsx @@ -0,0 +1,97 @@ +import { Stack, Typography, Alert, FormControl, FormLabel, Box } from "@mui/material"; +import { CippWizardStepButtons } from "/src/components/CippWizard/CippWizardStepButtons"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { Grid } from "@mui/system"; + +export const CippDriftNotificationSettings = (props) => { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + + return ( + + + Notification Settings + + + Configure how you want to receive drift reports when configuration changes are detected. + You can specify an email address and choose additional notification methods. + + + + Email Configuration + + + + + + + + + + + + Additional Notification Methods + + + + + + + + + + + + + + + + + + ); +}; + +export default CippDriftNotificationSettings; diff --git a/src/pages/tenant/standards/list-standards/apply-drift-template/CippDriftTemplateSelection.jsx b/src/pages/tenant/standards/list-standards/apply-drift-template/CippDriftTemplateSelection.jsx new file mode 100644 index 000000000000..40b981f51337 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/apply-drift-template/CippDriftTemplateSelection.jsx @@ -0,0 +1,120 @@ +import { Stack, Typography, Card, CardContent, Chip, Box } from "@mui/material"; +import { CippWizardStepButtons } from "/src/components/CippWizard/CippWizardStepButtons"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { useEffect, useState } from "react"; +import { useWatch } from "react-hook-form"; + +export const CippDriftTemplateSelection = (props) => { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + const [selectedTemplate, setSelectedTemplate] = useState(null); + + // Watch for template selection changes + const watchedTemplate = useWatch({ + control: formControl.control, + name: "driftTemplate", + }); + + // API call to get drift standard templates + const driftTemplates = ApiGetCall({ + url: "/api/listStandardTemplates", + queryKey: "DriftStandardTemplates", + data: { + type: "drift", // Filter for drift templates only + }, + }); + + // Update selected template when watcher changes + useEffect(() => { + if (driftTemplates.isSuccess && watchedTemplate?.value) { + const template = driftTemplates.data.find((t) => t.GUID === watchedTemplate.value); + setSelectedTemplate(template); + } + }, [driftTemplates, watchedTemplate]); + + // Template summary component + const TemplateSummary = ({ template }) => { + if (!template) return null; + + const standardsCount = template.standards ? Object.keys(template.standards)?.length : 0; + const tenantCount = + template.tenantFilter === "AllTenants" + ? "All Tenants" + : Array.isArray(template.tenantFilter) + ? template.tenantFilter?.length + : 1; + + return ( + + + + {template.templateName} + + + {template.description || "No description available"} + + + + + + {template.runManually && } + + + {template.standards && ( + + + Included Standards: + + + {Object.keys(template.standards).map((standard) => ( + + ))} + + + )} + + + ); + }; + + return ( + + + Select Drift Standard Template + + Choose a standard template that will be used to monitor configuration drift across your + selected tenants. + + + + + {/* Template Summary */} + + + + + + ); +}; + +export default CippDriftTemplateSelection; diff --git a/src/pages/tenant/standards/list-standards/apply-drift-template/index.js b/src/pages/tenant/standards/list-standards/apply-drift-template/index.js new file mode 100644 index 000000000000..292feeffffc8 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/apply-drift-template/index.js @@ -0,0 +1,49 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippWizardConfirmation } from "/src/components/CippWizard/CippWizardConfirmation"; +import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx"; +import { CippTenantStep } from "/src/components/CippWizard/CippTenantStep.jsx"; +import { CippDriftTemplateSelection } from "./CippDriftTemplateSelection"; +import { CippDriftNotificationSettings } from "./CippDriftNotificationSettings"; + +const Page = () => { + const steps = [ + { + title: "Step 1", + description: "Information & Tenant Selection", + component: CippTenantStep, + componentProps: { + type: "multiple", + allTenants: true, + }, + }, + { + title: "Step 2", + description: "Drift Standard Template Selection", + component: CippDriftTemplateSelection, + }, + { + title: "Step 3", + description: "Notification Settings", + component: CippDriftNotificationSettings, + }, + { + title: "Step 4", + description: "Confirmation", + component: CippWizardConfirmation, + }, + ]; + + return ( + <> + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/standards/list-standards/classic-standard-alignment/index.js b/src/pages/tenant/standards/list-standards/classic-standard-alignment/index.js new file mode 100644 index 000000000000..ec72da3805dc --- /dev/null +++ b/src/pages/tenant/standards/list-standards/classic-standard-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 = "Classic Standard 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/classic-standards/index.js b/src/pages/tenant/standards/list-standards/classic-standards/index.js new file mode 100644 index 000000000000..a7fe79e37306 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js @@ -0,0 +1,192 @@ +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 = "Standard 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]", + 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 ( + }> + Add Template + + } + actions={actions} + tableFilter={tableFilter} + simpleColumns={[ + "templateName", + "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/drift-templates/index.js b/src/pages/tenant/standards/list-standards/drift-templates/index.js new file mode 100644 index 000000000000..b26434d52d60 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/drift-templates/index.js @@ -0,0 +1,114 @@ +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 { TabbedLayout } from "/src/layouts/TabbedLayout"; +import Link from "next/link"; +import { CopyAll, Delete, AddBox, Edit, GitHub } from "@mui/icons-material"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import tabOptions from "../tabOptions.json"; + +const Page = () => { + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const pageTitle = "Drift Templates"; + const actions = [ + { + label: "Edit Template", + 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", + target: "_self", + }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveStandardTemplate", + icon: , + data: { + ID: "GUID", + }, + confirmText: "Are you sure you want to delete [templateName]?", + multiPost: false, + }, + ]; + const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" }); + + return ( + }> + Add Template + + } + actions={actions} + simpleColumns={["templateName", "updatedAt", "updatedBy"]} + queryKey="listStandardTemplates-drift" + /> + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index 0c479d6fa838..1e08b4cd8b01 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -1,186 +1,84 @@ -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 { CopyAll, Delete, PlayArrow, Add, Edit, GitHub, Download } 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 = "Drift Templates"; const actions = [ { - label: "View Tenant Report", - link: "/tenant/standards/compare?templateId=[GUID]", + label: "Manage Drift", + link: "/tenant/standards/manageDrift?templateId=[standardId]&tenantFilter=[tenantFilter]", 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", - 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", + label: "Remove assigned Drift Standard", type: "POST", - url: "/api/ExecCommunityRepo", - icon: , + url: "/api/RemoveStandardTemplate", + icon: , data: { - Action: "UploadTemplate", - GUID: "GUID", + ID: "standardId", }, - 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, + confirmText: + "Are you sure you want to remove the assigned Drift Standard? This does not undo any customizations or applied policies.", + multiPost: false, }, { - label: "Delete Template", + label: "Remove Drift Customization", type: "POST", url: "/api/RemoveStandardTemplate", icon: , data: { - ID: "GUID", + ID: "standardId", }, - 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, }, ]; - 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", + "alignmentScore", + "acceptedDeviations", + "currentDeviations", ]} - queryKey="listStandardTemplates" + queryKey="ListTenantDrift" /> ); }; -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..45a8b56107de --- /dev/null +++ b/src/pages/tenant/standards/list-standards/tabOptions.json @@ -0,0 +1,18 @@ +[ + { + "label": "Drift Alignment", + "path": "/tenant/standards/list-standards" + }, + { + "label": "Classic Standard Alignment", + "path": "/tenant/standards/list-standards/classic-standard-alignment" + }, + { + "label": "Drift Templates", + "path": "/tenant/standards/list-standards/drift-templates" + }, + { + "label": "Classic Templates", + "path": "/tenant/standards/list-standards/classic-standards" + } +] diff --git a/src/pages/tenant/standards/manageDrift/history.js b/src/pages/tenant/standards/manageDrift/history.js new file mode 100644 index 000000000000..53f77047a9c8 --- /dev/null +++ b/src/pages/tenant/standards/manageDrift/history.js @@ -0,0 +1,114 @@ +import { useState } from "react"; +import { Box, Stack, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { CippChartCard } from "/src/components/CippCards/CippChartCard"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { useRouter } from "next/router"; +import { Policy } from "@mui/icons-material"; +import tabOptions from "/src/pages/tenant/standards/manageDrift/tabOptions.json"; + +const Page = () => { + const router = useRouter(); + const { templateId } = router.query; + + // Mock data for demonstration - replace with actual API call + const driftHistoryData = ApiGetCall({ + url: `/api/GetDriftHistory`, + data: { templateId }, + queryKey: `GetDriftHistory-${templateId}`, + }); + + // Generate mock timeline data for the last 30 days + const generateTimelineData = () => { + const days = []; + const deviations = []; + const acceptedDeviations = []; + const deniedDeviations = []; + const inAlignment = []; + + for (let i = 29; i >= 0; i--) { + const date = new Date(); + date.setDate(date.getDate() - i); + days.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); + + // Mock data - replace with actual data processing + deviations.push(Math.floor(Math.random() * 20) + 5); + acceptedDeviations.push(Math.floor(Math.random() * 8) + 2); + deniedDeviations.push(Math.floor(Math.random() * 5) + 1); + inAlignment.push(Math.floor(Math.random() * 15) + 10); + } + + return { days, deviations, acceptedDeviations, deniedDeviations, inAlignment }; + }; + + const timelineData = generateTimelineData(); + + // Format data like secureScore example - array of objects with name and data + const timelineChartSeries = [ + { + name: "Deviations Detected", + data: timelineData.days.map((day, index) => ({ + x: day, + y: timelineData.deviations[index], + })), + }, + { + name: "Accepted deviations - Customer Specific", + data: timelineData.days.map((day, index) => ({ + x: day, + y: timelineData.acceptedDeviations[index], + })), + }, + { + name: "Denied Deviation", + data: timelineData.days.map((day, index) => ({ + x: day, + y: timelineData.deniedDeviations[index], + })), + }, + ]; + + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + return ( + + + + Drift History + + Historical timeline of drift deviations, acceptances, denials, and alignment status over the last 30 days. + + + + {/* Single Timeline Chart */} + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/tenant/standards/manageDrift/index.js b/src/pages/tenant/standards/manageDrift/index.js new file mode 100644 index 000000000000..c0d4d366c7b1 --- /dev/null +++ b/src/pages/tenant/standards/manageDrift/index.js @@ -0,0 +1,456 @@ +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, +} from "@mui/icons-material"; +import { Box, Stack, Typography, Button, Menu, MenuItem } 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 tabOptions from "./tabOptions.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 [whatIfAnchorEl, setWhatIfAnchorEl] = useState(null); + const [apiDialogOpen, setApiDialogOpen] = useState(false); + const [apiData, setApiData] = useState({}); + + // API calls for drift data + const driftApi = ApiGetCall({ + url: "/api/listTenantDrift", + data: { + TenantFilter: tenantFilter, + }, + queryKey: `TenantDrift-${tenantFilter}`, + }); + + // API call for available standards (for What If dropdown) + const standardsApi = ApiGetCall({ + url: "/api/ListStandards", + queryKey: "ListStandards-drift", + }); + + // 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.customerSpecificDeviations || 0; + + // Collect all current deviations + if (item.currentDeviations && Array.isArray(item.currentDeviations)) { + acc.currentDeviations.push(...item.currentDeviations.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, + currentDeviations: [], + latestDataCollection: null + }); + + const chartLabels = [ + "Accepted Deviations", + "Current Deviations", + "Aligned Policies", + "Customer Specific Deviations", + ]; + const chartSeries = [ + processedDriftData.acceptedDeviationsCount || 0, + processedDriftData.currentDeviationsCount || 0, + processedDriftData.alignedCount || 0, + processedDriftData.customerSpecificDeviations || 0, + ]; + + // Transform currentDeviations into deviation items for display + const getDeviationIcon = (state) => { + switch (state?.toLowerCase()) { + case "current": + return ; + case "denied": + 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 "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 "accepted": + return "Accepted Deviation"; + case "customerspecific": + return "Customer Specific"; + default: + return "Deviation"; + } + }; + + const deviationItems = (processedDriftData.currentDeviations || []).map((deviation, index) => ({ + id: index + 1, + cardLabelBox: { + cardLabelBoxHeader: getDeviationIcon(deviation.state), + }, + text: deviation.standardName || "Unknown Standard", + subtext: deviation.standardDescription || "No description available", + statusColor: getDeviationColor(deviation.state), + statusText: getDeviationStatusText(deviation.state), + propertyItems: [ + { label: "Standard Name", value: deviation.standardName || "N/A" }, + { label: "Description", value: deviation.standardDescription || "N/A" }, + { label: "Expected Value", value: deviation.expectedValue || "N/A" }, + { label: "Current Value", value: deviation.receivedValue || "N/A" }, + { label: "Status", value: getDeviationStatusText(deviation.state) }, + { + label: "Last Updated", + value: processedDriftData.latestDataCollection + ? new Date(processedDriftData.latestDataCollection).toLocaleString() + : "N/A", + }, + ].filter((item) => item.value !== "N/A"), // Filter out N/A values + })); + + 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; + switch (action) { + case "accept-customer-specific": + status = "CustomerSpecific"; + break; + case "accept": + status = "Accepted"; + break; + case "bring-into-standard": + status = "Accepted"; + break; + case "deny-remove": + status = "Denied"; + break; + default: + return; + } + + // Use CippApiDialog for the API call + const apiData = { + url: `/api/ExecUpdateDriftDeviation?TenantFilter=${tenantFilter}`, + data: { + deviations: [{ + standardName: deviation.standardName, + status: status, + }], + }, + }; + + // Set the API data for CippApiDialog + setApiData(apiData); + setApiDialogOpen(true); + + handleMenuClose(itemId); + }; + + const handleBulkAction = (action) => { + if (!processedDriftData.currentDeviations || processedDriftData.currentDeviations.length === 0) { + setBulkActionsAnchorEl(null); + return; + } + + let status; + switch (action) { + case "accept-all-customer-specific": + status = "CustomerSpecific"; + break; + case "accept-all": + status = "Accepted"; + break; + case "deny-all": + status = "Denied"; + break; + default: + setBulkActionsAnchorEl(null); + return; + } + + const deviations = processedDriftData.currentDeviations.map(deviation => ({ + standardName: deviation.standardName, + status: status, + })); + + // Use CippApiDialog for the API call + const apiData = { + url: `/api/ExecUpdateDriftDeviation?TenantFilter=${tenantFilter}`, + data: { + deviations: deviations, + }, + }; + + // Set the API data for CippApiDialog + setApiData(apiData); + setApiDialogOpen(true); + + setBulkActionsAnchorEl(null); + }; + + const handleWhatIfAction = (standardId) => { + console.log(`What If Analysis with standard: ${standardId}`); + // Here you would implement the what-if analysis + setWhatIfAnchorEl(null); + }; + + // Actions for the ActionsMenu + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + driftApi.refetch(); + standardsApi.refetch(); + if (templateId) { + comparisonApi.refetch(); + } + }, + }, + ]; + + // Process standards data for "What If" dropdown + const availableStandards = (standardsApi.data || []).map((standard) => ({ + id: standard.GUID || standard.id || standard.name, + name: standard.displayName || standard.name || "Unknown Standard", + })); + + // Add action buttons to each deviation item + const deviationItemsWithActions = deviationItems.map((item) => ({ + ...item, + actionButton: ( + <> + + handleMenuClose(item.id)} + > + handleAction("accept-customer-specific", item.id)}> + + Accept Deviation - Customer Specific + + handleAction("accept", item.id)}> + + Accept Deviation + + handleAction("bring-into-standard", item.id)}> + + Bring into Drift Standard + + handleAction("deny-remove", item.id)}> + + Deny - Remove Policy + + + + ), + })); + + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + return ( + + + + + {/* Left side - Chart */} + + + + + {/* Right side - Deviation Management */} + + + {/* 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 + + handleBulkAction("deny-all")}> + + Deny All Deviations + + + + {/* What If Button */} + + setWhatIfAnchorEl(null)} + > + {availableStandards.map((standard) => ( + handleWhatIfAction(standard.id)}> + + {standard.name} + + ))} + + + + + + + + + setApiDialogOpen(false)} + data={apiData} + onSuccess={() => { + driftApi.refetch(); + setApiDialogOpen(false); + }} + /> + + ); +}; + +ManageDriftPage.getLayout = (page) => {page}; + +export default ManageDriftPage; diff --git a/src/pages/tenant/standards/manageDrift/policies-deployed.js b/src/pages/tenant/standards/manageDrift/policies-deployed.js new file mode 100644 index 000000000000..74040d3db0f3 --- /dev/null +++ b/src/pages/tenant/standards/manageDrift/policies-deployed.js @@ -0,0 +1,229 @@ +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, +} 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, + }); + + // 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 + const getStatus = (standardKey) => { + const comparisonKey = `standards.${standardKey}`; + const value = comparisonData[comparisonKey]?.Value; + return value === true ? "Deployed" : "Deviation"; + }; + + // 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(); + }; + + // 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) => ({ + id: index + 1, + name: template.TemplateList?.label || "Unknown Template", + category: "Intune Template", + platform: "Multi-Platform", + status: getStatus(`IntuneTemplate.${template.TemplateList?.value}`), + lastModified: getLastRefresh(`IntuneTemplate.${template.TemplateList?.value}`), + assignedGroups: template.AssignTo || "N/A", + templateValue: template.TemplateList?.value, + })); + + // Process Conditional Access Templates + const conditionalAccessPolicies = (templateStandards.ConditionalAccessTemplate || []).map( + (template, index) => ({ + id: index + 1, + name: template.TemplateList?.label || "Unknown Policy", + state: template.state || "Unknown", + conditions: "Conditional Access Policy", + controls: "Access Control", + lastModified: getLastRefresh(`ConditionalAccessTemplate.${template.TemplateList?.value}`), + status: getStatus(`ConditionalAccessTemplate.${template.TemplateList?.value}`), + templateValue: template.TemplateList?.value, + }) + ); + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + standardsApi.refetch(); + comparisonApi.refetch(); + }, + }, + ]; + 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/manageDrift/recover-policies.js b/src/pages/tenant/standards/manageDrift/recover-policies.js new file mode 100644 index 000000000000..73e68b63d15f --- /dev/null +++ b/src/pages/tenant/standards/manageDrift/recover-policies.js @@ -0,0 +1,185 @@ +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 } 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), + }, + }); + }; + + 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/manageDrift/tabOptions.json b/src/pages/tenant/standards/manageDrift/tabOptions.json new file mode 100644 index 000000000000..e7719869cb44 --- /dev/null +++ b/src/pages/tenant/standards/manageDrift/tabOptions.json @@ -0,0 +1,18 @@ +[ + { + "label": "Manage Drift", + "path": "/tenant/standards/manageDrift" + }, + { + "label": "Policies and Settings Deployed", + "path": "/tenant/standards/manageDrift/policies-deployed" + }, + { + "label": "History", + "path": "/tenant/standards/manageDrift/history" + }, + { + "label": "Recover Policies", + "path": "/tenant/standards/manageDrift/recover-policies" + } +] \ No newline at end of file diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index c5f32f98e8f0..e1988d5103ca 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -30,6 +30,16 @@ const Page = () => { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [currentStep, setCurrentStep] = useState(0); 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 +55,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 +70,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 +261,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 // For drift mode, only require steps 1, 3, and 4 (skip tenant requirement) + : (!_.get(watchForm, "tenantFilter") || + !_.get(watchForm, "tenantFilter").length || + currentStep < 3); const actions = []; @@ -309,7 +320,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") + } )} - + <> + + + } actions={actions} tableFilter={tableFilter} simpleColumns={[ "templateName", + "type", "tenantFilter", "excludedTenants", "updatedAt", diff --git a/src/pages/tenant/standards/list-standards/drift-templates/index.js b/src/pages/tenant/standards/list-standards/drift-templates/index.js deleted file mode 100644 index b26434d52d60..000000000000 --- a/src/pages/tenant/standards/list-standards/drift-templates/index.js +++ /dev/null @@ -1,114 +0,0 @@ -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 { TabbedLayout } from "/src/layouts/TabbedLayout"; -import Link from "next/link"; -import { CopyAll, Delete, AddBox, Edit, GitHub } from "@mui/icons-material"; -import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; -import tabOptions from "../tabOptions.json"; - -const Page = () => { - const integrations = ApiGetCall({ - url: "/api/ListExtensionsConfig", - queryKey: "Integrations", - refetchOnMount: false, - refetchOnReconnect: false, - }); - const pageTitle = "Drift Templates"; - const actions = [ - { - label: "Edit Template", - 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", - target: "_self", - }, - { - label: "Save to GitHub", - type: "POST", - url: "/api/ExecCommunityRepo", - icon: , - data: { - Action: "UploadTemplate", - GUID: "GUID", - }, - fields: [ - { - label: "Repository", - name: "FullName", - type: "select", - api: { - url: "/api/ListCommunityRepos", - data: { - WriteAccess: true, - }, - queryKey: "CommunityRepos-Write", - dataKey: "Results", - valueField: "FullName", - labelField: "FullName", - }, - multiple: false, - creatable: false, - required: true, - validators: { - required: { value: true, message: "This field is required" }, - }, - }, - { - label: "Commit Message", - placeholder: "Enter a commit message for adding this file to GitHub", - name: "Message", - type: "textField", - multiline: true, - required: true, - rows: 4, - }, - ], - confirmText: "Are you sure you want to save this template to the selected repository?", - condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, - }, - { - label: "Delete Template", - type: "POST", - url: "/api/RemoveStandardTemplate", - icon: , - data: { - ID: "GUID", - }, - confirmText: "Are you sure you want to delete [templateName]?", - multiPost: false, - }, - ]; - const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" }); - - return ( - }> - Add Template - - } - actions={actions} - simpleColumns={["templateName", "updatedAt", "updatedBy"]} - queryKey="listStandardTemplates-drift" - /> - ); -}; - -Page.getLayout = (page) => ( - - {page} - -); - -export default Page; diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index 1e08b4cd8b01..daf7f0626b6a 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -3,74 +3,60 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import Link from "next/link"; -import { CopyAll, Delete, PlayArrow, Add, Edit, GitHub, Download } from "@mui/icons-material"; +import { Delete, Add } from "@mui/icons-material"; import { EyeIcon } from "@heroicons/react/24/outline"; import tabOptions from "./tabOptions.json"; const Page = () => { - const pageTitle = "Drift Templates"; + const pageTitle = "Standard & Drift Alignment"; + const actions = [ { - label: "Manage Drift", - link: "/tenant/standards/manageDrift?templateId=[standardId]&tenantFilter=[tenantFilter]", + label: "View Tenant Report", + link: "/tenant/standards/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , color: "info", target: "_self", }, { - label: "Remove assigned Drift Standard", - type: "POST", - url: "/api/RemoveStandardTemplate", - icon: , - data: { - ID: "standardId", - }, - confirmText: - "Are you sure you want to remove the assigned Drift Standard? This does not undo any customizations or applied policies.", - multiPost: false, + label: "Manage Drift", + link: "/tenant/standards/manageDrift?templateId=[standardId]&tenantFilter=[tenantFilter]", + icon: , + color: "info", + target: "_self", + condition: (row) => row.standardType === "drift", }, { label: "Remove Drift Customization", type: "POST", - url: "/api/RemoveStandardTemplate", + url: "/api/ExecUpdateDriftDeviation", icon: , data: { - ID: "standardId", + RemoveDriftCustomization: "true", + tenantFilter: "tenantFilter", }, 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", }, ]; return ( - - - - } actions={actions} simpleColumns={[ "tenantFilter", "standardName", + "standardType", "alignmentScore", - "acceptedDeviations", - "currentDeviations", + "LicenseMissingPercentage", + "combinedAlignmentScore", ]} - queryKey="ListTenantDrift" + queryKey="listTenantAlignment" /> ); }; diff --git a/src/pages/tenant/standards/list-standards/tabOptions.json b/src/pages/tenant/standards/list-standards/tabOptions.json index 45a8b56107de..1c522e6ca8ca 100644 --- a/src/pages/tenant/standards/list-standards/tabOptions.json +++ b/src/pages/tenant/standards/list-standards/tabOptions.json @@ -1,18 +1,10 @@ [ { - "label": "Drift Alignment", + "label": "Standard & Drift Alignment", "path": "/tenant/standards/list-standards" }, { - "label": "Classic Standard Alignment", - "path": "/tenant/standards/list-standards/classic-standard-alignment" - }, - { - "label": "Drift Templates", - "path": "/tenant/standards/list-standards/drift-templates" - }, - { - "label": "Classic Templates", + "label": "Templates", "path": "/tenant/standards/list-standards/classic-standards" } ] diff --git a/src/pages/tenant/standards/manageDrift/index.js b/src/pages/tenant/standards/manageDrift/index.js index c0d4d366c7b1..fb129fd301ec 100644 --- a/src/pages/tenant/standards/manageDrift/index.js +++ b/src/pages/tenant/standards/manageDrift/index.js @@ -24,7 +24,9 @@ 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(); @@ -34,8 +36,8 @@ const ManageDriftPage = () => { const [anchorEl, setAnchorEl] = useState({}); const [bulkActionsAnchorEl, setBulkActionsAnchorEl] = useState(null); const [whatIfAnchorEl, setWhatIfAnchorEl] = useState(null); - const [apiDialogOpen, setApiDialogOpen] = useState(false); - const [apiData, setApiData] = useState({}); + const createDialog = useDialog(); + const [actionData, setActionData] = useState({ data: {}, ready: false }); // API calls for drift data const driftApi = ApiGetCall({ @@ -67,46 +69,67 @@ const ManageDriftPage = () => { // 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) + ? 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.customerSpecificDeviations || 0; - - // Collect all current deviations - if (item.currentDeviations && Array.isArray(item.currentDeviations)) { - acc.currentDeviations.push(...item.currentDeviations.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; + 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; + + // Collect all current deviations + if (item.currentDeviations && Array.isArray(item.currentDeviations)) { + acc.currentDeviations.push(...item.currentDeviations.filter((dev) => dev !== null)); + } + + // Collect accepted deviations + if (item.acceptedDeviations && Array.isArray(item.acceptedDeviations)) { + acc.acceptedDeviations.push(...item.acceptedDeviations.filter((dev) => dev !== null)); + } + + // Collect customer specific deviations + if (item.customerSpecificDeviations && Array.isArray(item.customerSpecificDeviations)) { + acc.customerSpecificDeviationsList.push( + ...item.customerSpecificDeviations.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, + currentDeviations: [], + acceptedDeviations: [], + customerSpecificDeviationsList: [], + latestDataCollection: null, } - - return acc; - }, { - acceptedDeviationsCount: 0, - currentDeviationsCount: 0, - alignedCount: 0, - customerSpecificDeviations: 0, - currentDeviations: [], - latestDataCollection: null - }); + ); const chartLabels = [ + "Aligned Policies", "Accepted Deviations", "Current Deviations", - "Aligned Policies", "Customer Specific Deviations", ]; const chartSeries = [ + processedDriftData.alignedCount || 0, processedDriftData.acceptedDeviationsCount || 0, processedDriftData.currentDeviationsCount || 0, - processedDriftData.alignedCount || 0, processedDriftData.customerSpecificDeviations || 0, ]; @@ -156,29 +179,89 @@ const ManageDriftPage = () => { } }; - const deviationItems = (processedDriftData.currentDeviations || []).map((deviation, index) => ({ - id: index + 1, - cardLabelBox: { - cardLabelBoxHeader: getDeviationIcon(deviation.state), - }, - text: deviation.standardName || "Unknown Standard", - subtext: deviation.standardDescription || "No description available", - statusColor: getDeviationColor(deviation.state), - statusText: getDeviationStatusText(deviation.state), - propertyItems: [ - { label: "Standard Name", value: deviation.standardName || "N/A" }, - { label: "Description", value: deviation.standardDescription || "N/A" }, - { label: "Expected Value", value: deviation.expectedValue || "N/A" }, - { label: "Current Value", value: deviation.receivedValue || "N/A" }, - { label: "Status", value: getDeviationStatusText(deviation.state) }, - { - label: "Last Updated", - value: processedDriftData.latestDataCollection - ? new Date(processedDriftData.latestDataCollection).toLocaleString() - : "N/A", - }, - ].filter((item) => item.value !== "N/A"), // Filter out N/A values - })); + // 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) => { + // Get pretty name from standards.json first, then fallback to standardDisplayName, then raw name + const prettyName = + getStandardPrettyName(deviation.standardName) || + deviation.standardDisplayName || + 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), + 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: "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 handleMenuClick = (event, itemId) => { setAnchorEl((prev) => ({ ...prev, [itemId]: event.currentTarget })); @@ -188,86 +271,147 @@ const ManageDriftPage = () => { 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 "bring-into-standard": status = "Accepted"; + actionText = "bring into standard"; break; case "deny-remove": status = "Denied"; + actionText = "deny and remove"; break; default: return; } - // Use CippApiDialog for the API call - const apiData = { - url: `/api/ExecUpdateDriftDeviation?TenantFilter=${tenantFilter}`, + // Set action data for CippApiDialog + setActionData({ data: { - deviations: [{ - standardName: deviation.standardName, - status: status, - }], + deviations: [ + { + standardName: deviation.standardName, + status: status, + }, + ], + TenantFilter: tenantFilter, }, - }; - - // Set the API data for CippApiDialog - setApiData(apiData); - setApiDialogOpen(true); + 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; + default: + return; + } + + // Set action data for CippApiDialog + setActionData({ + data: { + deviations: [ + { + standardName: deviation.text, // Use the text field which contains standardName + status: status, + }, + ], + TenantFilter: tenantFilter, + }, + action: { + text: actionText, + type: "single", + }, + ready: true, + }); + + createDialog.handleOpen(); + }; + const handleBulkAction = (action) => { - if (!processedDriftData.currentDeviations || processedDriftData.currentDeviations.length === 0) { + 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; default: setBulkActionsAnchorEl(null); return; } - const deviations = processedDriftData.currentDeviations.map(deviation => ({ + const deviations = processedDriftData.currentDeviations.map((deviation) => ({ standardName: deviation.standardName, status: status, })); - // Use CippApiDialog for the API call - const apiData = { - url: `/api/ExecUpdateDriftDeviation?TenantFilter=${tenantFilter}`, + // Set action data for CippApiDialog + setActionData({ data: { deviations: deviations, + TenantFilter: tenantFilter, }, - }; - - // Set the API data for CippApiDialog - setApiData(apiData); - setApiDialogOpen(true); + action: { + text: actionText, + type: "bulk", + count: deviations.length, + }, + ready: true, + }); + createDialog.handleOpen(); setBulkActionsAnchorEl(null); }; @@ -277,6 +421,24 @@ const ManageDriftPage = () => { setWhatIfAnchorEl(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 = [ { @@ -338,6 +500,68 @@ const ManageDriftPage = () => { ), })); + // Add action buttons to accepted deviation items + const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => ({ + ...item, + actionButton: ( + <> + + handleMenuClose(`accepted-${item.id}`)} + > + handleDeviationAction("deny", item)}> + + Deny + + handleDeviationAction("accept-customer-specific", item)}> + + Accept - Customer Specific + + + + ), + })); + + // Add action buttons to customer specific deviation items + const customerSpecificDeviationItemsWithActions = customerSpecificDeviationItems.map((item) => ({ + ...item, + actionButton: ( + <> + + handleMenuClose(`customer-${item.id}`)} + > + handleDeviationAction("deny", item)}> + + Deny + + handleDeviationAction("accept", item)}> + + Accept + + + + ), + })); + const title = "Manage Drift"; const subtitle = [ { @@ -373,80 +597,136 @@ const ManageDriftPage = () => { {/* Right side - Deviation Management */} - {/* 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 - - handleBulkAction("deny-all")}> - - Deny All Deviations - - - - {/* What If Button */} - - setWhatIfAnchorEl(null)} - > - {availableStandards.map((standard) => ( - handleWhatIfAction(standard.id)}> - - {standard.name} + {/* 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 + + handleBulkAction("deny-all")}> + + Deny All Deviations + + + + Remove Drift Customization + + + + {/* What If Button */} + + setWhatIfAnchorEl(null)} + > + {availableStandards.map((standard) => ( + handleWhatIfAction(standard.id)}> + + {standard.name} + + ))} + + +
- + + {/* Accepted Deviations Section */} + {acceptedDeviationItemsWithActions.length > 0 && ( + + + Accepted Deviations + + + + )} + + {/* Customer Specific Deviations Section */} + {customerSpecificDeviationItemsWithActions.length > 0 && ( + + + Accepted Deviations - Customer Specific + + + + )}
- setApiDialogOpen(false)} - data={apiData} - onSuccess={() => { - driftApi.refetch(); - setApiDialogOpen(false); - }} - /> + {actionData.ready && ( + + )} ); }; diff --git a/src/pages/tenant/standards/manageDrift/tenant-report.js b/src/pages/tenant/standards/manageDrift/tenant-report.js new file mode 100644 index 000000000000..ce85fb2c29f1 --- /dev/null +++ b/src/pages/tenant/standards/manageDrift/tenant-report.js @@ -0,0 +1,75 @@ +import { Box, Typography, Stack } from "@mui/material"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useRouter } from "next/router"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import tabOptions from "./tabOptions.json"; + +const Page = () => { + const router = useRouter(); + const { tenantFilter, templateId } = router.query; + + const pageTitle = "Tenant Report"; + const subtitle = [ + { + icon: "info", + text: `Detailed drift report for tenant: ${tenantFilter || "All Tenants"}`, + }, + ]; + + const actions = [ + { + label: "View Details", + link: "/tenant/standards/manageDrift?templateId=[standardId]&tenantFilter=[tenantFilter]", + icon: , + color: "info", + target: "_self", + }, + ]; + + return ( + + + {pageTitle} + + + + + + + ); +}; + +Page.getLayout = (page) => ( + + + {page} + + +); + +export default Page; \ No newline at end of file diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 74084df57263..69902f42a221 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -358,6 +358,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 : ; From f4316e7108f2c442de155a7aecad65761c79ecb7 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:54:57 +0200 Subject: [PATCH 43/69] change how drift detection works --- .../CippStandards/CippStandardsSideBar.jsx | 18 ++++++++++-------- .../list-standards/classic-standards/index.js | 2 +- .../tenant/standards/manageDrift/index.js | 17 ++++++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 5e9d9bbabfd3..460fd54b0b44 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -71,14 +71,14 @@ const CippStandardsSideBar = ({ const [savedItem, setSavedItem] = useState(null); 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(); @@ -305,13 +305,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" - } : {}), + ...(isDriftMode + ? { + type: "drift", + driftAlertWebhook: "driftAlertWebhook", + driftAlertEmail: "driftAlertEmail", + } + : {}), }, }} row={formControl.getValues()} diff --git a/src/pages/tenant/standards/list-standards/classic-standards/index.js b/src/pages/tenant/standards/list-standards/classic-standards/index.js index ba8992f3bd72..958502d70541 100644 --- a/src/pages/tenant/standards/list-standards/classic-standards/index.js +++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js @@ -30,7 +30,7 @@ const Page = () => { { 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]", + link: "/tenant/standards/template?id=[GUID]&type=[type]", icon: , color: "success", target: "_self", diff --git a/src/pages/tenant/standards/manageDrift/index.js b/src/pages/tenant/standards/manageDrift/index.js index fb129fd301ec..6d0767ef552b 100644 --- a/src/pages/tenant/standards/manageDrift/index.js +++ b/src/pages/tenant/standards/manageDrift/index.js @@ -48,10 +48,13 @@ const ManageDriftPage = () => { queryKey: `TenantDrift-${tenantFilter}`, }); - // API call for available standards (for What If dropdown) + // API call for available drift templates (for What If dropdown) const standardsApi = ApiGetCall({ - url: "/api/ListStandards", - queryKey: "ListStandards-drift", + url: "/api/listStandardTemplates", + data: { + type: "drift" + }, + queryKey: "ListDriftTemplates", }); // API call for standards comparison (when templateId is available) @@ -455,10 +458,10 @@ const ManageDriftPage = () => { }, ]; - // Process standards data for "What If" dropdown - const availableStandards = (standardsApi.data || []).map((standard) => ({ - id: standard.GUID || standard.id || standard.name, - name: standard.displayName || standard.name || "Unknown Standard", + // Process drift templates data for "What If" dropdown + const availableStandards = (standardsApi.data || []).map((template) => ({ + id: template.GUID || template.id || template.templateName, + name: template.templateName || template.displayName || "Unknown Template", })); // Add action buttons to each deviation item From 547bacc985594cb4de11f723c6a6826fb89f724b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 29 Jul 2025 00:42:02 +0200 Subject: [PATCH 44/69] cleanup --- .../tenant/standards/list-standards/index.js | 2 +- .../index.js => manageDrift/compare.js} | 165 +++++----- .../tenant/standards/manageDrift/index.js | 296 +++++++++++------- .../standards/manageDrift/tabOptions.json | 4 + .../standards/manageDrift/tenant-report.js | 75 ----- 5 files changed, 277 insertions(+), 265 deletions(-) rename src/pages/tenant/standards/{compare/index.js => manageDrift/compare.js} (95%) delete mode 100644 src/pages/tenant/standards/manageDrift/tenant-report.js diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index daf7f0626b6a..6a981105dedd 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -13,7 +13,7 @@ const Page = () => { const actions = [ { label: "View Tenant Report", - link: "/tenant/standards/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", + link: "/tenant/standards/manageDrift/tenant-report?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , color: "info", target: "_self", diff --git a/src/pages/tenant/standards/compare/index.js b/src/pages/tenant/standards/manageDrift/compare.js similarity index 95% rename from src/pages/tenant/standards/compare/index.js rename to src/pages/tenant/standards/manageDrift/compare.js index 533ee5aec32f..c93575a1d039 100644 --- a/src/pages/tenant/standards/compare/index.js +++ b/src/pages/tenant/standards/manageDrift/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, @@ -40,6 +41,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 "../manageDrift/tabOptions.json"; const Page = () => { const router = useRouter(); @@ -454,41 +456,18 @@ const Page = () => { // 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: ( + @@ -505,7 +484,6 @@ const Page = () => { ? "warning" : "error" } - sx={{ ml: 2 }} /> { } /> - )} - - {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(); + }, + }, + ]; + + return ( + + {comparisonApi.isFetching && ( <> {[1, 2, 3].map((item) => ( @@ -632,7 +642,6 @@ const Page = () => { )} {!comparisonApi.isFetching && ( <> - { ))} )} - - - - + + + + ); }; diff --git a/src/pages/tenant/standards/manageDrift/index.js b/src/pages/tenant/standards/manageDrift/index.js index 6d0767ef552b..cd6f58388463 100644 --- a/src/pages/tenant/standards/manageDrift/index.js +++ b/src/pages/tenant/standards/manageDrift/index.js @@ -13,8 +13,9 @@ import { Policy, Error, Info, + FactCheck, } from "@mui/icons-material"; -import { Box, Stack, Typography, Button, Menu, MenuItem } from "@mui/material"; +import { Box, Stack, Typography, Button, Menu, MenuItem, Chip, SvgIcon, IconButton, Tooltip } from "@mui/material"; import { Grid } from "@mui/system"; import { useState } from "react"; import { CippChartCard } from "/src/components/CippCards/CippChartCard"; @@ -565,12 +566,59 @@ const ManageDriftPage = () => { ), })); + // 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 ( @@ -585,125 +633,151 @@ const ManageDriftPage = () => { > - - {/* 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 - - handleBulkAction("deny-all")}> - - Deny All Deviations - - - - Remove Drift Customization - - - - {/* What If Button */} - - setWhatIfAnchorEl(null)} - > - {availableStandards.map((standard) => ( - handleWhatIfAction(standard.id)}> - - {standard.name} + {/* 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 + + handleBulkAction("deny-all")}> + + Deny All Deviations + + + + Remove Drift Customization - ))} - + + + {/* What If Button */} + + setWhatIfAnchorEl(null)} + > + {availableStandards.map((standard) => ( + handleWhatIfAction(standard.id)}> + + {standard.name} + + ))} + + - - - - - {/* Accepted Deviations Section */} - {acceptedDeviationItemsWithActions.length > 0 && ( - - - Accepted Deviations - - )} - {/* Customer Specific Deviations Section */} - {customerSpecificDeviationItemsWithActions.length > 0 && ( - - - Accepted Deviations - Customer Specific - - - - )} - + {/* Accepted Deviations Section */} + {acceptedDeviationItemsWithActions.length > 0 && ( + + + Accepted Deviations + + + + )} + + {/* Customer Specific Deviations Section */} + {customerSpecificDeviationItemsWithActions.length > 0 && ( + + + Accepted Deviations - Customer Specific + + + + )} + +
- + )} {actionData.ready && ( { - const router = useRouter(); - const { tenantFilter, templateId } = router.query; - - const pageTitle = "Tenant Report"; - const subtitle = [ - { - icon: "info", - text: `Detailed drift report for tenant: ${tenantFilter || "All Tenants"}`, - }, - ]; - - const actions = [ - { - label: "View Details", - link: "/tenant/standards/manageDrift?templateId=[standardId]&tenantFilter=[tenantFilter]", - icon: , - color: "info", - target: "_self", - }, - ]; - - return ( - - - {pageTitle} - - - - - - - ); -}; - -Page.getLayout = (page) => ( - - - {page} - - -); - -export default Page; \ No newline at end of file From e780c29a7c93c46da3340b55442a02933aaaece1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 29 Jul 2025 00:51:37 +0200 Subject: [PATCH 45/69] updates --- .../tenant/standards/list-standards/index.js | 4 ++-- .../{manageDrift => manage-drift}/compare.js | 2 +- .../{manageDrift => manage-drift}/history.js | 2 +- .../{manageDrift => manage-drift}/index.js | 0 .../policies-deployed.js | 0 .../recover-policies.js | 0 .../standards/manage-drift/tabOptions.json | 22 +++++++++++++++++++ .../standards/manageDrift/tabOptions.json | 22 ------------------- 8 files changed, 26 insertions(+), 26 deletions(-) rename src/pages/tenant/standards/{manageDrift => manage-drift}/compare.js (99%) rename src/pages/tenant/standards/{manageDrift => manage-drift}/history.js (97%) rename src/pages/tenant/standards/{manageDrift => manage-drift}/index.js (100%) rename src/pages/tenant/standards/{manageDrift => manage-drift}/policies-deployed.js (100%) rename src/pages/tenant/standards/{manageDrift => manage-drift}/recover-policies.js (100%) create mode 100644 src/pages/tenant/standards/manage-drift/tabOptions.json delete mode 100644 src/pages/tenant/standards/manageDrift/tabOptions.json diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index 6a981105dedd..c4c95550bd02 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -13,14 +13,14 @@ const Page = () => { const actions = [ { label: "View Tenant Report", - link: "/tenant/standards/manageDrift/tenant-report?tenantFilter=[tenantFilter]&templateId=[standardId]", + link: "/tenant/standards/manage-drift/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , color: "info", target: "_self", }, { label: "Manage Drift", - link: "/tenant/standards/manageDrift?templateId=[standardId]&tenantFilter=[tenantFilter]", + link: "/tenant/standards/manage-drift?templateId=[standardId]&tenantFilter=[tenantFilter]", icon: , color: "info", target: "_self", diff --git a/src/pages/tenant/standards/manageDrift/compare.js b/src/pages/tenant/standards/manage-drift/compare.js similarity index 99% rename from src/pages/tenant/standards/manageDrift/compare.js rename to src/pages/tenant/standards/manage-drift/compare.js index c93575a1d039..2dc9c350fec6 100644 --- a/src/pages/tenant/standards/manageDrift/compare.js +++ b/src/pages/tenant/standards/manage-drift/compare.js @@ -41,7 +41,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 "../manageDrift/tabOptions.json"; +import tabOptions from "./tabOptions.json"; const Page = () => { const router = useRouter(); diff --git a/src/pages/tenant/standards/manageDrift/history.js b/src/pages/tenant/standards/manage-drift/history.js similarity index 97% rename from src/pages/tenant/standards/manageDrift/history.js rename to src/pages/tenant/standards/manage-drift/history.js index 53f77047a9c8..7a62080a4593 100644 --- a/src/pages/tenant/standards/manageDrift/history.js +++ b/src/pages/tenant/standards/manage-drift/history.js @@ -7,7 +7,7 @@ import { CippChartCard } from "/src/components/CippCards/CippChartCard"; import { ApiGetCall } from "/src/api/ApiCall"; import { useRouter } from "next/router"; import { Policy } from "@mui/icons-material"; -import tabOptions from "/src/pages/tenant/standards/manageDrift/tabOptions.json"; +import tabOptions from "./tabOptions.json"; const Page = () => { const router = useRouter(); diff --git a/src/pages/tenant/standards/manageDrift/index.js b/src/pages/tenant/standards/manage-drift/index.js similarity index 100% rename from src/pages/tenant/standards/manageDrift/index.js rename to src/pages/tenant/standards/manage-drift/index.js diff --git a/src/pages/tenant/standards/manageDrift/policies-deployed.js b/src/pages/tenant/standards/manage-drift/policies-deployed.js similarity index 100% rename from src/pages/tenant/standards/manageDrift/policies-deployed.js rename to src/pages/tenant/standards/manage-drift/policies-deployed.js diff --git a/src/pages/tenant/standards/manageDrift/recover-policies.js b/src/pages/tenant/standards/manage-drift/recover-policies.js similarity index 100% rename from src/pages/tenant/standards/manageDrift/recover-policies.js rename to src/pages/tenant/standards/manage-drift/recover-policies.js 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..24ad7600e5fb --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/tabOptions.json @@ -0,0 +1,22 @@ +[ + { + "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": "Recover Policies", + "path": "/tenant/standards/manage-drift/recover-policies" + }, + { + "label": "Tenant Report", + "path": "/tenant/standards/manage-drift/compare" + } +] diff --git a/src/pages/tenant/standards/manageDrift/tabOptions.json b/src/pages/tenant/standards/manageDrift/tabOptions.json deleted file mode 100644 index c9cce962bc9a..000000000000 --- a/src/pages/tenant/standards/manageDrift/tabOptions.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "label": "Manage Drift", - "path": "/tenant/standards/manageDrift" - }, - { - "label": "Policies and Settings Deployed", - "path": "/tenant/standards/manageDrift/policies-deployed" - }, - { - "label": "History", - "path": "/tenant/standards/manageDrift/history" - }, - { - "label": "Recover Policies", - "path": "/tenant/standards/manageDrift/recover-policies" - }, - { - "label": "Tenant Report", - "path": "/tenant/standards/compare" - } -] \ No newline at end of file From 8d6c96272a746e0c5f18104b58585dec2b05f307 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:49:48 +0800 Subject: [PATCH 46/69] Fixes the interval getting overridden by the recommended value when editing existing alerts --- .../tenant/administration/alert-configuration/alert.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index c991de079f57..fc3950a47a4e 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -253,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 From 4113da1b328484a172ebf58c55a9982b07b053f7 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:32:35 +0800 Subject: [PATCH 47/69] Add information about default configurations to docs helper --- src/data/standards.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/standards.json b/src/data/standards.json index aee94c9ffbb4..3f03319f53fa 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", From b0885737cea262135b0afd6856904adcaf07cc09 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Tue, 29 Jul 2025 09:19:33 +0200 Subject: [PATCH 48/69] Add log entry page and shorten error message in table --- src/pages/cipp/logs/index.js | 11 +++ src/pages/cipp/logs/logentry.js | 137 +++++++++++++++++++++++++++++++ src/utils/get-cipp-formatting.js | 9 ++ 3 files changed, 157 insertions(+) create mode 100644 src/pages/cipp/logs/logentry.js diff --git a/src/pages/cipp/logs/index.js b/src/pages/cipp/logs/index.js index f2577871fe58..b96278f71a30 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: { @@ -303,6 +313,7 @@ const Page = () => { Severity: severity, // Pass severity filter from state Filter: filterEnabled, // Pass filter toggle state }} + actions={actions} /> ); }; 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/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 74084df57263..9c60487d0a47 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 > 15) { + 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 ? ( From f9f2857280021dc407187712570683adbd0b49cf Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:06:35 +0200 Subject: [PATCH 49/69] prevent adding multiple drift standards --- .../CippStandards/CippStandardAccordion.jsx | 13 +- .../CippStandards/CippStandardsSideBar.jsx | 146 +++++++++++++++++- .../tenant/standards/manage-drift/index.js | 26 ++++ src/pages/tenant/standards/template.jsx | 4 +- 4 files changed, 176 insertions(+), 13 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index ec440221254a..61f98fe22880 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -110,7 +110,7 @@ const CippStandardAccordion = ({ // Watch all trackDrift values for all standards at once const allTrackDriftValues = useWatch({ control: formControl.control, - name: Object.keys(selectedStandards).map(standardName => `${standardName}.trackDrift`) + name: Object.keys(selectedStandards).map((standardName) => `${standardName}.trackDrift`), }); // Handle drift mode automatic action setting @@ -241,7 +241,6 @@ const CippStandardAccordion = ({ return; } - console.log("Initializing configuration state from template values"); const initial = {}; const initialConfigured = {}; @@ -285,7 +284,6 @@ const CippStandardAccordion = ({ // Update configured state right away const isConfigured = isStandardConfigured(standardName, standard, newValues); - console.log(`Saving standard ${standardName}, configured: ${isConfigured}`); setConfiguredState((prev) => ({ ...prev, @@ -575,8 +573,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); @@ -628,8 +624,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) { @@ -667,10 +662,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 ( { const { complete } = props; @@ -65,10 +67,13 @@ 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); @@ -87,6 +92,135 @@ 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 + const existingTemplates = driftValidationApi.data.filter((template) => + edit ? template.GUID !== watchForm.GUID : true + ); + + // 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 + for (const templateId in uniqueTemplates) { + const template = uniqueTemplates[templateId]; + const templateTenants = template.tenants; + + const hasConflict = selectedTenantList.some((selectedTenant) => { + // Check if any template tenant matches the selected tenant + return templateTenants.some((templateTenant) => { + if (selectedTenant === "AllTenants" || templateTenant === "AllTenants") { + return true; + } + return selectedTenant === templateTenant; + }); + }); + + if (hasConflict) { + conflicts.push(template.standardName || "Unknown Template"); + } + } + + 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"), @@ -131,6 +265,7 @@ const CippStandardsSideBar = ({ return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); }), }; + return ( @@ -171,6 +306,10 @@ const CippStandardsSideBar = ({ required={true} includeGroups={true} /> + + {/* Show drift error */} + {isDriftMode && driftError && {driftError}} + {watchForm.tenantFilter?.some( (tenant) => tenant.value === "AllTenants" || tenant.type === "Group" ) && ( @@ -278,7 +417,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) } /> ))} @@ -322,6 +463,8 @@ const CippStandardsSideBar = ({ "listStandardTemplates", "listStandards", `listStandardTemplates-${watchForm.GUID}`, + "ListTenantAlignment-drift-validation", + "ListTenantGroups-drift-validation", ]} /> @@ -342,6 +485,7 @@ CippStandardsSideBar.propTypes = { updatedAt: PropTypes.string, formControl: PropTypes.object.isRequired, onSaveSuccess: PropTypes.func, + onDriftConflictChange: PropTypes.func, }; export default CippStandardsSideBar; diff --git a/src/pages/tenant/standards/manage-drift/index.js b/src/pages/tenant/standards/manage-drift/index.js index cd6f58388463..6770c4d4bdc5 100644 --- a/src/pages/tenant/standards/manage-drift/index.js +++ b/src/pages/tenant/standards/manage-drift/index.js @@ -14,6 +14,7 @@ import { Error, Info, FactCheck, + PlayArrow, } from "@mui/icons-material"; import { Box, Stack, Typography, Button, Menu, MenuItem, Chip, SvgIcon, IconButton, Tooltip } from "@mui/material"; import { Grid } from "@mui/system"; @@ -457,6 +458,31 @@ const ManageDriftPage = () => { } }, }, + ...(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, + }, + ] : []), ]; // Process drift templates data for "What If" dropdown diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index e1988d5103ca..0262b6a6e1ad 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -29,6 +29,7 @@ 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 @@ -262,7 +263,7 @@ const Page = () => { // Determine if save button should be disabled based on configuration const isSaveDisabled = isDriftMode - ? currentStep < 3 // For drift mode, only require steps 1, 3, and 4 (skip tenant requirement) + ? 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); @@ -361,6 +362,7 @@ const Page = () => { edit={editMode} updatedAt={updatedAt} isDriftMode={isDriftMode} + onDriftConflictChange={setHasDriftConflict} onSaveSuccess={() => { // Reset unsaved changes flag setHasUnsavedChanges(false); From 361ed5862a00a0a8f3aac527ffdc00399b9e64b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 29 Jul 2025 23:48:27 +0200 Subject: [PATCH 50/69] Feat: Add securityEnabled field switch --- .../identity/administration/groups/edit.jsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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" && ( + + + + )} )} From d3a10e326a7d7baae62143488943f60a0112374b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:06:21 +0200 Subject: [PATCH 51/69] more improvements --- .../CippStandards/CippStandardsSideBar.jsx | 80 ++- .../tenant/standards/manage-drift/index.js | 532 +++++++++++++----- .../manage-drift/policies-deployed.js | 171 +++++- 3 files changed, 597 insertions(+), 186 deletions(-) diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 593eefb3a56f..c27c4485467c 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -107,16 +107,16 @@ const CippStandardsSideBar = ({ // 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); + const group = groups?.find((g) => g.Id === tenantValue); if (group && group.Members) { - group.Members.forEach(member => { + group.Members.forEach((member) => { expandedTenants.push(member.defaultDomainName); }); } @@ -125,7 +125,7 @@ const CippStandardsSideBar = ({ expandedTenants.push(tenantValue); } }); - + return expandedTenants; }; @@ -271,6 +271,70 @@ const CippStandardsSideBar = ({ + + {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 && ( @@ -333,7 +397,7 @@ const CippStandardsSideBar = ({ name="driftAlertWebhook" label="Drift Alert Webhook" formControl={formControl} - placeholder="Enter webhook URL for drift alerts" + placeholder="Enter webhook URL for drift alerts. Leave blank to use the default webhook URL." fullWidth /> @@ -431,7 +495,7 @@ const CippStandardsSideBar = ({ title="Add Standard" api={{ confirmText: isDriftMode - ? "This template will only run after assigning it through the Drift Template setup wizard" + ? "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.", diff --git a/src/pages/tenant/standards/manage-drift/index.js b/src/pages/tenant/standards/manage-drift/index.js index 6770c4d4bdc5..4471302b6910 100644 --- a/src/pages/tenant/standards/manage-drift/index.js +++ b/src/pages/tenant/standards/manage-drift/index.js @@ -16,7 +16,7 @@ import { FactCheck, PlayArrow, } from "@mui/icons-material"; -import { Box, Stack, Typography, Button, Menu, MenuItem, Chip, SvgIcon, IconButton, Tooltip } from "@mui/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"; @@ -54,7 +54,7 @@ const ManageDriftPage = () => { const standardsApi = ApiGetCall({ url: "/api/listStandardTemplates", data: { - type: "drift" + type: "drift", }, queryKey: "ListDriftTemplates", }); @@ -73,9 +73,11 @@ const ManageDriftPage = () => { // Process drift data for chart - filter by current tenant and aggregate const rawDriftData = driftApi.data || []; + console.log("Raw drift API data:", rawDriftData); const tenantDriftData = Array.isArray(rawDriftData) ? rawDriftData.filter((item) => item.tenantFilter === tenantFilter) : []; + console.log("Filtered tenant drift data:", tenantDriftData); // Aggregate data across all standards for this tenant const processedDriftData = tenantDriftData.reduce( @@ -85,21 +87,54 @@ const ManageDriftPage = () => { acc.alignedCount += item.alignedCount || 0; acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; - // Collect all current deviations - if (item.currentDeviations && Array.isArray(item.currentDeviations)) { - acc.currentDeviations.push(...item.currentDeviations.filter((dev) => dev !== null)); - } - - // Collect accepted deviations - if (item.acceptedDeviations && Array.isArray(item.acceptedDeviations)) { - acc.acceptedDeviations.push(...item.acceptedDeviations.filter((dev) => dev !== null)); - } + // Use allDeviations array which contains standardDisplayName for template policies + if (item.allDeviations && Array.isArray(item.allDeviations)) { + const allDeviations = item.allDeviations.filter((dev) => dev !== null); - // Collect customer specific deviations - if (item.customerSpecificDeviations && Array.isArray(item.customerSpecificDeviations)) { - acc.customerSpecificDeviationsList.push( - ...item.customerSpecificDeviations.filter((dev) => dev !== null) + // Filter deviations by state/status + const currentDeviations = allDeviations.filter( + (d) => d.state === "current" || d.Status === "New" || (!d.state && !d.Status) + ); + const acceptedDeviations = allDeviations.filter( + (d) => d.state === "accepted" || d.Status === "Accepted" + ); + const customerSpecificDeviations = allDeviations.filter( + (d) => + d.state === "customerSpecific" || + d.Status === "CustomerSpecific" || + d.Status === "Customer Specific" + ); + const deniedDeleteDeviations = allDeviations.filter( + (d) => + d.state === "deniedDelete" || + d.Status === "DeniedDelete" || + d.Status === "Denied - Delete" + ); + const deniedRemediateDeviations = allDeviations.filter( + (d) => + d.state === "deniedRemediate" || + d.Status === "DeniedRemediate" || + d.Status === "Denied - Remediate" ); + + acc.currentDeviations.push(...currentDeviations); + acc.acceptedDeviations.push(...acceptedDeviations); + acc.customerSpecificDeviationsList.push(...customerSpecificDeviations); + acc.deniedDeleteDeviationsList.push(...deniedDeleteDeviations); + acc.deniedRemediateDeviationsList.push(...deniedRemediateDeviations); + } else { + // Fallback to separate arrays if allDeviations is not available + 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) + ); + } } // Use the latest data collection timestamp @@ -121,6 +156,8 @@ const ManageDriftPage = () => { currentDeviations: [], acceptedDeviations: [], customerSpecificDeviationsList: [], + deniedDeleteDeviationsList: [], + deniedRemediateDeviationsList: [], latestDataCollection: null, } ); @@ -145,6 +182,12 @@ const ManageDriftPage = () => { return ; case "denied": return ; + case "denieddelete": + case "denied - delete": + return ; + case "deniedremediate": + case "denied - remediate": + return ; case "accepted": return ; case "customerspecific": @@ -160,6 +203,12 @@ const ManageDriftPage = () => { 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": @@ -175,6 +224,12 @@ const ManageDriftPage = () => { 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": @@ -215,17 +270,19 @@ const ManageDriftPage = () => { // Helper function to create deviation items const createDeviationItems = (deviations, statusOverride = null) => { return (deviations || []).map((deviation, index) => { - // Get pretty name from standards.json first, then fallback to standardDisplayName, then raw name + // Prioritize standardDisplayName from drift data (which has user-friendly names for templates) + // then fallback to standards.json lookup, then raw name const prettyName = - getStandardPrettyName(deviation.standardName) || 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"; + const description = + getStandardDescription(deviation.standardName) || + deviation.standardDescription || + "No description available"; return { id: index + 1, @@ -238,6 +295,7 @@ const ManageDriftPage = () => { 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 propertyItems: [ { label: "Standard Name", value: prettyName }, { label: "Description", value: description }, @@ -267,6 +325,14 @@ const ManageDriftPage = () => { processedDriftData.customerSpecificDeviationsList, "customerspecific" ); + const deniedDeleteDeviationItems = createDeviationItems( + processedDriftData.deniedDeleteDeviationsList, + "denieddelete" + ); + const deniedRemediateDeviationItems = createDeviationItems( + processedDriftData.deniedRemediateDeviationsList, + "deniedremediate" + ); const handleMenuClick = (event, itemId) => { setAnchorEl((prev) => ({ ...prev, [itemId]: event.currentTarget })); @@ -291,14 +357,18 @@ const ManageDriftPage = () => { status = "Accepted"; actionText = "accept"; break; - case "bring-into-standard": - status = "Accepted"; - actionText = "bring into standard"; - break; case "deny-remove": status = "Denied"; actionText = "deny and remove"; 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; } @@ -343,6 +413,14 @@ const ManageDriftPage = () => { 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; } @@ -352,7 +430,7 @@ const ManageDriftPage = () => { data: { deviations: [ { - standardName: deviation.text, // Use the text field which contains standardName + standardName: deviation.standardName, // Use the standardName from the original deviation data status: status, }, ], @@ -392,6 +470,14 @@ const ManageDriftPage = () => { 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; @@ -458,31 +544,33 @@ const ManageDriftPage = () => { } }, }, - ...(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, - }, - ] : []), + ...(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, + }, + ] + : []), ]; // Process drift templates data for "What If" dropdown @@ -492,65 +580,159 @@ const ManageDriftPage = () => { })); // Add action buttons to each deviation item - const deviationItemsWithActions = deviationItems.map((item) => ({ - ...item, - actionButton: ( - <> - - handleMenuClose(item.id)} - > - handleAction("accept-customer-specific", item.id)}> - - Accept Deviation - Customer Specific - - handleAction("accept", item.id)}> - - Accept Deviation - - handleAction("bring-into-standard", item.id)}> - - Bring into Drift Standard - - handleAction("deny-remove", item.id)}> - - Deny - Remove Policy - - - - ), - })); + const deviationItemsWithActions = deviationItems.map((item) => { + // Check if this is a template that supports delete action + const supportsDelete = + item.standardName === "ConditionalAccessTemplate" || item.standardName === "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 + + )} + handleAction("deny-remediate", item.id)}> + + Deny Deviation - Remediate to align with template + + + + ), + }; + }); // Add action buttons to accepted deviation items - const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => ({ + const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => { + // Check if this is a template that supports delete action + const supportsDelete = + item.standardName === "ConditionalAccessTemplate" || item.standardName === "IntuneTemplate"; + + return { + ...item, + actionButton: ( + <> + + handleMenuClose(`accepted-${item.id}`)} + > + {supportsDelete && ( + handleDeviationAction("deny-delete", item)}> + + Deny - Delete + + )} + 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 === "ConditionalAccessTemplate" || item.standardName === "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 delete deviation items + const deniedDeleteDeviationItemsWithActions = deniedDeleteDeviationItems.map((item) => ({ ...item, actionButton: ( <> handleMenuClose(`accepted-${item.id}`)} + anchorEl={anchorEl[`denied-delete-${item.id}`]} + open={Boolean(anchorEl[`denied-delete-${item.id}`])} + onClose={() => handleMenuClose(`denied-delete-${item.id}`)} > - handleDeviationAction("deny", item)}> - - Deny + handleDeviationAction("accept", item)}> + + Accept handleDeviationAction("accept-customer-specific", item)}> @@ -561,47 +743,47 @@ const ManageDriftPage = () => { ), })); - // Add action buttons to customer specific deviation items - const customerSpecificDeviationItemsWithActions = customerSpecificDeviationItems.map((item) => ({ + // Add action buttons to denied remediate deviation items + const deniedRemediateDeviationItemsWithActions = deniedRemediateDeviationItems.map((item) => ({ ...item, actionButton: ( <> handleMenuClose(`customer-${item.id}`)} + anchorEl={anchorEl[`denied-remediate-${item.id}`]} + open={Boolean(anchorEl[`denied-remediate-${item.id}`])} + onClose={() => handleMenuClose(`denied-remediate-${item.id}`)} > - handleDeviationAction("deny", item)}> - - Deny - 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 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; @@ -612,39 +794,41 @@ const ManageDriftPage = () => { 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" - } - /> - - ), - }, - ] : []), + ...(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 ( @@ -660,13 +844,15 @@ const ManageDriftPage = () => { {/* Check if there's no drift data */} - {!driftApi.isFetching && (!rawDriftData || rawDriftData.length === 0 || tenantDriftData.length === 0) ? ( + {!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. + 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: @@ -732,9 +918,20 @@ const ManageDriftPage = () => { Accept All Deviations - handleBulkAction("deny-all")}> + {/* Only show delete option if there are template deviations that support deletion */} + {processedDriftData.currentDeviations.some( + (deviation) => + deviation.standardName === "ConditionalAccessTemplate" || + deviation.standardName === "IntuneTemplate" + ) && ( + handleBulkAction("deny-all-delete")}> + + Deny All Deviations - Delete + + )} + handleBulkAction("deny-all-remediate")}> - Deny All Deviations + Deny All Deviations - Remediate to align with template @@ -758,7 +955,10 @@ const ManageDriftPage = () => { onClose={() => setWhatIfAnchorEl(null)} > {availableStandards.map((standard) => ( - handleWhatIfAction(standard.id)}> + handleWhatIfAction(standard.id)} + > {standard.name} @@ -800,6 +1000,34 @@ const ManageDriftPage = () => { /> )} + + {/* Denied Delete Deviations Section */} + {deniedDeleteDeviationItemsWithActions.length > 0 && ( + + + Denied Deviations - Delete + + + + )} + + {/* Denied Remediate Deviations Section */} + {deniedRemediateDeviationItemsWithActions.length > 0 && ( + + + Denied Deviations - Remediate + + + + )} diff --git a/src/pages/tenant/standards/manage-drift/policies-deployed.js b/src/pages/tenant/standards/manage-drift/policies-deployed.js index 74040d3db0f3..a24b6f95d9de 100644 --- a/src/pages/tenant/standards/manage-drift/policies-deployed.js +++ b/src/pages/tenant/standards/manage-drift/policies-deployed.js @@ -49,6 +49,17 @@ const PoliciesDeployedPage = () => { 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 @@ -56,11 +67,78 @@ const PoliciesDeployedPage = () => { const templateStandards = currentTemplate?.standards || {}; const comparisonData = comparisonApi.data?.[0] || {}; - // Helper function to get status from comparison data - const getStatus = (standardKey) => { + // 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; - return value === true ? "Deployed" : "Deviation"; + + 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 @@ -77,6 +155,34 @@ const PoliciesDeployedPage = () => { 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") @@ -90,29 +196,41 @@ const PoliciesDeployedPage = () => { })); // Process Intune Templates - const intunePolices = (templateStandards.IntuneTemplate || []).map((template, index) => ({ - id: index + 1, - name: template.TemplateList?.label || "Unknown Template", - category: "Intune Template", - platform: "Multi-Platform", - status: getStatus(`IntuneTemplate.${template.TemplateList?.value}`), - lastModified: getLastRefresh(`IntuneTemplate.${template.TemplateList?.value}`), - assignedGroups: template.AssignTo || "N/A", - templateValue: template.TemplateList?.value, - })); + 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) => ({ - id: index + 1, - name: template.TemplateList?.label || "Unknown Policy", - state: template.state || "Unknown", - conditions: "Conditional Access Policy", - controls: "Access Control", - lastModified: getLastRefresh(`ConditionalAccessTemplate.${template.TemplateList?.value}`), - status: getStatus(`ConditionalAccessTemplate.${template.TemplateList?.value}`), - templateValue: template.TemplateList?.value, - }) + (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 = [ { @@ -122,6 +240,7 @@ const PoliciesDeployedPage = () => { customFunction: () => { standardsApi.refetch(); comparisonApi.refetch(); + driftApi.refetch(); }, }, ]; @@ -160,7 +279,7 @@ const PoliciesDeployedPage = () => { data={deployedStandards} simpleColumns={["name", "category", "status", "lastModified"]} noCard={true} - isFetching={standardsApi.isFetching || comparisonApi.isFetching} + isFetching={standardsApi.isFetching || comparisonApi.isFetching || driftApi.isFetching} /> @@ -187,7 +306,7 @@ const PoliciesDeployedPage = () => { "assignedGroups", ]} noCard={true} - isFetching={standardsApi.isFetching || comparisonApi.isFetching} + isFetching={standardsApi.isFetching || comparisonApi.isFetching || driftApi.isFetching} /> @@ -214,7 +333,7 @@ const PoliciesDeployedPage = () => { "lastModified", ]} noCard={true} - isFetching={standardsApi.isFetching || comparisonApi.isFetching} + isFetching={standardsApi.isFetching || comparisonApi.isFetching || driftApi.isFetching} /> From 093dce15420a4415f0d9effa7556bb083b68dbc4 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:18:58 +0800 Subject: [PATCH 52/69] Add bulk mailbox permissions action Introduces a new 'Bulk Add Mailbox Permissions' action to CippExchangeActions, allowing admins to assign Full Access, Send As, and Send On Behalf permissions to multiple mailboxes at once. Utilizes user selection fields with API integration and a custom data formatter for bulk requests. --- .../CippComponents/CippExchangeActions.jsx | 128 +++++++++++++++++- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx index f660349e690e..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]", @@ -70,7 +189,6 @@ export const CippExchangeActions = () => { multiPost: false, }, { - //tested label: "Enable Online Archive", type: "POST", icon: , From ece5b18dbb8fbf1b34732e39daf2989746cbaf85 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 30 Jul 2025 23:27:15 +0800 Subject: [PATCH 53/69] Add SharePoint File Requests standard Introduces a new standard for enabling or disabling File Requests in SharePoint and OneDrive, including configuration for link expiration. This allows secure upload-only links for external users and enhances document collection workflows. --- src/data/standards.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 3f03319f53fa..23505da2cd6a 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -3536,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", From 3a238d9ea56241628f079ccb371adc659a763660 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 30 Jul 2025 12:49:24 -0400 Subject: [PATCH 54/69] fix nav issue with hiding menu items --- src/layouts/index.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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({ From b4e60ea6beb7ffb503790bf7be0cb6c63adeb9ee Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:58:25 +0200 Subject: [PATCH 55/69] Fix flexwrap issue --- .../CippCards/CippBannerListCard.jsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index cc7c0639aa83..747907a39941 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 +111,16 @@ export const CippBannerListCard = (props) => { {/* Main Text and Subtext */} - - + + {item.text} @@ -122,7 +130,7 @@ export const CippBannerListCard = (props) => { {/* Right Side: Status and Expand Icon */} - + {item?.statusText && ( Date: Wed, 30 Jul 2025 23:51:46 +0200 Subject: [PATCH 56/69] update drift settings --- .../CippCards/CippBannerListCard.jsx | 15 +- .../tenant/standards/manage-drift/index.js | 169 +++++------------- 2 files changed, 58 insertions(+), 126 deletions(-) diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index 747907a39941..7c96e641df6e 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -85,7 +85,12 @@ export const CippBannerListCard = (props) => { onClick={isCollapsible ? () => handleExpand(item.id) : undefined} > {/* Left Side: cardLabelBox */} - + { color="text.primary" variant="h6" sx={{ - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", }} > {item.text} @@ -174,7 +179,7 @@ export const CippBannerListCard = (props) => { {item?.propertyItems?.length > 0 && ( )} diff --git a/src/pages/tenant/standards/manage-drift/index.js b/src/pages/tenant/standards/manage-drift/index.js index 4471302b6910..ac5b16c7a065 100644 --- a/src/pages/tenant/standards/manage-drift/index.js +++ b/src/pages/tenant/standards/manage-drift/index.js @@ -86,55 +86,22 @@ const ManageDriftPage = () => { acc.currentDeviationsCount += item.currentDeviationsCount || 0; acc.alignedCount += item.alignedCount || 0; acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; + acc.deniedDeviationsCount += item.deniedDeviationsCount || 0; - // Use allDeviations array which contains standardDisplayName for template policies - if (item.allDeviations && Array.isArray(item.allDeviations)) { - const allDeviations = item.allDeviations.filter((dev) => dev !== null); - - // Filter deviations by state/status - const currentDeviations = allDeviations.filter( - (d) => d.state === "current" || d.Status === "New" || (!d.state && !d.Status) - ); - const acceptedDeviations = allDeviations.filter( - (d) => d.state === "accepted" || d.Status === "Accepted" - ); - const customerSpecificDeviations = allDeviations.filter( - (d) => - d.state === "customerSpecific" || - d.Status === "CustomerSpecific" || - d.Status === "Customer Specific" - ); - const deniedDeleteDeviations = allDeviations.filter( - (d) => - d.state === "deniedDelete" || - d.Status === "DeniedDelete" || - d.Status === "Denied - Delete" - ); - const deniedRemediateDeviations = allDeviations.filter( - (d) => - d.state === "deniedRemediate" || - d.Status === "DeniedRemediate" || - d.Status === "Denied - Remediate" + // 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) ); - - acc.currentDeviations.push(...currentDeviations); - acc.acceptedDeviations.push(...acceptedDeviations); - acc.customerSpecificDeviationsList.push(...customerSpecificDeviations); - acc.deniedDeleteDeviationsList.push(...deniedDeleteDeviations); - acc.deniedRemediateDeviationsList.push(...deniedRemediateDeviations); - } else { - // Fallback to separate arrays if allDeviations is not available - 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 @@ -153,11 +120,11 @@ const ManageDriftPage = () => { currentDeviationsCount: 0, alignedCount: 0, customerSpecificDeviations: 0, + deniedDeviationsCount: 0, currentDeviations: [], acceptedDeviations: [], customerSpecificDeviationsList: [], - deniedDeleteDeviationsList: [], - deniedRemediateDeviationsList: [], + deniedDeviationsList: [], latestDataCollection: null, } ); @@ -305,6 +272,14 @@ const ManageDriftPage = () => { 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 @@ -325,13 +300,9 @@ const ManageDriftPage = () => { processedDriftData.customerSpecificDeviationsList, "customerspecific" ); - const deniedDeleteDeviationItems = createDeviationItems( - processedDriftData.deniedDeleteDeviationsList, - "denieddelete" - ); - const deniedRemediateDeviationItems = createDeviationItems( - processedDriftData.deniedRemediateDeviationsList, - "deniedremediate" + const deniedDeviationItems = createDeviationItems( + processedDriftData.deniedDeviationsList, + "denied" ); const handleMenuClick = (event, itemId) => { @@ -712,54 +683,23 @@ const ManageDriftPage = () => { }; }); - // Add action buttons to denied delete deviation items - const deniedDeleteDeviationItemsWithActions = deniedDeleteDeviationItems.map((item) => ({ + // Add action buttons to denied deviation items + const deniedDeviationItemsWithActions = deniedDeviationItems.map((item) => ({ ...item, actionButton: ( <> handleMenuClose(`denied-delete-${item.id}`)} - > - handleDeviationAction("accept", item)}> - - Accept - - handleDeviationAction("accept-customer-specific", item)}> - - Accept - Customer Specific - - - - ), - })); - - // Add action buttons to denied remediate deviation items - const deniedRemediateDeviationItemsWithActions = deniedRemediateDeviationItems.map((item) => ({ - ...item, - actionButton: ( - <> - - handleMenuClose(`denied-remediate-${item.id}`)} + anchorEl={anchorEl[`denied-${item.id}`]} + open={Boolean(anchorEl[`denied-${item.id}`])} + onClose={() => handleMenuClose(`denied-${item.id}`)} > handleDeviationAction("accept", item)}> @@ -939,16 +879,6 @@ const ManageDriftPage = () => { - {/* What If Button */} - { @@ -982,6 +913,7 @@ const ManageDriftPage = () => { @@ -996,34 +928,22 @@ const ManageDriftPage = () => { )} - {/* Denied Delete Deviations Section */} - {deniedDeleteDeviationItemsWithActions.length > 0 && ( - - - Denied Deviations - Delete - - - - )} - - {/* Denied Remediate Deviations Section */} - {deniedRemediateDeviationItemsWithActions.length > 0 && ( + {/* Denied Deviations Section */} + {deniedDeviationItemsWithActions.length > 0 && ( - Denied Deviations - Remediate + Denied Deviations @@ -1037,6 +957,13 @@ const ManageDriftPage = () => { Date: Thu, 31 Jul 2025 00:02:52 +0200 Subject: [PATCH 57/69] updates to actions --- .../tenant/standards/manage-drift/compare.js | 29 ++++++++++++ .../tenant/standards/manage-drift/history.js | 44 ++++++++++++++++++- .../manage-drift/policies-deployed.js | 28 ++++++++++++ .../manage-drift/recover-policies.js | 44 ++++++++++++++++++- 4 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/compare.js b/src/pages/tenant/standards/manage-drift/compare.js index 2dc9c350fec6..8ddc70251193 100644 --- a/src/pages/tenant/standards/manage-drift/compare.js +++ b/src/pages/tenant/standards/manage-drift/compare.js @@ -27,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"; @@ -549,8 +550,36 @@ const Page = () => { 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 ( diff --git a/src/pages/tenant/standards/manage-drift/history.js b/src/pages/tenant/standards/manage-drift/history.js index 7a62080a4593..af4a1f007b55 100644 --- a/src/pages/tenant/standards/manage-drift/history.js +++ b/src/pages/tenant/standards/manage-drift/history.js @@ -6,7 +6,7 @@ import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; import { CippChartCard } from "/src/components/CippCards/CippChartCard"; import { ApiGetCall } from "/src/api/ApiCall"; import { useRouter } from "next/router"; -import { Policy } from "@mui/icons-material"; +import { Policy, Sync, PlayArrow } from "@mui/icons-material"; import tabOptions from "./tabOptions.json"; const Page = () => { @@ -70,6 +70,45 @@ const Page = () => { }, ]; + // Actions for the ActionsMenu + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + driftHistoryData.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 = [ { @@ -84,6 +123,9 @@ const Page = () => { title={title} subtitle={subtitle} backUrl="/tenant/standards/list-standards" + actions={actions} + actionsData={{}} + isFetching={driftHistoryData.isLoading} > diff --git a/src/pages/tenant/standards/manage-drift/policies-deployed.js b/src/pages/tenant/standards/manage-drift/policies-deployed.js index a24b6f95d9de..909b19a733f4 100644 --- a/src/pages/tenant/standards/manage-drift/policies-deployed.js +++ b/src/pages/tenant/standards/manage-drift/policies-deployed.js @@ -8,6 +8,7 @@ import { Devices, ExpandMore, Sync, + PlayArrow, } from "@mui/icons-material"; import { Box, @@ -243,6 +244,33 @@ const PoliciesDeployedPage = () => { 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 = [ diff --git a/src/pages/tenant/standards/manage-drift/recover-policies.js b/src/pages/tenant/standards/manage-drift/recover-policies.js index 73e68b63d15f..32b2a059a474 100644 --- a/src/pages/tenant/standards/manage-drift/recover-policies.js +++ b/src/pages/tenant/standards/manage-drift/recover-policies.js @@ -1,7 +1,7 @@ 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 } from "@mui/icons-material"; +import { Policy, Restore, ExpandMore, Sync, PlayArrow } from "@mui/icons-material"; import { Box, Stack, @@ -77,6 +77,45 @@ const RecoverPoliciesPage = () => { }); }; + // 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 = [ { @@ -91,6 +130,9 @@ const RecoverPoliciesPage = () => { title={title} subtitle={subtitle} backUrl="/tenant/standards/list-standards" + actions={actions} + actionsData={{}} + isFetching={recoverApi.isPending} > From e615b2318c486eefb465d53cfc304bd0ee8159a0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:25:52 +0200 Subject: [PATCH 58/69] hsitory timeline for tenant --- .../tenant/standards/manage-drift/history.js | 322 ++++++++++++++---- 1 file changed, 253 insertions(+), 69 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/history.js b/src/pages/tenant/standards/manage-drift/history.js index af4a1f007b55..396282a0edf7 100644 --- a/src/pages/tenant/standards/manage-drift/history.js +++ b/src/pages/tenant/standards/manage-drift/history.js @@ -1,74 +1,131 @@ -import { useState } from "react"; -import { Box, Stack, Typography } from "@mui/material"; +import { useState, useEffect } from "react"; +import { + Box, + Stack, + Typography, + Button, + Chip, + Card, + CardContent, + CircularProgress, + Alert, + Collapse, + Link +} from "@mui/material"; +import { + Timeline, + TimelineItem, + TimelineSeparator, + TimelineConnector, + TimelineContent, + TimelineDot, + TimelineOppositeContent +} from "@mui/lab"; import { Grid } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; -import { CippChartCard } from "/src/components/CippCards/CippChartCard"; import { ApiGetCall } from "/src/api/ApiCall"; import { useRouter } from "next/router"; -import { Policy, Sync, PlayArrow } from "@mui/icons-material"; +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"; const Page = () => { const router = useRouter(); const { templateId } = router.query; + const [daysToLoad, setDaysToLoad] = useState(7); + const [tenant] = useState("oglenet.onmicrosoft.com"); // You might want to get this from context or props + const [expandedMessages, setExpandedMessages] = useState(new Set()); - // Mock data for demonstration - replace with actual API call - const driftHistoryData = ApiGetCall({ - url: `/api/GetDriftHistory`, - data: { templateId }, - queryKey: `GetDriftHistory-${templateId}`, - }); + // Toggle message expansion + const toggleMessageExpansion = (index) => { + const newExpanded = new Set(expandedMessages); + if (newExpanded.has(index)) { + newExpanded.delete(index); + } else { + newExpanded.add(index); + } + setExpandedMessages(newExpanded); + }; - // Generate mock timeline data for the last 30 days - const generateTimelineData = () => { - const days = []; - const deviations = []; - const acceptedDeviations = []; - const deniedDeviations = []; - const inAlignment = []; - - for (let i = 29; i >= 0; i--) { - const date = new Date(); - date.setDate(date.getDate() - i); - days.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); - - // Mock data - replace with actual data processing - deviations.push(Math.floor(Math.random() * 20) + 5); - acceptedDeviations.push(Math.floor(Math.random() * 8) + 2); - deniedDeviations.push(Math.floor(Math.random() * 5) + 1); - inAlignment.push(Math.floor(Math.random() * 15) + 10); + // 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 + }; + }; - return { days, deviations, acceptedDeviations, deniedDeviations, inAlignment }; + // 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 timelineData = generateTimelineData(); + const { startDate, endDate } = getDateRange(daysToLoad); - // Format data like secureScore example - array of objects with name and data - const timelineChartSeries = [ - { - name: "Deviations Detected", - data: timelineData.days.map((day, index) => ({ - x: day, - y: timelineData.deviations[index], - })), - }, - { - name: "Accepted deviations - Customer Specific", - data: timelineData.days.map((day, index) => ({ - x: day, - y: timelineData.acceptedDeviations[index], - })), - }, - { - name: "Denied Deviation", - data: timelineData.days.map((day, index) => ({ - x: day, - y: timelineData.deniedDeviations[index], - })), - }, - ]; + // API call to get logs + 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 = [ @@ -77,7 +134,7 @@ const Page = () => { icon: , noConfirm: true, customFunction: () => { - driftHistoryData.refetch(); + logsData.refetch(); }, }, ...(templateId @@ -117,6 +174,11 @@ const Page = () => { }, ]; + // Sort logs by date (newest first) + const sortedLogs = logsData.data ? [...logsData.data].sort((a, b) => + new Date(b.DateTime) - new Date(a.DateTime) + ) : []; + return ( { backUrl="/tenant/standards/list-standards" actions={actions} actionsData={{}} - isFetching={driftHistoryData.isLoading} + isFetching={logsData.isLoading} > - Drift History + Activity Timeline - Historical timeline of drift deviations, acceptances, denials, and alignment status over the last 30 days. + Historical timeline of system activities and events for the last {daysToLoad} days. - - {/* Single Timeline Chart */} - - - - + {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} + + )} + + + + ); + })} + + + + + + + + )} From 2f14434934d7732d88e816a36fe5963fb5ebbcf3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:29:00 +0200 Subject: [PATCH 59/69] decreased history to 5 days. --- .../tenant/standards/manage-drift/history.js | 131 ++++++++++-------- 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/history.js b/src/pages/tenant/standards/manage-drift/history.js index 396282a0edf7..450f6bf1b813 100644 --- a/src/pages/tenant/standards/manage-drift/history.js +++ b/src/pages/tenant/standards/manage-drift/history.js @@ -10,7 +10,7 @@ import { CircularProgress, Alert, Collapse, - Link + Link, } from "@mui/material"; import { Timeline, @@ -19,30 +19,31 @@ import { TimelineConnector, TimelineContent, TimelineDot, - TimelineOppositeContent + TimelineOppositeContent, } from "@mui/lab"; import { Grid } from "@mui/system"; 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, +import { + Policy, + Sync, + PlayArrow, Error as ErrorIcon, Warning as WarningIcon, Info as InfoIcon, CheckCircle as SuccessIcon, - ExpandMore + 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(7); - const [tenant] = useState("oglenet.onmicrosoft.com"); // You might want to get this from context or props + const [daysToLoad, setDaysToLoad] = useState(5); + const tenant = useSettings().currentTenant; const [expandedMessages, setExpandedMessages] = useState(new Set()); // Toggle message expansion @@ -64,7 +65,7 @@ const Page = () => { return { text: message.substring(0, maxLength) + "...", fullText: message, - isTruncated: true + isTruncated: true, }; }; @@ -73,16 +74,15 @@ const Page = () => { 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, '') + startDate: startDate.toISOString().split("T")[0].replace(/-/g, ""), + endDate: endDate.toISOString().split("T")[0].replace(/-/g, ""), }; }; const { startDate, endDate } = getDateRange(daysToLoad); - // API call to get logs const logsData = ApiGetCall({ url: `/api/Listlogs?tenant=${tenant}&StartDate=${startDate}&EndDate=${endDate}&Filter=true`, queryKey: `Listlogs-${tenant}-${startDate}-${endDate}`, @@ -92,16 +92,16 @@ const Page = () => { 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' }; + 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' }; + return { icon: , color: "grey", chipColor: "default" }; } }; @@ -109,22 +109,22 @@ const Page = () => { const formatDate = (dateString) => { const date = new Date(dateString); return { - time: date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: false + time: date.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), + date: date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", }), - date: date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }) }; }; // Load more days const handleLoadMore = () => { - setDaysToLoad(prev => prev + 7); + setDaysToLoad((prev) => prev + 7); }; // Actions for the ActionsMenu @@ -175,9 +175,9 @@ const Page = () => { ]; // Sort logs by date (newest first) - const sortedLogs = logsData.data ? [...logsData.data].sort((a, b) => - new Date(b.DateTime) - new Date(a.DateTime) - ) : []; + const sortedLogs = logsData.data + ? [...logsData.data].sort((a, b) => new Date(b.DateTime) - new Date(a.DateTime)) + : []; return ( { )} {logsData.isError && ( - - Failed to load activity logs. Please try again. - + Failed to load activity logs. Please try again. )} {logsData.data && sortedLogs.length === 0 && ( - - No activity logs found for the selected time period. - + No activity logs found for the selected time period. )} {logsData.data && sortedLogs.length > 0 && ( @@ -233,11 +229,11 @@ const Page = () => { 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 && } - - + + { color={chipColor} size="small" variant="outlined" - sx={{ fontSize: '0.7rem', height: 20 }} + sx={{ fontSize: "0.7rem", height: 20 }} /> {log.IP && ( )} - + - + {isExpanded ? fullText : text} {isTruncated && ( @@ -294,18 +299,22 @@ const Page = () => { onClick={() => toggleMessageExpansion(index)} sx={{ mt: 0.5, - display: 'block', - textAlign: 'left', - fontSize: '0.75rem' + display: "block", + textAlign: "left", + fontSize: "0.75rem", }} > - {isExpanded ? 'Show less' : 'Show more'} + {isExpanded ? "Show less" : "Show more"} )} - + {log.User && ( - + User: {log.User} )} @@ -315,7 +324,7 @@ const Page = () => { ); })} - + - - setWhatIfAnchorEl(null)} - > - {availableStandards.map((standard) => ( - handleWhatIfAction(standard.id)} - > - - {standard.name} - - ))} - Date: Fri, 1 Aug 2025 15:22:41 +0200 Subject: [PATCH 66/69] LicenseAssignmentErrors alert --- src/data/alerts.json | 5 +++++ 1 file changed, 5 insertions(+) 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.", From 4b4ab7d37f8065099033e13c47176ae95fa369f2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:39:55 +0200 Subject: [PATCH 67/69] prerelease push --- .../tenant/standards/manage-drift/index.js | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/index.js b/src/pages/tenant/standards/manage-drift/index.js index 64e884ea85a6..39a4f03b312a 100644 --- a/src/pages/tenant/standards/manage-drift/index.js +++ b/src/pages/tenant/standards/manage-drift/index.js @@ -37,7 +37,6 @@ const ManageDriftPage = () => { const tenantFilter = userSettingsDefaults.currentTenant || ""; const [anchorEl, setAnchorEl] = useState({}); const [bulkActionsAnchorEl, setBulkActionsAnchorEl] = useState(null); - const [whatIfAnchorEl, setWhatIfAnchorEl] = useState(null); const createDialog = useDialog(); const [actionData, setActionData] = useState({ data: {}, ready: false }); @@ -73,11 +72,9 @@ const ManageDriftPage = () => { // Process drift data for chart - filter by current tenant and aggregate const rawDriftData = driftApi.data || []; - console.log("Raw drift API data:", rawDriftData); const tenantDriftData = Array.isArray(rawDriftData) ? rawDriftData.filter((item) => item.tenantFilter === tenantFilter) : []; - console.log("Filtered tenant drift data:", tenantDriftData); // Aggregate data across all standards for this tenant const processedDriftData = tenantDriftData.reduce( @@ -263,6 +260,9 @@ const ManageDriftPage = () => { 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 }, @@ -347,6 +347,7 @@ const ManageDriftPage = () => { { standardName: deviation.standardName, status: status, + receivedValue: deviation.receivedValue, }, ], TenantFilter: tenantFilter, @@ -399,6 +400,7 @@ const ManageDriftPage = () => { { standardName: deviation.standardName, // Use the standardName from the original deviation data status: status, + receivedValue: deviation.receivedValue, }, ], TenantFilter: tenantFilter, @@ -453,6 +455,7 @@ const ManageDriftPage = () => { const deviations = processedDriftData.currentDeviations.map((deviation) => ({ standardName: deviation.standardName, status: status, + receivedValue: deviation.receivedValue, })); // Set action data for CippApiDialog @@ -460,6 +463,7 @@ const ManageDriftPage = () => { data: { deviations: deviations, TenantFilter: tenantFilter, + receivedValues: deviations.map((d) => d.receivedValue), }, action: { text: actionText, @@ -473,12 +477,6 @@ const ManageDriftPage = () => { setBulkActionsAnchorEl(null); }; - const handleWhatIfAction = (standardId) => { - console.log(`What If Analysis with standard: ${standardId}`); - // Here you would implement the what-if analysis - setWhatIfAnchorEl(null); - }; - const handleRemoveDriftCustomization = () => { // Set action data for CippApiDialog setActionData({ @@ -540,12 +538,6 @@ const ManageDriftPage = () => { : []), ]; - // Process drift templates data for "What If" dropdown - const availableStandards = (standardsApi.data || []).map((template) => ({ - id: template.GUID || template.id || template.templateName, - name: template.templateName || template.displayName || "Unknown Template", - })); - // Add action buttons to each deviation item const deviationItemsWithActions = deviationItems.map((item) => { // Check if this is a template that supports delete action From f1f19f384882eef8c7bbe8f5971f13368b5a30ce Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:13:02 +0200 Subject: [PATCH 68/69] temporary removal of this --- src/pages/tenant/standards/manage-drift/tabOptions.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/tabOptions.json b/src/pages/tenant/standards/manage-drift/tabOptions.json index 24ad7600e5fb..50b3adfd16dc 100644 --- a/src/pages/tenant/standards/manage-drift/tabOptions.json +++ b/src/pages/tenant/standards/manage-drift/tabOptions.json @@ -11,10 +11,6 @@ "label": "History", "path": "/tenant/standards/manage-drift/history" }, - { - "label": "Recover Policies", - "path": "/tenant/standards/manage-drift/recover-policies" - }, { "label": "Tenant Report", "path": "/tenant/standards/manage-drift/compare" From 0d484f715310f750d527a6666737f80cd650773c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 1 Aug 2025 20:26:16 +0200 Subject: [PATCH 69/69] up version --- public/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" }