diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..e8faf3f82477 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + target-branch: "dev" diff --git a/package.json b/package.json index 6d01e9706b32..75f5b6ef3148 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "8.8.2", + "version": "10.0.0", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { @@ -106,7 +106,7 @@ "redux-persist": "^6.0.0", "redux-thunk": "3.1.0", "rehype-raw": "^7.0.0", - "remark-gfm": "^3.0.1", + "remark-gfm": "^4.0.0", "simplebar": "6.3.2", "simplebar-react": "3.3.2", "stylis-plugin-rtl": "2.1.1", diff --git a/public/sponsors/relentless-dark.png b/public/sponsors/relentless-dark.png new file mode 100644 index 000000000000..b234ac7c7903 Binary files /dev/null and b/public/sponsors/relentless-dark.png differ diff --git a/public/sponsors/relentless-light.png b/public/sponsors/relentless-light.png new file mode 100644 index 000000000000..5884597febe7 Binary files /dev/null and b/public/sponsors/relentless-light.png differ diff --git a/public/version.json b/public/version.json index 7a3adc4ddaba..d8ac32de0bb3 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.8.2" + "version": "10.0.0" } \ No newline at end of file diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index c1fe171cd4e2..55f5c2ecbae6 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -3,6 +3,7 @@ import { useState, useCallback } from "react"; import { Box, Card, + Checkbox, Collapse, Divider, IconButton, @@ -16,13 +17,34 @@ import { CippPropertyListCard } from "./CippPropertyListCard"; import { CippDataTable } from "../CippTable/CippDataTable"; export const CippBannerListCard = (props) => { - const { items = [], isCollapsible = false, isFetching = false, children, ...other } = props; + const { + items = [], + isCollapsible = false, + isFetching = false, + children, + onSelectionChange, + selectedItems = [], + ...other + } = props; const [expanded, setExpanded] = useState(null); const handleExpand = useCallback((itemId) => { setExpanded((prevState) => (prevState === itemId ? null : itemId)); }, []); + const handleCheckboxChange = useCallback( + (itemId, checked) => { + if (onSelectionChange) { + if (checked) { + onSelectionChange([...selectedItems, itemId]); + } else { + onSelectionChange(selectedItems.filter((id) => id !== itemId)); + } + } + }, + [onSelectionChange, selectedItems] + ); + const hasItems = items.length > 0; if (isFetching) { @@ -91,6 +113,16 @@ export const CippBannerListCard = (props) => { alignItems="center" sx={{ flex: 1, minWidth: 0 }} > + {onSelectionChange && ( + { + e.stopPropagation(); + handleCheckboxChange(item.id, e.target.checked); + }} + onClick={(e) => e.stopPropagation()} + /> + )} { + // Extract data with null safety + const identityPassed = data?.TestResultSummary?.IdentityPassed || 0; + const identityTotal = data?.TestResultSummary?.IdentityTotal || 1; + const devicesPassed = data?.TestResultSummary?.DevicesPassed || 0; + const devicesTotal = data?.TestResultSummary?.DevicesTotal || 0; + + // Determine if we should show devices section + const hasDeviceTests = devicesTotal > 0; + + // Calculate percentages for the radial chart + // If no device tests, set devices to 100% (complete) + const devicesPercentage = hasDeviceTests ? (devicesPassed / devicesTotal) * 100 : 100; + const identityPercentage = (identityPassed / identityTotal) * 100; + + const chartData = [ + { + value: devicesPercentage, + fill: "#22c55e", + }, + { + value: identityPercentage, + fill: "#3b82f6", + }, + ]; + + return ( + + + + Assessment + + } + sx={{ pb: 1.5 }} + /> + + + + + + Identity + + + {isLoading ? ( + + ) : ( + <> + {identityPassed}/{identityTotal} + + tests + + + )} + + + {hasDeviceTests && ( + + + Devices + + + {isLoading ? ( + + ) : ( + <> + {devicesPassed}/{devicesTotal} + + tests + + + )} + + + )} + + + Last Data Collection + + + {isLoading ? ( + + ) : data?.ExecutedAt ? ( + + ) : ( + "Not Available" + )} + + + + + {isLoading ? ( + + ) : ( + + + + + + + )} + + + + + ); +}; diff --git a/src/components/CippComponents/AuthMethodCard.jsx b/src/components/CippComponents/AuthMethodCard.jsx new file mode 100644 index 000000000000..a50baef5b6f2 --- /dev/null +++ b/src/components/CippComponents/AuthMethodCard.jsx @@ -0,0 +1,155 @@ +import { Box, Card, CardHeader, CardContent, Typography, Skeleton } from "@mui/material"; +import { People as UsersIcon } from "@mui/icons-material"; +import { CippSankey } from "./CippSankey"; + +export const AuthMethodCard = ({ data, isLoading }) => { + const processData = () => { + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + const enabledUsers = data.filter((user) => user.AccountEnabled === true); + if (enabledUsers.length === 0) { + return null; + } + + const phishableMethods = ["mobilePhone", "email", "microsoftAuthenticatorPush"]; + const phishResistantMethods = ["fido2", "windowsHelloForBusiness", "x509Certificate"]; + + let singleFactor = 0; + let phishableCount = 0; + let phishResistantCount = 0; + let perUserMFA = 0; + let phoneCount = 0; + let authenticatorCount = 0; + let passkeyCount = 0; + let whfbCount = 0; + + enabledUsers.forEach((user) => { + const methods = user.MFAMethods || []; + const perUser = user.PerUser === "enforced" || user.PerUser === "enabled"; + const hasRegistered = user.MFARegistration === true; + + if (perUser && !hasRegistered && methods.length === 0) { + perUserMFA++; + return; + } + + if (!hasRegistered || methods.length === 0) { + singleFactor++; + return; + } + + const hasPhishResistant = methods.some((m) => phishResistantMethods.includes(m)); + const hasPhishable = methods.some((m) => phishableMethods.includes(m)); + + if (hasPhishResistant) { + phishResistantCount++; + if (methods.includes("fido2") || methods.includes("x509Certificate")) { + passkeyCount++; + } + if (methods.includes("windowsHelloForBusiness")) { + whfbCount++; + } + } else if (hasPhishable) { + phishableCount++; + if (methods.includes("mobilePhone") || methods.includes("email")) { + phoneCount++; + } + if ( + methods.includes("microsoftAuthenticatorPush") || + methods.includes("softwareOneTimePasscode") + ) { + authenticatorCount++; + } + } else { + phishableCount++; + authenticatorCount++; + } + }); + + const mfaPercentage = ( + ((phishableCount + phishResistantCount + perUserMFA) / enabledUsers.length) * + 100 + ).toFixed(1); + const phishResistantPercentage = ((phishResistantCount / enabledUsers.length) * 100).toFixed(1); + + const links = [ + { source: "Users", target: "Single factor", value: singleFactor }, + { source: "Users", target: "Multi factor", value: perUserMFA }, + { source: "Users", target: "Phishable", value: phishableCount }, + { source: "Users", target: "Phish resistant", value: phishResistantCount }, + ]; + + if (phoneCount > 0) links.push({ source: "Phishable", target: "Phone", value: phoneCount }); + if (authenticatorCount > 0) + links.push({ source: "Phishable", target: "Authenticator", value: authenticatorCount }); + + if (passkeyCount > 0) + links.push({ source: "Phish resistant", target: "Passkey", value: passkeyCount }); + if (whfbCount > 0) links.push({ source: "Phish resistant", target: "WHfB", value: whfbCount }); + + const description = `${mfaPercentage}% of enabled users have MFA configured. ${phishResistantPercentage}% use phish-resistant authentication methods.`; + + return { + nodes: [ + { id: "Users", nodeColor: "hsl(28, 100%, 53%)" }, + { id: "Single factor", nodeColor: "hsl(0, 100%, 50%)" }, + { id: "Multi factor", nodeColor: "hsl(200, 70%, 50%)" }, + { id: "Phishable", nodeColor: "hsl(39, 100%, 50%)" }, + { id: "Phone", nodeColor: "hsl(39, 100%, 45%)" }, + { id: "Authenticator", nodeColor: "hsl(39, 100%, 55%)" }, + { id: "Phish resistant", nodeColor: "hsl(99, 70%, 50%)" }, + { id: "Passkey", nodeColor: "hsl(140, 70%, 50%)" }, + { id: "WHfB", nodeColor: "hsl(160, 70%, 50%)" }, + ], + links, + description, + }; + }; + + const processedData = processData(); + + return ( + + + + All users auth methods + + } + sx={{ pb: 1 }} + /> + + + {isLoading ? ( + + ) : processedData ? ( + + ) : ( + + + No authentication method data available + + + )} + + + {!isLoading && processedData?.description && ( + + + {processedData.description} + + + )} + + ); +}; diff --git a/src/components/CippComponents/AuthMethodSankey.jsx b/src/components/CippComponents/AuthMethodSankey.jsx index 6ef4e61e666d..f57c42573c52 100644 --- a/src/components/CippComponents/AuthMethodSankey.jsx +++ b/src/components/CippComponents/AuthMethodSankey.jsx @@ -1,45 +1,159 @@ import { CippSankey } from "./CippSankey"; export const AuthMethodSankey = ({ data }) => { + // Null safety checks + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + // Count enabled users only + const enabledUsers = data.filter((user) => user.AccountEnabled === true); + + if (enabledUsers.length === 0) { + return null; + } + + // Categorize MFA methods as phishable or phish-resistant + const phishableMethods = ["mobilePhone", "email", "microsoftAuthenticatorPush"]; + const phishResistantMethods = ["fido2", "windowsHelloForBusiness", "x509Certificate"]; + + let singleFactor = 0; + let phishableCount = 0; + let phishResistantCount = 0; + let perUserMFA = 0; + + // Breakdown of phishable methods + let phoneCount = 0; + let authenticatorCount = 0; + + // Breakdown of phish-resistant methods + let passkeyCount = 0; + let whfbCount = 0; + + enabledUsers.forEach((user) => { + const methods = user.MFAMethods || []; + const perUser = user.PerUser === "enforced" || user.PerUser === "enabled"; + const hasRegistered = user.MFARegistration === true; + + // If user has per-user MFA enforced but no specific methods, count as generic MFA + if (perUser && !hasRegistered && methods.length === 0) { + perUserMFA++; + return; + } + + // Check if user has any MFA methods + if (!hasRegistered || methods.length === 0) { + singleFactor++; + return; + } + + // Categorize by method type + const hasPhishResistant = methods.some((m) => phishResistantMethods.includes(m)); + const hasPhishable = methods.some((m) => phishableMethods.includes(m)); + + if (hasPhishResistant) { + phishResistantCount++; + // Count specific phish-resistant methods + if (methods.includes("fido2") || methods.includes("x509Certificate")) { + passkeyCount++; + } + if (methods.includes("windowsHelloForBusiness")) { + whfbCount++; + } + } else if (hasPhishable) { + phishableCount++; + // Count specific phishable methods + if (methods.includes("mobilePhone") || methods.includes("email")) { + phoneCount++; + } + if ( + methods.includes("microsoftAuthenticatorPush") || + methods.includes("softwareOneTimePasscode") + ) { + authenticatorCount++; + } + } else { + // Has MFA methods but not in our categorized lists + phishableCount++; + authenticatorCount++; + } + }); + + const mfaPercentage = ( + ((phishableCount + phishResistantCount + perUserMFA) / enabledUsers.length) * + 100 + ).toFixed(1); + const phishResistantPercentage = ((phishResistantCount / enabledUsers.length) * 100).toFixed(1); + + const links = [ + { source: "Users", target: "Single factor", value: singleFactor }, + { source: "Users", target: "Multi factor", value: perUserMFA }, + { source: "Users", target: "Phishable", value: phishableCount }, + { source: "Users", target: "Phish resistant", value: phishResistantCount }, + ]; + + // Add phishable method breakdowns + if (phoneCount > 0) links.push({ source: "Phishable", target: "Phone", value: phoneCount }); + if (authenticatorCount > 0) + links.push({ source: "Phishable", target: "Authenticator", value: authenticatorCount }); + + // Add phish-resistant method breakdowns + if (passkeyCount > 0) + links.push({ source: "Phish resistant", target: "Passkey", value: passkeyCount }); + if (whfbCount > 0) links.push({ source: "Phish resistant", target: "WHfB", value: whfbCount }); + + const description = `${mfaPercentage}% of enabled users have MFA configured. ${phishResistantPercentage}% use phish-resistant authentication methods.`; + return ( - + <> + + {description && ( +
+ {description} +
+ )} + ); }; diff --git a/src/components/CippComponents/CaSankey.jsx b/src/components/CippComponents/CaSankey.jsx index 5b860e45dda5..ffad546d8738 100644 --- a/src/components/CippComponents/CaSankey.jsx +++ b/src/components/CippComponents/CaSankey.jsx @@ -6,24 +6,32 @@ export const CaSankey = ({ data }) => { data={{ nodes: [ { - id: "User sign in", + id: "Enabled users", nodeColor: "hsl(28, 100%, 53%)", }, { - id: "No CA applied", - nodeColor: "hsl(0, 100%, 50%)", + id: "MFA registered", + nodeColor: "hsl(99, 70%, 50%)", }, { - id: "CA applied", - nodeColor: "hsl(12, 76%, 61%)", + id: "Not registered", + nodeColor: "hsl(39, 100%, 50%)", }, { - id: "No MFA", - nodeColor: "hsl(0, 69%, 50%)", + id: "CA policy", + nodeColor: "hsl(99, 70%, 50%)", }, { - id: "MFA", - nodeColor: "hsl(99, 70%, 50%)", + id: "Security defaults", + nodeColor: "hsl(140, 70%, 50%)", + }, + { + id: "Per-user MFA", + nodeColor: "hsl(200, 70%, 50%)", + }, + { + id: "No enforcement", + nodeColor: "hsl(0, 100%, 50%)", }, ], links: data, diff --git a/src/components/CippComponents/CippAddTestReportDrawer.jsx b/src/components/CippComponents/CippAddTestReportDrawer.jsx new file mode 100644 index 000000000000..d7ee1646656f --- /dev/null +++ b/src/components/CippComponents/CippAddTestReportDrawer.jsx @@ -0,0 +1,385 @@ +import React, { useState, useEffect } from "react"; +import { + Button, + Card, + CardContent, + TextField, + Typography, + Box, + Chip, + Tab, + Tabs, + Paper, + Stack, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState, useWatch } from "react-hook-form"; +import { Add } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippApiResults } from "./CippApiResults"; +import { ApiPostCall, ApiGetCall } from "../../api/ApiCall"; + +export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const [activeTab, setActiveTab] = useState(0); + const [searchTerm, setSearchTerm] = useState(""); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + name: "", + description: "", + IdentityTests: [], + DevicesTests: [], + }, + }); + + const { isValid } = useFormState({ control: formControl.control }); + const selectedIdentityTests = + useWatch({ control: formControl.control, name: "IdentityTests" }) || []; + const selectedDeviceTests = + useWatch({ control: formControl.control, name: "DevicesTests" }) || []; + + const createReport = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ListTestReports"], + }); + + // Fetch available tests for the form + const availableTestsApi = ApiGetCall({ + url: "/api/ListAvailableTests", + queryKey: ["ListAvailableTests"], + }); + + const availableTests = availableTestsApi.data || { IdentityTests: [], DevicesTests: [] }; + + // Reset form fields on successful creation + useEffect(() => { + if (createReport.isSuccess) { + formControl.reset({ + name: "", + description: "", + IdentityTests: [], + DevicesTests: [], + }); + } + }, [createReport.isSuccess, formControl]); + + const handleSubmit = () => { + formControl.trigger(); + if (!isValid) { + return; + } + + const values = formControl.getValues(); + Object.keys(values).forEach((key) => { + if (values[key] === "" || values[key] === null) { + delete values[key]; + } + }); + + createReport.mutate({ + url: "/api/AddTestReport", + data: values, + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + setSearchTerm(""); + setActiveTab(0); + formControl.reset({ + name: "", + description: "", + IdentityTests: [], + DevicesTests: [], + }); + }; + + const toggleTest = (testId, testType) => { + const fieldName = testType === "Identity" ? "IdentityTests" : "DevicesTests"; + const currentTests = formControl.getValues(fieldName) || []; + + if (currentTests.includes(testId)) { + formControl.setValue( + fieldName, + currentTests.filter((id) => id !== testId), + { shouldValidate: true } + ); + } else { + formControl.setValue(fieldName, [...currentTests, testId], { shouldValidate: true }); + } + }; + + const isTestSelected = (testId, testType) => { + return testType === "Identity" + ? selectedIdentityTests.includes(testId) + : selectedDeviceTests.includes(testId); + }; + + const filterTests = (tests) => { + if (!searchTerm) return tests; + return tests.filter( + (test) => + test.id.toLowerCase().includes(searchTerm.toLowerCase()) || + test.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }; + + const currentTests = + activeTab === 0 + ? filterTests(availableTests.IdentityTests || []) + : filterTests(availableTests.DevicesTests || []); + + const currentTestType = activeTab === 0 ? "Identity" : "Devices"; + + return ( + <> + + + +
+ + +
+ + } + > + + {/* Report Details Section */} + + + + Report Details + + + + + + + + + + + + + {/* Selection Summary */} + + + + + Selected Tests: + + + + + + Total: {selectedIdentityTests.length + selectedDeviceTests.length} tests + + + + + + {/* Test Selection Section */} + + + + { + setActiveTab(newValue); + setSearchTerm(""); + }} + variant="fullWidth" + > + + Identity Tests + {selectedIdentityTests.length > 0 && ( + + )} + + } + /> + + Device Tests + {selectedDeviceTests.length > 0 && ( + + )} + + } + /> + + + + {/* Search Bar */} + + setSearchTerm(e.target.value)} + /> + + + {/* Test List */} + + {availableTestsApi.isFetching ? ( + + Loading tests... + + ) : currentTests.length === 0 ? ( + + + {searchTerm ? "No tests found matching your search" : "No tests available"} + + + ) : ( + + {currentTests.map((test) => { + const isSelected = isTestSelected(test.id, currentTestType); + return ( + + toggleTest(test.id, currentTestType)} + > + + + + + + + {test.name} + + + {test.description && ( + + {test.description} + + )} + + + + + + ); + })} + + )} + + + + +
+ + ); +}; diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index daaa6dda867a..ea5d9811bda8 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -6,6 +6,8 @@ import { TextField, IconButton, Tooltip, + Box, + Typography, } from "@mui/material"; import { useEffect, useState, useMemo, useCallback, useRef } from "react"; import { useSettings } from "../../hooks/use-settings"; @@ -189,6 +191,12 @@ export const CippAutoComplete = (props) => { typeof api?.valueField === "function" ? api.valueField(option) : option[api?.valueField], + description: + typeof api?.descriptionField === "function" + ? api.descriptionField(option) + : api?.descriptionField + ? option[api?.descriptionField] + : undefined, addedFields, rawData: option, // Store the full original object }; @@ -545,6 +553,21 @@ export const CippAutoComplete = (props) => { )} groupBy={groupBy} renderGroup={renderGroup} + renderOption={(props, option) => { + const { key, ...optionProps } = props; + return ( + + + {option.label} + {option.description && ( + + {option.description} + + )} + + + ); + }} {...other} /> {api?.templateView && ( diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index 25b05ed69a28..b8e5b548e94d 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -4,6 +4,8 @@ import { getCippTranslation } from "../../utils/get-cipp-translation"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; import { useMediaQuery, Grid } from "@mui/system"; import CloseIcon from "@mui/icons-material/Close"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; export const CippOffCanvas = (props) => { const { @@ -17,6 +19,10 @@ export const CippOffCanvas = (props) => { children, size = "sm", footer, + onNavigateUp, + onNavigateDown, + canNavigateUp = false, + canNavigateDown = false, } = props; const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); @@ -84,9 +90,31 @@ export const CippOffCanvas = (props) => { sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", p: 1.5 }} > {title} - - - + + {(canNavigateUp || canNavigateDown) && ( + <> + + + + + + + + )} + + + + { onChange={(nv) => setSelectedTenant(nv)} options={ tenantList.isSuccess && tenantList.data && tenantList.data.length > 0 - ? tenantList.data.map(({ customerId, displayName, defaultDomainName }) => ({ + ? tenantList.data.map(({ customerId, displayName, defaultDomainName, initialDomainName }) => ({ value: defaultDomainName, label: `${displayName} (${defaultDomainName})`, - addedField: { - defaultDomainName: "defaultDomainName", - displayName: "displayName", - customerId: "customerId", + addedFields: { + defaultDomainName: defaultDomainName, + displayName: displayName, + customerId: customerId, + initialDomainName: initialDomainName, }, })) : [] diff --git a/src/components/CippComponents/LicenseCard.jsx b/src/components/CippComponents/LicenseCard.jsx new file mode 100644 index 000000000000..d6762196bc9b --- /dev/null +++ b/src/components/CippComponents/LicenseCard.jsx @@ -0,0 +1,190 @@ +import { Box, Card, CardHeader, CardContent, Typography, Divider, Skeleton } from "@mui/material"; +import { CardMembership as CardMembershipIcon } from "@mui/icons-material"; +import { CippSankey } from "./CippSankey"; + +export const LicenseCard = ({ data, isLoading }) => { + const processData = () => { + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + const topLicenses = data + .filter((license) => license && parseInt(license?.TotalLicenses || 0) > 0) + .sort((a, b) => parseInt(b?.TotalLicenses || 0) - parseInt(a?.TotalLicenses || 0)) + .slice(0, 5); + + if (topLicenses.length === 0) { + return null; + } + + const nodes = []; + const links = []; + + topLicenses.forEach((license, index) => { + if (license) { + const licenseName = + license.License || license.skuPartNumber || license.SkuPartNumber || "Unknown License"; + const shortName = + licenseName.length > 30 ? licenseName.substring(0, 27) + "..." : licenseName; + + const assigned = parseInt(license?.CountUsed || 0) || 0; + const available = parseInt(license?.CountAvailable || 0) || 0; + + nodes.push({ + id: shortName, + nodeColor: `hsl(${210 + index * 30}, 70%, 50%)`, + }); + + const assignedId = `${shortName} - Assigned`; + const availableId = `${shortName} - Available`; + + if (assigned > 0) { + nodes.push({ + id: assignedId, + nodeColor: "hsl(99, 70%, 50%)", + }); + + links.push({ + source: shortName, + target: assignedId, + value: assigned, + }); + } + + if (available > 0) { + nodes.push({ + id: availableId, + nodeColor: "hsl(28, 100%, 53%)", + }); + + links.push({ + source: shortName, + target: availableId, + value: available, + }); + } + } + }); + + if (nodes.length === 0 || links.length === 0) { + return null; + } + + return { nodes, links }; + }; + + const processedData = processData(); + + const calculateStats = () => { + if (!data || !Array.isArray(data)) { + return { total: 0, assigned: 0, available: 0 }; + } + + return { + total: data.reduce((sum, lic) => sum + (parseInt(lic?.TotalLicenses || 0) || 0), 0), + assigned: data.reduce((sum, lic) => sum + (parseInt(lic?.CountUsed || 0) || 0), 0), + available: data.reduce((sum, lic) => sum + (parseInt(lic?.CountAvailable || 0) || 0), 0), + }; + }; + + const stats = calculateStats(); + + return ( + + + + License Overview + + } + sx={{ pb: 1 }} + /> + + + {isLoading ? ( + + ) : processedData ? ( + + ) : ( + + + No license data available + + + )} + + + + + {isLoading ? ( + + + + + + + + + + + + + + + + + ) : data && Array.isArray(data) && data.length > 0 ? ( + + + + Total Licenses + + + {stats.total.toLocaleString()} + + + + + + Assigned + + + {stats.assigned.toLocaleString()} + + + + + + Available + + + {stats.available.toLocaleString()} + + + + ) : ( + + + No license statistics available + + + )} + + + ); +}; diff --git a/src/components/CippComponents/LicenseSankey.jsx b/src/components/CippComponents/LicenseSankey.jsx new file mode 100644 index 000000000000..fd4e1763f260 --- /dev/null +++ b/src/components/CippComponents/LicenseSankey.jsx @@ -0,0 +1,77 @@ +import { CippSankey } from "./CippSankey"; + +export const LicenseSankey = ({ data }) => { + // Null safety checks + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + // Get top 5 licenses by total count with null safety + const topLicenses = data + .filter((license) => license && parseInt(license?.TotalLicenses || 0) > 0) + .sort((a, b) => parseInt(b?.TotalLicenses || 0) - parseInt(a?.TotalLicenses || 0)) + .slice(0, 5); + + if (topLicenses.length === 0) { + return null; + } + + // Create Sankey flow: Top 5 Licenses -> Assigned/Available for each + const nodes = []; + const links = []; + + topLicenses.forEach((license, index) => { + if (license) { + const licenseName = + license.License || license.skuPartNumber || license.SkuPartNumber || "Unknown License"; + const shortName = + licenseName.length > 30 ? licenseName.substring(0, 27) + "..." : licenseName; + + const assigned = parseInt(license?.CountUsed || 0) || 0; + const available = parseInt(license?.CountAvailable || 0) || 0; + + // Add license node + nodes.push({ + id: shortName, + nodeColor: `hsl(${210 + index * 30}, 70%, 50%)`, + }); + + // Add Assigned and Available nodes for this license + const assignedId = `${shortName} - Assigned`; + const availableId = `${shortName} - Available`; + + if (assigned > 0) { + nodes.push({ + id: assignedId, + nodeColor: "hsl(99, 70%, 50%)", + }); + + links.push({ + source: shortName, + target: assignedId, + value: assigned, + }); + } + + if (available > 0) { + nodes.push({ + id: availableId, + nodeColor: "hsl(28, 100%, 53%)", + }); + + links.push({ + source: shortName, + target: availableId, + value: available, + }); + } + } + }); + + // Only render if we have valid data + if (nodes.length === 0 || links.length === 0) { + return null; + } + + return ; +}; diff --git a/src/components/CippComponents/MFACard.jsx b/src/components/CippComponents/MFACard.jsx new file mode 100644 index 000000000000..3b2468415958 --- /dev/null +++ b/src/components/CippComponents/MFACard.jsx @@ -0,0 +1,151 @@ +import { Box, Card, CardHeader, CardContent, Typography, Skeleton } from "@mui/material"; +import { Person as UserIcon } from "@mui/icons-material"; +import { CippSankey } from "./CippSankey"; + +export const MFACard = ({ data, isLoading }) => { + // Process data inside component + const processData = () => { + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + const enabledUsers = data.filter((user) => user.AccountEnabled === true); + if (enabledUsers.length === 0) { + return null; + } + + let registeredUsers = 0; + let notRegisteredUsers = 0; + let registeredCA = 0; + let registeredSD = 0; + let registeredPerUser = 0; + let registeredNone = 0; + let notRegisteredCA = 0; + let notRegisteredSD = 0; + let notRegisteredPerUser = 0; + let notRegisteredNone = 0; + + enabledUsers.forEach((user) => { + const hasRegistered = user.MFARegistration === true; + const coveredByCA = user.CoveredByCA?.startsWith("Enforced") || false; + const coveredBySD = user.CoveredBySD === true; + const perUserEnabled = user.PerUser === "enforced" || user.PerUser === "enabled"; + + if (hasRegistered || perUserEnabled) { + registeredUsers++; + if (perUserEnabled) { + registeredPerUser++; + } else if (coveredByCA) { + registeredCA++; + } else if (coveredBySD) { + registeredSD++; + } else { + registeredNone++; + } + } else { + notRegisteredUsers++; + if (coveredByCA) { + notRegisteredCA++; + } else if (coveredBySD) { + notRegisteredSD++; + } else { + notRegisteredNone++; + } + } + }); + + const registeredPercentage = ((registeredUsers / enabledUsers.length) * 100).toFixed(1); + const protectedPercentage = ( + ((registeredCA + registeredSD + registeredPerUser) / enabledUsers.length) * + 100 + ).toFixed(1); + + const links = [ + { source: "Enabled users", target: "MFA registered", value: registeredUsers }, + { source: "Enabled users", target: "Not registered", value: notRegisteredUsers }, + ]; + + if (registeredCA > 0) + links.push({ source: "MFA registered", target: "CA policy", value: registeredCA }); + if (registeredSD > 0) + links.push({ source: "MFA registered", target: "Security defaults", value: registeredSD }); + if (registeredPerUser > 0) + links.push({ source: "MFA registered", target: "Per-user MFA", value: registeredPerUser }); + if (registeredNone > 0) + links.push({ source: "MFA registered", target: "No enforcement", value: registeredNone }); + + if (notRegisteredCA > 0) + links.push({ source: "Not registered", target: "CA policy", value: notRegisteredCA }); + if (notRegisteredSD > 0) + links.push({ + source: "Not registered", + target: "Security defaults", + value: notRegisteredSD, + }); + if (notRegisteredPerUser > 0) + links.push({ source: "Not registered", target: "Per-user MFA", value: notRegisteredPerUser }); + if (notRegisteredNone > 0) + links.push({ source: "Not registered", target: "No enforcement", value: notRegisteredNone }); + + const description = `${registeredPercentage}% of enabled users have registered MFA methods. ${protectedPercentage}% are protected by policies requiring MFA.`; + + return { + nodes: [ + { id: "Enabled users", nodeColor: "hsl(28, 100%, 53%)" }, + { id: "MFA registered", nodeColor: "hsl(99, 70%, 50%)" }, + { id: "Not registered", nodeColor: "hsl(39, 100%, 50%)" }, + { id: "CA policy", nodeColor: "hsl(99, 70%, 50%)" }, + { id: "Security defaults", nodeColor: "hsl(140, 70%, 50%)" }, + { id: "Per-user MFA", nodeColor: "hsl(200, 70%, 50%)" }, + { id: "No enforcement", nodeColor: "hsl(0, 100%, 50%)" }, + ], + links, + description, + }; + }; + + const processedData = processData(); + + return ( + + + + User authentication + + } + sx={{ pb: 1 }} + /> + + + {isLoading ? ( + + ) : processedData ? ( + + ) : ( + + + No MFA data available + + + )} + + + {!isLoading && processedData?.description && ( + + + {processedData.description} + + + )} + + ); +}; diff --git a/src/components/CippComponents/MFASankey.jsx b/src/components/CippComponents/MFASankey.jsx new file mode 100644 index 000000000000..9fb387cefa9d --- /dev/null +++ b/src/components/CippComponents/MFASankey.jsx @@ -0,0 +1,140 @@ +import { CippSankey } from "./CippSankey"; + +export const MFASankey = ({ data }) => { + // Null safety checks + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + // Count enabled users only + const enabledUsers = data.filter((user) => user.AccountEnabled === true); + + if (enabledUsers.length === 0) { + return null; + } + + // Split by MFA registration status + let registeredUsers = 0; + let notRegisteredUsers = 0; + + // For registered users, split by protection method + let registeredCA = 0; + let registeredSD = 0; + let registeredPerUser = 0; + let registeredNone = 0; + + // For not registered users, split by protection method + let notRegisteredCA = 0; + let notRegisteredSD = 0; + let notRegisteredPerUser = 0; + let notRegisteredNone = 0; + + enabledUsers.forEach((user) => { + const hasRegistered = user.MFARegistration === true; + const coveredByCA = user.CoveredByCA?.startsWith("Enforced") || false; + const coveredBySD = user.CoveredBySD === true; + const perUserEnabled = user.PerUser === "enforced" || user.PerUser === "enabled"; + + // Consider PerUser as MFA enabled/registered + if (hasRegistered || perUserEnabled) { + registeredUsers++; + // Per-User gets its own separate terminal path + if (perUserEnabled) { + registeredPerUser++; + } else if (coveredByCA) { + registeredCA++; + } else if (coveredBySD) { + registeredSD++; + } else { + registeredNone++; + } + } else { + notRegisteredUsers++; + if (coveredByCA) { + notRegisteredCA++; + } else if (coveredBySD) { + notRegisteredSD++; + } else { + notRegisteredNone++; + } + } + }); + + const registeredPercentage = ((registeredUsers / enabledUsers.length) * 100).toFixed(1); + const protectedPercentage = ( + ((registeredCA + registeredSD + registeredPerUser) / enabledUsers.length) * + 100 + ).toFixed(1); + + const links = [ + { source: "Enabled users", target: "MFA registered", value: registeredUsers }, + { source: "Enabled users", target: "Not registered", value: notRegisteredUsers }, + ]; + + // Add protection methods for registered users + if (registeredCA > 0) + links.push({ source: "MFA registered", target: "CA policy", value: registeredCA }); + if (registeredSD > 0) + links.push({ source: "MFA registered", target: "Security defaults", value: registeredSD }); + if (registeredPerUser > 0) + links.push({ source: "MFA registered", target: "Per-user MFA", value: registeredPerUser }); + if (registeredNone > 0) + links.push({ source: "MFA registered", target: "No enforcement", value: registeredNone }); + + // Add protection methods for not registered users + if (notRegisteredCA > 0) + links.push({ source: "Not registered", target: "CA policy", value: notRegisteredCA }); + if (notRegisteredSD > 0) + links.push({ source: "Not registered", target: "Security defaults", value: notRegisteredSD }); + if (notRegisteredPerUser > 0) + links.push({ source: "Not registered", target: "Per-user MFA", value: notRegisteredPerUser }); + if (notRegisteredNone > 0) + links.push({ source: "Not registered", target: "No enforcement", value: notRegisteredNone }); + + const description = `${registeredPercentage}% of enabled users have registered MFA methods. ${protectedPercentage}% are protected by policies requiring MFA.`; + + return ( + <> + + {description && ( +
+ {description} +
+ )} + + ); +}; diff --git a/src/components/CippComponents/ScheduledTaskDetails.jsx b/src/components/CippComponents/ScheduledTaskDetails.jsx index ba52c316646a..c02cf9d8f3f6 100644 --- a/src/components/CippComponents/ScheduledTaskDetails.jsx +++ b/src/components/CippComponents/ScheduledTaskDetails.jsx @@ -24,7 +24,7 @@ import { ActionsMenu } from "/src/components/actions-menu"; import { CippScheduledTaskActions } from "./CippScheduledTaskActions"; import { CippApiLogsDrawer } from "./CippApiLogsDrawer"; -const ScheduledTaskDetails = ({ data, showActions = true }) => { +const ScheduledTaskDetails = ({ data, showActions = true, showTitle = true }) => { const [taskDetails, setTaskDetails] = useState(null); const [expanded, setExpanded] = useState(false); const [searchQuery, setSearchQuery] = useState(""); @@ -84,7 +84,7 @@ const ScheduledTaskDetails = ({ data, showActions = true }) => { - {taskDetailResults.isLoading ? : taskDetails?.Task?.Name} + {showTitle && (taskDetailResults.isLoading ? : taskDetails?.Task?.Name)} {showActions && ( diff --git a/src/components/CippComponents/SecureScoreCard.jsx b/src/components/CippComponents/SecureScoreCard.jsx new file mode 100644 index 000000000000..da849c069b3c --- /dev/null +++ b/src/components/CippComponents/SecureScoreCard.jsx @@ -0,0 +1,162 @@ +import { Box, Card, CardHeader, CardContent, Typography, Divider, Skeleton } from "@mui/material"; +import { Security as SecurityIcon } from "@mui/icons-material"; +import { + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + ResponsiveContainer, + Tooltip as RechartsTooltip, +} from "recharts"; + +export const SecureScoreCard = ({ data, isLoading }) => { + return ( + + + + Secure Score + + } + sx={{ pb: 1 }} + /> + + {isLoading ? ( + <> + + + + + + + The Secure Score measures your security posture across your tenant. + + + ) : !data || !Array.isArray(data) || data.length === 0 ? ( + <> + + + + No secure score data available + + + + + The Secure Score measures your security posture across your tenant. + + + ) : ( + <> + + + new Date(a.createdDateTime) - new Date(b.createdDateTime)) + .map((score) => ({ + date: new Date(score.createdDateTime).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + score: score.currentScore, + percentage: Math.round((score.currentScore / score.maxScore) * 100), + }))} + margin={{ left: 12, right: 12, top: 10, bottom: 10 }} + > + + + + { + if (name === "score") return [value.toFixed(2), "Score"]; + if (name === "percentage") return [value + "%", "Percentage"]; + return value; + }} + /> + + + + + + The Secure Score measures your security posture across your tenant. + + + )} + + + + {isLoading ? ( + + + + + + + + + + + + + + ) : !data || !Array.isArray(data) || data.length === 0 ? ( + + Enable secure score monitoring in your tenant + + ) : ( + + + + Latest % + + + {Math.round( + (data[data.length - 1].currentScore / data[data.length - 1].maxScore) * 100 + )} + % + + + + + + Current Score + + + {data[data.length - 1].currentScore.toFixed(2)} + + + + + + Max Score + + + {data[data.length - 1].maxScore.toFixed(2)} + + + + )} + + + ); +}; diff --git a/src/components/CippComponents/SecureScoreChart.jsx b/src/components/CippComponents/SecureScoreChart.jsx new file mode 100644 index 000000000000..f9830d128481 --- /dev/null +++ b/src/components/CippComponents/SecureScoreChart.jsx @@ -0,0 +1,153 @@ +import { Box, Typography, Divider, Skeleton } from "@mui/material"; +import { + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + ResponsiveContainer, + Tooltip as RechartsTooltip, +} from "recharts"; + +export const SecureScoreChart = ({ data, isLoading }) => { + if (isLoading) { + return ( + <> + + + + + + + The Secure Score measures your security posture across your tenant. + + + + + + + + + + + + + + + + + ); + } + + if (!data || !Array.isArray(data) || data.length === 0) { + return ( + <> + + + + No secure score data available + + + + + The Secure Score measures your security posture across your tenant. + + + + + Enable secure score monitoring in your tenant + + + + ); + } + + const sortedData = [...data].sort( + (a, b) => new Date(a.createdDateTime) - new Date(b.createdDateTime) + ); + + const chartData = sortedData.map((score) => ({ + date: new Date(score.createdDateTime).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + score: score.currentScore, + percentage: Math.round((score.currentScore / score.maxScore) * 100), + })); + + const latestScore = sortedData[sortedData.length - 1]; + const latestPercentage = Math.round((latestScore.currentScore / latestScore.maxScore) * 100); + + return ( + <> + + + + + + + { + if (name === "score") return [value.toFixed(2), "Score"]; + if (name === "percentage") return [value + "%", "Percentage"]; + return value; + }} + /> + + + + + + The Secure Score measures your security posture across your tenant. + + + + + + Latest % + + + {latestPercentage}% + + + + + + Current Score + + + {latestScore.currentScore.toFixed(2)} + + + + + + Max Score + + + {latestScore.maxScore.toFixed(2)} + + + + + ); +}; diff --git a/src/components/CippComponents/TenantInfoCard.jsx b/src/components/CippComponents/TenantInfoCard.jsx new file mode 100644 index 000000000000..cd4b753e8f2c --- /dev/null +++ b/src/components/CippComponents/TenantInfoCard.jsx @@ -0,0 +1,70 @@ +import { Box, Card, CardHeader, CardContent, Typography, Skeleton } from "@mui/material"; +import { Business as BuildingIcon } from "@mui/icons-material"; +import { CippCopyToClipBoard } from "./CippCopyToClipboard"; + +export const TenantInfoCard = ({ data, isLoading }) => { + return ( + + + + Tenant + + } + sx={{ pb: 1.5 }} + /> + + + + + Name + + {isLoading ? ( + + ) : ( + + {data?.displayName || "Not Available"} + + )} + + + + Tenant ID + + + {isLoading ? ( + + ) : data?.id ? ( + + ) : ( + + Not Available + + )} + + + + + Primary Domain + + + {isLoading ? ( + + ) : data?.verifiedDomains?.find((d) => d.isDefault)?.name ? ( + d.isDefault).name} + type="chip" + /> + ) : ( + + Not Available + + )} + + + + + + ); +}; diff --git a/src/components/CippComponents/TenantMetricsGrid.jsx b/src/components/CippComponents/TenantMetricsGrid.jsx new file mode 100644 index 000000000000..b8b0cfacc272 --- /dev/null +++ b/src/components/CippComponents/TenantMetricsGrid.jsx @@ -0,0 +1,100 @@ +import { Box, Grid, Tooltip, Avatar, Typography, Skeleton } from "@mui/material"; +import { + Person as UserIcon, + PersonOutline as GuestIcon, + Group as GroupIcon, + Apps as AppsIcon, + Devices as DevicesIcon, + PhoneAndroid as ManagedIcon, +} from "@mui/icons-material"; + +const formatNumber = (num) => { + if (num >= 1000000) return (num / 1000000).toFixed(1) + "M"; + if (num >= 1000) return (num / 1000).toFixed(1) + "K"; + return num?.toString() || "0"; +}; + +export const TenantMetricsGrid = ({ data, isLoading }) => { + const metrics = [ + { + label: "Users", + value: data?.UserCount || 0, + icon: UserIcon, + color: "primary", + }, + { + label: "Guests", + value: data?.GuestCount || 0, + icon: GuestIcon, + color: "info", + }, + { + label: "Groups", + value: data?.GroupCount || 0, + icon: GroupIcon, + color: "secondary", + }, + { + label: "Service Principals", + value: data?.ApplicationCount || 0, + icon: AppsIcon, + color: "error", + }, + { + label: "Devices", + value: data?.DeviceCount || 0, + icon: DevicesIcon, + color: "warning", + }, + { + label: "Managed", + value: data?.ManagedDeviceCount || 0, + icon: ManagedIcon, + color: "success", + }, + ]; + + return ( + + {metrics.map((metric) => { + const IconComponent = metric.icon; + return ( + + + + + + + + + {metric.label} + + + {isLoading ? : formatNumber(metric.value)} + + + + + + ); + })} + + ); +}; diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index bfc03594152e..79b42de33f6f 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -606,9 +606,10 @@ const CippAddEditUser = (props) => { label: tenantGroup.displayName, value: tenantGroup.id, addedFields: { - calculatedGroupType: tenantGroup.calculatedGroupType, + groupType: tenantGroup.groupType, }, }))} + creatable={false} formControl={formControl} /> @@ -624,9 +625,10 @@ const CippAddEditUser = (props) => { label: userGroups.DisplayName, value: userGroups.id, addedFields: { - calculatedGroupType: userGroups.calculatedGroupType, + groupType: userGroups.groupType, }, }))} + creatable={false} formControl={formControl} /> diff --git a/src/components/CippFormPages/CippCustomDataMappingForm.jsx b/src/components/CippFormPages/CippCustomDataMappingForm.jsx index acb0c7134a56..f8dce65b8592 100644 --- a/src/components/CippFormPages/CippCustomDataMappingForm.jsx +++ b/src/components/CippFormPages/CippCustomDataMappingForm.jsx @@ -42,7 +42,7 @@ const CippCustomDataMappingForm = ({ formControl }) => { multiple: false, placeholder: "Select a Source Type", options: [ - { value: "extensionSync", label: "Extension Sync" }, + { value: "reportingDb", label: "Reporting DB" }, { value: "manualEntry", label: "Manual Entry" }, ], }; @@ -65,7 +65,7 @@ const CippCustomDataMappingForm = ({ formControl }) => { condition: { field: "sourceType", compareType: "valueEq", - compareValue: "extensionSync", + compareValue: "reportingDb", }, }, { @@ -219,7 +219,7 @@ const CippCustomDataMappingForm = ({ formControl }) => { - {selectedSourceType?.value === "extensionSync" && ( + {selectedSourceType?.value === "reportingDb" && ( <> @@ -282,7 +282,7 @@ const CippCustomDataMappingForm = ({ formControl }) => { - {selectedExtensionSyncDataset && selectedSourceType?.value === "extensionSync" && ( + {selectedExtensionSyncDataset && selectedSourceType?.value === "reportingDb" && ( { return ( - - - {/* Main offcanvas */} - setOffcanvasVisible(false)} - title={`${cat}.${obj} Endpoints`} - > - - - Listed below are the available API endpoints based on permission level. - ReadWrite level includes endpoints under Read. - - {Object.keys(apiPermissions[cat][obj]).map((type, typeIndex) => { - var items = []; - for (var api in apiPermissions[cat][obj][type]) { - const apiFunction = apiPermissions[cat][obj][type][api]; - items.push({ - name: apiFunction.Name, - description: apiFunction.Description?.[0]?.Text || null - }); - } - return ( - - {type} - - {items.map((item, idx) => ( - - - {item.name} - - {item.description && ( - - )} - - ))} - - - ); - })} - - - - {/* Description offcanvas */} - setDescriptionOffcanvasVisible(false)} - title="Function Description" - > - - - {selectedDescription.name} - - - {selectedDescription.description} - - - - - ); - }; - - return ( - <> - - - - ({ - label: role.RowKey, - value: role.RowKey, - }))} - isFetching={customRoleListFetching} - refreshFunction={() => refetchCustomRoleList()} - creatable={true} - formControl={formControl} - multiple={false} - fullWidth={true} - /> - {cippApiRoleSelected && ( - - This is the default role for all API clients in the CIPP-API integration. If you - would like different permissions for specific applications, create a role per - application and select it from the CIPP-API integrations page. - - )} - - - - {allTenantSelected && blockedTenants?.length == 0 && ( - - All tenants selected, no tenant restrictions will be applied unless blocked tenants - are specified. - - )} - - {allTenantSelected && ( - - - - )} - - {currentRole && ( - <> - {apiPermissionFetching && } - {apiPermissionSuccess && ( - <> - API Permissions - - Set All Permissions - - - - - - - <> - {Object.keys(apiPermissions) - .sort() - .map((cat, catIndex) => ( - - }> - {cat} - - - {Object.keys(apiPermissions[cat]) - .sort() - .map((obj, index) => { - return ( - - - - ); - })} - - - ))} - - - - )} - - )} - - - - {selectedRole && selectedTenant?.length > 0 && ( - <> -
Allowed Tenants
-
    - {selectedTenant.map((tenant, idx) => ( -
  • {tenant?.label}
  • - ))} -
- - )} - {selectedRole && blockedTenants?.length > 0 && ( - <> -
Blocked Tenants
-
    - {blockedTenants.map((tenant, idx) => ( -
  • {tenant?.label}
  • - ))} -
- - )} - {selectedRole && selectedPermissions && ( - <> -
Selected Permissions
-
    - {selectedPermissions && - Object.keys(selectedPermissions) - ?.sort() - .map((cat, idx) => ( - <> - {selectedPermissions?.[cat] && - !selectedPermissions?.[cat]?.includes("None") && ( -
  • {selectedPermissions[cat]}
  • - )} - - ))} -
- - )} -
-
- - - - - {currentRole && ( - - )} - - - - ); -}; - -export default CippCustomRoles; diff --git a/src/components/CippSettings/CippJitAdminSettings.jsx b/src/components/CippSettings/CippJitAdminSettings.jsx new file mode 100644 index 000000000000..f4c175e6c65d --- /dev/null +++ b/src/components/CippSettings/CippJitAdminSettings.jsx @@ -0,0 +1,128 @@ +import { Button, Typography, Alert, Box } from "@mui/material"; +import { ClockIcon } from "@heroicons/react/24/outline"; +import CippButtonCard from "/src/components/CippCards/CippButtonCard"; +import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { useForm } from "react-hook-form"; +import { useEffect } from "react"; + +const CippJitAdminSettings = () => { + const jitSettings = ApiGetCall({ + url: "/api/ExecJITAdminSettings?Action=Get", + queryKey: "jitAdminSettings", + }); + + const jitChange = ApiPostCall({ + datafromUrl: true, + relatedQueryKeys: ["jitAdminSettings"], + }); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + MaxDuration: "", + }, + }); + + useEffect(() => { + if (jitSettings.isSuccess && jitSettings.data) { + formControl.reset({ + MaxDuration: jitSettings.data?.MaxDuration || "", + }); + } + }, [jitSettings.isSuccess, jitSettings.data]); + + const handleSave = () => { + const formData = formControl.getValues(); + jitChange.mutate({ + url: "/api/ExecJITAdminSettings", + data: { + Action: "Set", + MaxDuration: formData.MaxDuration || null, + }, + queryKey: "jitAdminSettingsPost", + }); + }; + + return ( + } + > + Save Settings + + } + > + + + Configure maximum allowed duration for Just-In-Time (JIT) admin accounts. This setting + helps enforce security policies by preventing technicians from creating JIT admin accounts + with excessively long lifespans. + + + {/* Maximum Duration Section */} + + + Maximum Duration + + { + // Allow empty value (no limit) + if (!value || typeof value !== "string" || value.trim() === "") { + return true; + } + const iso8601Regex = + /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; + if (!iso8601Regex.test(value)) { + return "Invalid format. Use PT1H, P1D, P7D, P28D, etc."; + } + return true; + }, + }, + }} + formControl={formControl} + /> + + + + + Leave empty for no limit on JIT admin account duration. When set, technicians cannot + create JIT admin accounts with durations exceeding this limit. This setting applies + globally to all tenants. + + + + {/* API Results */} + + + + ); +}; + +export default CippJitAdminSettings; diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx index b2212bb39303..86bf06168ac9 100644 --- a/src/components/CippSettings/CippRoleAddEdit.jsx +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -68,6 +68,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { const setDefaults = useWatch({ control: formControl.control, name: "Defaults" }); const selectedPermissions = useWatch({ control: formControl.control, name: "Permissions" }); const selectedEntraGroup = useWatch({ control: formControl.control, name: "EntraGroup" }); + const ipRanges = useWatch({ control: formControl.control, name: "IPRange" }); const { data: apiPermissions = [], @@ -87,7 +88,11 @@ export const CippRoleAddEdit = ({ selectedRole }) => { queryKey: "customRoleList", }); - const { data: { pages = [] } = {}, isSuccess: tenantsSuccess } = ApiGetCallWithPagination({ + const { + data: { pages = [] } = {}, + isSuccess: tenantsSuccess, + isFetching: tenantsFetching, + } = ApiGetCallWithPagination({ url: "/api/ListTenants?AllTenantSelector=true", queryKey: "ListTenants-All", }); @@ -240,6 +245,13 @@ export const CippRoleAddEdit = ({ selectedRole }) => { value: endpoint, })) || []; + // Process IP ranges + const processedIPRanges = + currentPermissions?.IPRange?.map((ip) => ({ + label: ip, + value: ip, + })) || []; + formControl.reset({ Permissions: basePermissions && Object.keys(basePermissions).length > 0 @@ -249,6 +261,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { allowedTenants: newAllowedTenants, blockedTenants: newBlockedTenants, BlockedEndpoints: processedBlockedEndpoints, + IPRange: processedIPRanges, EntraGroup: currentPermissions?.EntraGroup, }); } @@ -340,6 +353,11 @@ export const CippRoleAddEdit = ({ selectedRole }) => { return endpoint.value || endpoint; }) || []; + const processedIPRanges = + ipRanges?.map((ip) => { + return ip?.value || ip; + }) || []; + updatePermissions.mutate({ url: "/api/ExecCustomRole?Action=AddUpdate", data: { @@ -349,6 +367,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { AllowedTenants: processedAllowedTenants, BlockedTenants: processedBlockedTenants, BlockedEndpoints: processedBlockedEndpoints, + IPRange: processedIPRanges, }, }); }; @@ -509,6 +528,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { dataKey: "Results", labelField: "displayName", valueField: "id", + showRefresh: true, }} formControl={formControl} fullWidth={true} @@ -612,6 +632,26 @@ export const CippRoleAddEdit = ({ selectedRole }) => {
)} + + + {apiPermissionFetching && ( <> @@ -821,6 +861,16 @@ export const CippRoleAddEdit = ({ selectedRole }) => { )} + {ipRanges?.length > 0 && ( + <> +
Allowed IP Ranges
+
    + {ipRanges.map((ip, idx) => ( +
  • {ip?.value || ip?.label || ip}
  • + ))} +
+ + )} {selectedPermissions && apiPermissionSuccess && ( <>
Selected Permissions
@@ -849,7 +899,13 @@ export const CippRoleAddEdit = ({ selectedRole }) => { className="me-2" type="submit" variant="contained" - disabled={updatePermissions.isPending || customRoleListFetching || !formState.isValid} + disabled={ + updatePermissions.isPending || + customRoleListFetching || + apiPermissionFetching || + tenantsFetching || + !formState.isValid + } startIcon={ diff --git a/src/components/CippSettings/CippRoles.jsx b/src/components/CippSettings/CippRoles.jsx index c155064b634a..34b1f08dcd78 100644 --- a/src/components/CippSettings/CippRoles.jsx +++ b/src/components/CippSettings/CippRoles.jsx @@ -44,7 +44,7 @@ const CippRoles = () => { disableVariables: true, }, ], - relatedQueryKeys: ["customRoleList"], + relatedQueryKeys: ["customRoleList", "customRoleTable"], confirmText: "Are you sure you want to clone this custom role?", condition: (row) => row?.Type === "Custom", }, @@ -63,7 +63,7 @@ const CippRoles = () => { RoleName: "RoleName", }, condition: (row) => row?.Type === "Custom", - relatedQueryKeys: ["customRoleList"], + relatedQueryKeys: ["customRoleList", "customRoleTable"], }, ]; diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index afbd54594256..e5c90b075932 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -714,6 +714,14 @@ const CippStandardAccordion = ({ {accordionTitle} + {standard.deprecated && ( + + )} {/* Hide action chips in drift mode */} {!isDriftMode && selectedActions && selectedActions?.length > 0 && ( <> @@ -780,10 +788,21 @@ const CippStandardAccordion = ({ {standard.multiple && ( - - handleAddMultipleStandard(standardName)}> - - + + + handleAddMultipleStandard(standardName)} + disabled={standard.deprecated} + > + + + )} - + {standard.deprecated && ( + + + ⚠️ This standard is deprecated and cannot be configured. Please remove it + from your template and use an alternative standard if available. + + + )} + {isDriftMode ? ( /* Drift mode layout - full width with slider first */ diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index d74d6f4d3630..6873936d9cda 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -102,9 +102,6 @@ const StandardCard = memo( height: "100%", display: "flex", flexDirection: "column", - ...(isNewStandard(standard.addedDate) && { - mt: 1.2, // Add top margin to accommodate the "New" label - }), }} > {isNewStandard(standard.addedDate) && ( @@ -123,6 +120,22 @@ const StandardCard = memo( }} /> )} + {standard.deprecated && ( + + )} @@ -243,7 +262,34 @@ const StandardCard = memo( - {standard.multiple ? ( + {standard.deprecated ? ( + + + } + label={ + isSelected + ? "Remove this standard from the template" + : "This standard is deprecated" + } + /> + {!isSelected && ( + + This standard is deprecated and cannot be added. Please use an alternative + standard if available. + + )} + + ) : standard.multiple ? ( } label="Add this standard to the template" @@ -329,7 +376,7 @@ const VirtualizedStandardGrid = memo(({ items, renderItem }) => { overscan={5} defaultItemHeight={320} // Provide estimated row height for better virtualization itemContent={(index) => ( - + {standard.label}
+ {standard.deprecated && ( + + )} {isNewStandard(standard.addedDate) && ( - {standard.multiple ? ( + {standard.deprecated ? ( + isSelected ? ( + + } + label="Remove" + sx={{ mr: 1 }} + /> + ) : ( + + Deprecated - Cannot be added + + ) + ) : standard.multiple ? ( { return ( @@ -336,6 +340,39 @@ export const CIPPTableToptoolbar = ({ restoredFiltersRef.current.clear(); }, [pageName]); + // Detect overflow and switch to compact mode + useEffect(() => { + const checkOverflow = () => { + if (!leftContainerRef.current || !actionsContainerRef.current) { + return; + } + + const leftContainerWidth = leftContainerRef.current.offsetWidth; + const leftContainerScrollWidth = leftContainerRef.current.scrollWidth; + const actionsWidth = actionsContainerRef.current.scrollWidth; + const isOverflowing = leftContainerScrollWidth > leftContainerWidth; + const shouldBeCompact = isOverflowing || actionsWidth > leftContainerWidth * 0.6; // Actions taking > 60% of left container + + setUseCompactMode(shouldBeCompact); + }; + + // Check immediately on mount and when dependencies change + checkOverflow(); + + // Also check after a brief delay to ensure elements are fully rendered + const timeoutId = setTimeout(checkOverflow, 100); + + const resizeObserver = new ResizeObserver(checkOverflow); + if (leftContainerRef.current) { + resizeObserver.observe(leftContainerRef.current); + } + + return () => { + clearTimeout(timeoutId); + resizeObserver.disconnect(); + }; + }, [hasSelection, customBulkActions.length, exportEnabled, filters?.length, usedColumns?.length]); + // Restore last used filter on mount if persistFilters is enabled (non-graph filters) useEffect(() => { // Wait for table to be initialized and data to be available @@ -593,6 +630,7 @@ export const CIPPTableToptoolbar = ({ return ( <> {/* Left side - Main controls */} @@ -675,9 +714,22 @@ export const CIPPTableToptoolbar = ({ /> - {/* Desktop Buttons */} + {/* Desktop Buttons - always render for measurement, hide when in compact mode */} {!mdDown && ( - <> + {/* Filters Button */} } @@ -811,7 +863,17 @@ export const CIPPTableToptoolbar = ({ Export )} - + + )} + + {/* Mobile/Compact Action Button */} + {(mdDown || useCompactMode) && !hasSelection && ( + setActionMenuAnchor(event.currentTarget)} + sx={{ flexShrink: 0 }} + > + + )} {/* Mobile Action Menu */} @@ -1065,24 +1127,6 @@ export const CIPPTableToptoolbar = ({ )} - - {/* Mobile Action Menu */} - {mdDown && ( - setActionMenuAnchor(event.currentTarget)} - size="small" - sx={{ - height: "40px", - width: "40px", - border: "1px solid", - borderColor: "divider", - borderRadius: "8px", - ml: "auto", - }} - > - - - )} {/* Right side - Additional controls */} @@ -1249,7 +1293,6 @@ export const CIPPTableToptoolbar = ({ }} > - API Response { simple = false, cardButton, offCanvas = false, + offCanvasOnRowClick = false, noCard = false, hideTitle = false, refreshFunction, @@ -114,6 +115,8 @@ export const CippDataTable = (props) => { const [usedColumns, setUsedColumns] = useState([]); const [offcanvasVisible, setOffcanvasVisible] = useState(false); const [offCanvasData, setOffCanvasData] = useState({}); + const [offCanvasRowIndex, setOffCanvasRowIndex] = useState(0); + const [filteredRows, setFilteredRows] = useState([]); const [customComponentData, setCustomComponentData] = useState({}); const [customComponentVisible, setCustomComponentVisible] = useState(false); const [actionData, setActionData] = useState({ data: {}, action: {}, ready: false }); @@ -280,12 +283,45 @@ export const CippDataTable = (props) => { baseBackgroundColor: theme.palette.background.paper, }), muiTablePaperProps: ({ table }) => ({ - //not sx - style: { - zIndex: table.getState().isFullScreen ? 1000 : undefined, - top: table.getState().isFullScreen ? 64 : undefined, + sx: { + ...(table.getState().isFullScreen && { + position: 'fixed !important', + top: '64px !important', + bottom: '0 !important', + left: { xs: '0 !important', lg: settings?.sidebarCollapse ? '73px !important' : '270px !important' }, + right: '0 !important', + zIndex: '1300 !important', + m: '0 !important', + p: '16px !important', + overflow: 'auto', + bgcolor: 'background.paper', + maxWidth: 'none !important', + width: 'auto !important', + height: 'auto !important', + }), }, }), + muiTableBodyRowProps: + offCanvasOnRowClick && offCanvas + ? ({ row }) => ({ + onClick: () => { + setOffCanvasData(row.original); + // Find the index of this row in the filtered rows + const filteredRowsArray = table.getFilteredRowModel().rows; + const indexInFiltered = filteredRowsArray.findIndex( + (r) => r.original === row.original + ); + setOffCanvasRowIndex(indexInFiltered >= 0 ? indexInFiltered : 0); + setOffcanvasVisible(true); + }, + sx: { + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }, + }) + : undefined, // Add global styles to target the specific filter components enableColumnFilterModes: true, muiTableHeadCellProps: { @@ -437,6 +473,12 @@ export const CippDataTable = (props) => { onClick={() => { closeMenu(); setOffCanvasData(row.original); + // Find the index of this row in the filtered rows + const filteredRowsArray = table.getFilteredRowModel().rows; + const indexInFiltered = filteredRowsArray.findIndex( + (r) => r.original === row.original + ); + setOffCanvasRowIndex(indexInFiltered >= 0 ? indexInFiltered : 0); setOffcanvasVisible(true); }} > @@ -452,6 +494,12 @@ export const CippDataTable = (props) => { onClick={() => { closeMenu(); setOffCanvasData(row.original); + // Find the index of this row in the filtered rows + const filteredRowsArray = table.getFilteredRowModel().rows; + const indexInFiltered = filteredRowsArray.findIndex( + (r) => r.original === row.original + ); + setOffCanvasRowIndex(indexInFiltered >= 0 ? indexInFiltered : 0); setOffcanvasVisible(true); }} > @@ -668,6 +716,19 @@ export const CippDataTable = (props) => { } }, [table.getSelectedRowModel().rows]); + useEffect(() => { + // Update filtered rows whenever table filtering/sorting changes + if (table && table.getFilteredRowModel) { + const rows = table.getFilteredRowModel().rows; + setFilteredRows(rows.map((row) => row.original)); + } + }, [ + table, + table.getState().columnFilters, + table.getState().globalFilter, + table.getState().sorting, + ]); + useEffect(() => { //check if the simplecolumns are an array, if (Array.isArray(simpleColumns) && simpleColumns.length > 0) { @@ -742,8 +803,27 @@ export const CippDataTable = (props) => { extendedData={offCanvasData} extendedInfoFields={offCanvas?.extendedInfoFields} actions={actions} - children={offCanvas?.children} + title={offCanvasData?.Name || offCanvas?.title || "Extended Info"} + children={ + offCanvas?.children ? (row) => offCanvas.children(row, offCanvasRowIndex) : undefined + } customComponent={offCanvas?.customComponent} + onNavigateUp={() => { + const newIndex = offCanvasRowIndex - 1; + if (newIndex >= 0 && filteredRows && filteredRows[newIndex]) { + setOffCanvasRowIndex(newIndex); + setOffCanvasData(filteredRows[newIndex]); + } + }} + onNavigateDown={() => { + const newIndex = offCanvasRowIndex + 1; + if (filteredRows && newIndex < filteredRows.length) { + setOffCanvasRowIndex(newIndex); + setOffCanvasData(filteredRows[newIndex]); + } + }} + canNavigateUp={offCanvasRowIndex > 0} + canNavigateDown={filteredRows && offCanvasRowIndex < filteredRows.length - 1} {...offCanvas} /> {/* Render custom component */} diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js index 3167e5b7ca91..6ce020886005 100644 --- a/src/components/CippTable/CippGraphExplorerFilter.js +++ b/src/components/CippTable/CippGraphExplorerFilter.js @@ -47,6 +47,7 @@ const CippGraphExplorerFilter = ({ $expand: "", $top: "", $search: "", + $orderby: "", $format: "", NoPagination: false, ReverseTenantLookup: false, @@ -326,6 +327,10 @@ const CippGraphExplorerFilter = ({ Key: "$expand", Value: formParameters.$expand, }, + { + Key: "$orderby", + Value: formParameters.$orderby, + }, { Key: "$format", Value: formParameters.$format, @@ -722,6 +727,17 @@ const CippGraphExplorerFilter = ({ /> + {/* OrderBy Field */} + + + + {/* Format Field */} { + switch (status?.toLowerCase()) { + case "passed": + return "success"; + case "failed": + return "error"; + case "investigate": + return "warning"; + case "skipped": + return "default"; + default: + return "default"; + } +}; + +const getRiskColor = (risk) => { + switch (risk?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } +}; + +const getImpactColor = (impact) => { + switch (impact?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } +}; + +const checkCIPPStandardAvailable = (testName) => { + if (!testName) return "No"; + console.log(testName); + // Check if any standard's tag array contains a reference to this test + const hasStandard = standardsData.some((standard) => { + if (!standard.tag || !Array.isArray(standard.tag)) return false; + // Check if any tag matches the test name or contains it + return standard.tag.some((tag) => { + const tagLower = tag.toLowerCase(); + const testLower = testName.toLowerCase(); + return tagLower.includes(testLower) || testLower.includes(tagLower); + }); + }); + + return hasStandard ? "Yes" : "No"; +}; + +// Shared markdown styling for consistent rendering +const markdownStyles = { + "& a": { + color: (theme) => theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + "& p": { + my: 1, + }, + "& ul": { + my: 1, + pl: 2, + }, + "& li": { + my: 0.5, + }, + "& h1, & h2, & h3, & h4, & h5, & h6": { + mt: 2, + mb: 1, + fontWeight: "bold", + }, + "& table": { + width: "100%", + borderCollapse: "collapse", + marginTop: 2, + marginBottom: 2, + }, + "& th, & td": { + border: 1, + borderColor: "divider", + padding: 1, + textAlign: "left", + }, + "& th": { + backgroundColor: "action.hover", + fontWeight: "bold", + }, + "& code": { + backgroundColor: "action.hover", + padding: "2px 6px", + borderRadius: 1, + fontSize: "0.85em", + }, + "& pre": { + backgroundColor: "action.hover", + padding: 2, + borderRadius: 1, + overflow: "auto", + }, +}; + +export const CippTestDetailOffCanvas = ({ row }) => { + return ( + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + Risk + + + + + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + User Impact + + + + + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + Effort + + + + + + + + + + + + Standard Available + + + + + + + + + + + {row.ResultMarkdown && ( + + + + {row.Name} + + + + ( + + {children} + + ), + }} + > + {row.ResultMarkdown} + + + + + )} + + + + + + What did we check + + + {row.Category && ( + + + Category + + {row.Category} + + )} + + {row.Description && ( + + ( + + {children} + + ), + }} + > + {row.Description} + + + )} + + + + + ); +}; diff --git a/src/components/ExecutiveReportButton.js b/src/components/ExecutiveReportButton.js index e7d0cdde65de..922a3c550850 100644 --- a/src/components/ExecutiveReportButton.js +++ b/src/components/ExecutiveReportButton.js @@ -2533,7 +2533,7 @@ const ExecutiveReportDocument = ({ }; export const ExecutiveReportButton = (props) => { - const { tenantName, tenantId, userStats, standardsData, organizationData, ...other } = props; + const { ...other } = props; const settings = useSettings(); const brandingSettings = settings.customBranding; @@ -2550,6 +2550,22 @@ export const ExecutiveReportButton = (props) => { infographics: true, }); + // Fetch organization data - only when preview is open + const organization = ApiGetCall({ + url: "/api/ListOrg", + queryKey: `${settings.currentTenant}-ListOrg-report`, + data: { tenantFilter: settings.currentTenant }, + waiting: previewOpen, + }); + + // Fetch user counts - only when preview is open + const dashboard = ApiGetCall({ + url: "/api/ListuserCounts", + data: { tenantFilter: settings.currentTenant }, + queryKey: `${settings.currentTenant}-ListuserCounts-report`, + waiting: previewOpen, + }); + // Only fetch additional data when preview dialog is opened const secureScore = useSecureScore({ waiting: previewOpen }); @@ -2606,7 +2622,9 @@ export const ExecutiveReportButton = (props) => { // Check if all data is loaded (either successful or failed) - only relevant when preview is open const isDataLoading = previewOpen && - (secureScore.isFetching || + (organization.isFetching || + dashboard.isFetching || + secureScore.isFetching || licenseData.isFetching || deviceData.isFetching || conditionalAccessData.isFetching || @@ -2615,7 +2633,9 @@ export const ExecutiveReportButton = (props) => { const hasAllDataFinished = !previewOpen || - ((secureScore.isSuccess || secureScore.isError) && + ((organization.isSuccess || organization.isError) && + (dashboard.isSuccess || dashboard.isError) && + (secureScore.isSuccess || secureScore.isError) && (licenseData.isSuccess || licenseData.isError) && (deviceData.isSuccess || deviceData.isError) && (conditionalAccessData.isSuccess || conditionalAccessData.isError) && @@ -2625,6 +2645,18 @@ export const ExecutiveReportButton = (props) => { // Button is always available now since we don't need to wait for data const shouldShowButton = true; + const tenantName = organization.data?.displayName || "Tenant"; + const tenantId = organization.data?.id; + const userStats = { + licensedUsers: dashboard.data?.LicUsers || 0, + unlicensedUsers: + dashboard.data?.Users && dashboard.data?.LicUsers + ? dashboard.data?.Users - dashboard.data?.LicUsers + : 0, + guests: dashboard.data?.Guests || 0, + globalAdmins: dashboard.data?.Gas || 0, + }; + const fileName = `Executive_Report_${tenantName?.replace(/[^a-zA-Z0-9]/g, "_") || "Tenant"}_${ new Date().toISOString().split("T")[0] }.pdf`; @@ -2655,8 +2687,8 @@ export const ExecutiveReportButton = (props) => { tenantName={tenantName} tenantId={tenantId} userStats={userStats} - standardsData={standardsData} - organizationData={organizationData} + standardsData={driftComplianceData.data} + organizationData={organization.data} brandingSettings={brandingSettings} secureScoreData={secureScore.isSuccess ? secureScore : null} licensingData={licenseData.isSuccess ? licenseData?.data : null} @@ -2687,8 +2719,8 @@ export const ExecutiveReportButton = (props) => { tenantName, tenantId, userStats, - standardsData, - organizationData, + organization.data, + dashboard.data, brandingSettings, secureScore?.isSuccess, licenseData?.isSuccess, @@ -3007,8 +3039,8 @@ export const ExecutiveReportButton = (props) => { tenantName={tenantName} tenantId={tenantId} userStats={userStats} - standardsData={standardsData} - organizationData={organizationData} + standardsData={driftComplianceData.data} + organizationData={organization.data} brandingSettings={brandingSettings} secureScoreData={secureScore.isSuccess ? secureScore : null} licensingData={licenseData.isSuccess ? licenseData?.data : null} diff --git a/src/data/Extensions.json b/src/data/Extensions.json index c6d8ced44146..21471f5e295c 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -274,7 +274,7 @@ "links": [ { "name": "HaloPSA Documentation", - "url": "https://halopsa.com/guides/" + "url": "https://usehalo.com/halopsa/guides/2697" } ], "SettingOptions": [ diff --git a/src/data/alerts.json b/src/data/alerts.json index 497436c7d6a3..dd6d8b6cc065 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -103,6 +103,11 @@ "label": "Alert on Security Defaults automatic enablement", "recommendedRunInterval": "1d" }, + { + "name": "SecDefaultsDisabled", + "label": "Alert when Security Defaults is disabled with no Conditional Access policies", + "recommendedRunInterval": "1d" + }, { "name": "DefenderStatus", "label": "Alert if Defender is not running (Tenant must be on-boarded in Lighthouse)", @@ -187,6 +192,41 @@ "label": "Alert on device compliance issues", "recommendedRunInterval": "4h" }, + { + "name": "IntunePolicyConflicts", + "label": "Alert on Intune policy or app conflicts/errors", + "recommendedRunInterval": "4h", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "switch", + "inputLabel": "Alert per issue (off = aggregated)", + "inputName": "AlertEachIssue" + }, + { + "inputType": "switch", + "inputLabel": "Include policy status issues", + "inputName": "IncludePolicies" + }, + { + "inputType": "switch", + "inputLabel": "Include app install issues", + "inputName": "IncludeApplications" + }, + { + "inputType": "switch", + "inputLabel": "Alert on conflicts", + "inputName": "AlertConflicts" + }, + { + "inputType": "switch", + "inputLabel": "Alert on errors/failures", + "inputName": "AlertErrors" + } + ], + "description": "Monitors Intune policy assignment states and app install statuses for conflicts or errors. Defaults to aggregated alerts with all mechanisms enabled and both conflicts and errors included." + }, { "name": "BreachAlert", "label": "Alert on (new) potentially breached passwords. Generates an alert if a password is found to be breached.", diff --git a/src/data/standards.json b/src/data/standards.json index 92d605cf2036..dce1f074d9e9 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -116,7 +116,13 @@ { "name": "standards.AuditLog", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"], + "tag": [ + "CIS M365 5.0 (3.1.1)", + "mip_search_auditlog", + "NIST CSF 2.0 (DE.CM-09)", + "CISAMSEXO171", + "CISAMSEXO173" + ], "helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.", "executiveText": "Activates comprehensive activity logging across Microsoft 365 services to track user actions, system changes, and security events. This provides essential audit trails for compliance requirements, security investigations, and regulatory reporting.", "addedComponent": [], @@ -319,7 +325,10 @@ "EIDSCA.AP14", "EIDSCA.ST08", "EIDSCA.ST09", - "NIST CSF 2.0 (PR.AA-05)" + "NIST CSF 2.0 (PR.AA-05)", + "EIDSCAAP07", + "EIDSCAST08", + "EIDSCAST09" ], "helpText": "Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory.", "docsDescription": "Sets it so guests can view only their own user profile. Permission to view other users isn't allowed. Also restricts guest users from seeing the membership of groups they're in. See exactly what get locked down in the [Microsoft documentation.](https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions)", @@ -335,7 +344,7 @@ { "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], + "tag": ["CIS M365 5.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)", "ZTNA21799", "CISAMSEXO51"], "helpText": "Disables SMTP AUTH organization-wide, impacting POP and IMAP clients that rely on SMTP for sending emails. Default for new tenants. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)", "docsDescription": "Disables tenant-wide SMTP basic authentication, including for all explicitly enabled users, impacting POP and IMAP clients that rely on SMTP for sending emails. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).", "executiveText": "Disables outdated email authentication methods that are vulnerable to security attacks, forcing applications and devices to use modern, more secure authentication protocols. This reduces the risk of email-based security breaches and credential theft.", @@ -350,7 +359,14 @@ { "name": "standards.ActivityBasedTimeout", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"], + "tag": [ + "CIS M365 5.0 (1.3.2)", + "spo_idle_session_timeout", + "NIST CSF 2.0 (PR.AA-03)", + "ZTNA21813", + "ZTNA21814", + "ZTNA21815" + ], "helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps", "executiveText": "Automatically logs out inactive users from Microsoft 365 applications after a specified time period to prevent unauthorized access to company data on unattended devices. This security measure protects against data breaches when employees leave workstations unlocked.", "addedComponent": [ @@ -394,7 +410,7 @@ { "name": "standards.AuthMethodsSettings", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03"], + "tag": ["EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", "EIDSCAAG02", "EIDSCAAG03"], "helpText": "Configures the report suspicious activity settings and system credential preferences in the authentication methods policy.", "docsDescription": "Controls the authentication methods policy settings for reporting suspicious activity and system credential preferences. These settings help enhance the security of authentication in your organization.", "executiveText": "Configures security settings that allow users to report suspicious login attempts and manages how the system handles authentication credentials. This enhances overall security by enabling early detection of potential security threats and optimizing authentication processes.", @@ -454,7 +470,7 @@ { "name": "standards.AuthMethodsPolicyMigration", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["EIDSCAAG01"], "helpText": "Completes the migration of authentication methods policy to the new format", "docsDescription": "Sets the authentication methods policy migration state to complete. This is required when migrating from legacy authentication policies to the new unified authentication methods policy.", "executiveText": "Completes the transition from legacy authentication policies to Microsoft's modern unified authentication methods policy, ensuring the organization benefits from the latest security features and management capabilities. This migration enables enhanced security controls and simplified policy management.", @@ -533,7 +549,7 @@ { "name": "standards.laps", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21953", "ZTNA21955", "ZTNA24560"], "helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.", "docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.", "executiveText": "Enables Local Administrator Password Solution (LAPS) capability, which automatically manages and rotates local administrator passwords on company computers. This significantly improves security by preventing the use of shared or static administrator passwords that could be exploited by attackers.", @@ -556,7 +572,14 @@ "EIDSCA.AM07", "EIDSCA.AM09", "EIDSCA.AM10", - "NIST CSF 2.0 (PR.AA-03)" + "NIST CSF 2.0 (PR.AA-03)", + "EIDSCAAM01", + "EIDSCAAM03", + "EIDSCAAM04", + "EIDSCAAM06", + "EIDSCAAM07", + "EIDSCAAM09", + "EIDSCAAM10" ], "helpText": "Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name.", "docsDescription": "Allows users to use Passwordless with Number Matching and adds location information from the last request", @@ -572,7 +595,7 @@ { "name": "standards.allowOTPTokens", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AM02"], + "tag": ["EIDSCA.AM02", "EIDSCAAM02"], "helpText": "Allows you to use MS authenticator OTP token generator", "docsDescription": "Allows you to use Microsoft Authenticator OTP token generator. Useful for using the NPS extension as MFA on VPN clients.", "executiveText": "Enables one-time password generation through Microsoft Authenticator app, providing an additional secure authentication method for employees. This is particularly useful for secure VPN access and other systems requiring multi-factor authentication.", @@ -631,7 +654,13 @@ "EIDSCA.AF04", "EIDSCA.AF05", "EIDSCA.AF06", - "NIST CSF 2.0 (PR.AA-03)" + "NIST CSF 2.0 (PR.AA-03)", + "EIDSCAAF01", + "EIDSCAAF02", + "EIDSCAAF03", + "EIDSCAAF04", + "EIDSCAAF05", + "EIDSCAAF06" ], "helpText": "Enables the FIDO2 authenticationMethod for the tenant", "docsDescription": "Enables FIDO2 capabilities for the tenant. This allows users to use FIDO2 keys like a Yubikey for authentication.", @@ -692,7 +721,7 @@ { "name": "standards.TAP", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21845", "ZTNA21846", "EIDSCAAT01", "EIDSCAAT02"], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", "docsDescription": "Enables Temporary Password generation for the tenant.", "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", @@ -740,7 +769,17 @@ { "name": "standards.CustomBannedPasswordList", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (5.2.3.2)"], + "tag": [ + "CIS M365 5.0 (5.2.3.2)", + "ZTNA21848", + "ZTNA21849", + "ZTNA21850", + "EIDSCAPR01", + "EIDSCAPR02", + "EIDSCAPR03", + "EIDSCAPR05", + "EIDSCAPR06" + ], "helpText": "**Requires Entra ID P1.** Updates and enables the Entra ID custom banned password list with the supplied words. Enter words separated by commas or semicolons. Each word must be 4-16 characters long. Maximum 1,000 words allowed.", "docsDescription": "Updates and enables the Entra ID custom banned password list with the supplied words. This supplements the global banned password list maintained by Microsoft. The custom list is limited to 1,000 key base terms of 4-16 characters each. Entra ID will [block variations and common substitutions](https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-configure-custom-password-protection#configure-custom-banned-passwords) of these words in user passwords. [How are passwords evaluated?](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad#score-calculation)", "addedComponent": [ @@ -762,7 +801,7 @@ { "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21803", "ZTNA21804"], "helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.", "executiveText": "Allows external partners and vendors to use their own organization's multi-factor authentication when accessing company resources, streamlining collaboration while maintaining security standards. This reduces friction for external users while ensuring they still meet authentication requirements.", "addedComponent": [ @@ -794,7 +833,7 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (1.2.3)", "CISA (MS.AAD.6.1v1)"], + "tag": ["CIS M365 5.0 (1.2.3)", "CISA (MS.AAD.6.1v1)", "ZTNA21772", "ZTNA21787"], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", "executiveText": "Prevents regular employees from creating new Microsoft 365 organizations, ensuring all new tenants are properly managed and controlled by IT administrators. This prevents unauthorized shadow IT environments and maintains centralized governance over Microsoft 365 resources.", @@ -818,7 +857,12 @@ "EIDSCA.CR03", "EIDSCA.CR04", "Essential 8 (1507)", - "NIST CSF 2.0 (PR.AA-05)" + "NIST CSF 2.0 (PR.AA-05)", + "ZTNA21869", + "EIDSCACR01", + "EIDSCACR02", + "EIDSCACR03", + "EIDSCACR04" ], "helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings", "docsDescription": "Enables the ability for users to request admin consent for applications. Should be used in conjunction with the \"Require admin consent for applications\" standards", @@ -840,7 +884,7 @@ { "name": "standards.NudgeMFA", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21889"], "helpText": "Sets the state of the registration campaign for the tenant", "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.", "executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.", @@ -883,7 +927,7 @@ { "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.21.1v1)"], + "tag": ["CISA (MS.AAD.21.1v1)", "ZTNA21868"], "helpText": "Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "docsDescription": "Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "executiveText": "Restricts the creation of Microsoft 365 groups, Teams, and SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces. This ensures proper governance, naming conventions, and resource management while maintaining oversight of all collaborative environments.", @@ -903,7 +947,8 @@ "CISA (MS.AAD.4.1v1)", "EIDSCA.AP10", "Essential 8 (1175)", - "NIST CSF 2.0 (PR.AA-05)" + "NIST CSF 2.0 (PR.AA-05)", + "EIDSCAAP10" ], "helpText": "Disables the ability for users to create App registrations in the tenant.", "docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.", @@ -919,7 +964,7 @@ { "name": "standards.BitLockerKeysForOwnedDevice", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21954"], "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.", "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.", "executiveText": "Gives administrators centralized control over BitLocker recovery secrets—restrict access to ensure IT-assisted recovery flows, or allow self-service when rapid device unlocks are a priority.", @@ -952,7 +997,7 @@ { "name": "standards.DisableSecurityGroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)"], + "tag": ["CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA21868"], "helpText": "Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams", "executiveText": "Restricts the creation of security groups to IT administrators only, preventing employees from creating unauthorized access groups that could bypass security controls. This ensures proper governance of access permissions and maintains centralized control over who can access what resources.", "addedComponent": [], @@ -1001,7 +1046,7 @@ { "name": "standards.DisableGuests", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21858"], "helpText": "Blocks login for guest users that have not logged in for a number of days", "executiveText": "Automatically disables external guest accounts that haven't been used for a number of days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.", "addedComponent": [ @@ -1029,7 +1074,15 @@ "EIDSCA.AP08", "EIDSCA.AP09", "Essential 8 (1175)", - "NIST CSF 2.0 (PR.AA-05)" + "NIST CSF 2.0 (PR.AA-05)", + "ZTNA21772", + "ZTNA21774", + "ZTNA21807", + "EIDSCAAP08", + "EIDSCAAP09", + "EIDSCACP01", + "EIDSCACP03", + "EIDSCACP04" ], "helpText": "Disables users from being able to consent to applications, except for those specified in the field below", "docsDescription": "Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications.", @@ -1066,7 +1119,7 @@ { "name": "standards.GuestInvite", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07"], + "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "EIDSCAAP04"], "helpText": "This setting controls who can invite guests to your directory to collaborate on resources secured by your company, such as SharePoint sites or Azure resources.", "executiveText": "Controls who within the organization can invite external partners and vendors to access company resources, ensuring proper oversight of external access while enabling necessary business collaboration. This helps maintain security while supporting partnership and vendor relationships.", "addedComponent": [ @@ -1150,7 +1203,7 @@ { "name": "standards.SecurityDefaults", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.11.1v1)"], + "tag": ["CISA (MS.AAD.11.1v1)", "ZTNA21843"], "helpText": "Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access.", "docsDescription": "Enables SD for the tenant, which disables all forms of basic authentication and enforces users to configure MFA. Users are only prompted for MFA when a logon is considered 'suspect' by Microsoft.", "executiveText": "Activates Microsoft's baseline security configuration that requires multi-factor authentication and blocks legacy authentication methods. This provides essential security protection for organizations without complex conditional access policies, significantly improving security posture with minimal configuration.", @@ -1165,7 +1218,7 @@ { "name": "standards.DisableSMS", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)"], + "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAS04"], "helpText": "This blocks users from using SMS as an MFA method. If a user only has SMS as a MFA method, they will be unable to log in.", "docsDescription": "Disables SMS as an MFA method for the tenant. If a user only has SMS as a MFA method, they will be unable to sign in.", "executiveText": "Disables SMS text messages as a multi-factor authentication method due to security vulnerabilities like SIM swapping attacks. This forces users to adopt more secure authentication methods like authenticator apps or hardware tokens, significantly improving account security.", @@ -1180,7 +1233,7 @@ { "name": "standards.DisableVoice", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)"], + "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAV01"], "helpText": "This blocks users from using Voice call as an MFA method. If a user only has Voice as a MFA method, they will be unable to log in.", "docsDescription": "Disables Voice call as an MFA method for the tenant. If a user only has Voice call as a MFA method, they will be unable to sign in.", "executiveText": "Disables voice call authentication due to security vulnerabilities and social engineering risks. This forces users to adopt more secure authentication methods like authenticator apps, improving overall account security by eliminating phone-based attack vectors.", @@ -1248,7 +1301,10 @@ "Essential 8 (1504)", "Essential 8 (1173)", "Essential 8 (1401)", - "NIST CSF 2.0 (PR.AA-03)" + "NIST CSF 2.0 (PR.AA-03)", + "ZTNA21780", + "ZTNA21782", + "ZTNA21796" ], "helpText": "Enables per user MFA for all users.", "executiveText": "Requires all employees to use multi-factor authentication for enhanced account security, significantly reducing the risk of unauthorized access from compromised passwords. This fundamental security measure protects against the majority of account-based attacks and is essential for maintaining strong cybersecurity posture.", @@ -1521,7 +1577,7 @@ { "name": "standards.SpoofWarn", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.2.3)"], + "tag": ["CIS M365 5.0 (6.2.3)", "ORCA111", "ORCA240", "CISAMSEXO71"], "helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA", "docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)", "executiveText": "Displays visual warnings in Outlook when emails come from external senders, helping employees identify potentially suspicious messages and reducing the risk of phishing attacks. This security feature makes it easier for staff to distinguish between internal and external communications.", @@ -1644,7 +1700,7 @@ { "name": "standards.AddDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (2.1.9)"], + "tag": ["CIS M365 5.0 (2.1.9)", "ORCA108", "CISAMSEXO31"], "helpText": "Enables DKIM for all domains that currently support it", "executiveText": "Enables email authentication technology that digitally signs outgoing emails to verify they actually came from your organization. This prevents email spoofing, improves email deliverability, and protects the company's reputation by ensuring recipients can trust emails from your domains.", "addedComponent": [], @@ -1696,7 +1752,8 @@ "exo_mailboxaudit", "Essential 8 (1509)", "Essential 8 (1683)", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "CISAMSEXO131" ], "helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.", @@ -1913,7 +1970,7 @@ { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (1.3.3)", "exo_individualsharing"], + "tag": ["CIS M365 5.0 (1.3.3)", "exo_individualsharing", "ZTNA21803", "CISAMSEXO62"], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", "executiveText": "Prevents employees from sharing their calendars with external parties, protecting sensitive meeting information and internal schedules from unauthorized access. This security measure helps maintain confidentiality of business activities while still allowing internal collaboration.", @@ -1948,7 +2005,7 @@ { "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.5.3)", "exo_storageproviderrestricted"], + "tag": ["CIS M365 5.0 (6.5.3)", "exo_storageproviderrestricted", "ZTNA21817"], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", "executiveText": "Prevents employees from accessing personal cloud storage services like Dropbox or Google Drive through Outlook on the web, reducing data security risks and ensuring company information stays within approved corporate systems. This helps maintain data governance and prevents accidental data leaks.", @@ -2108,7 +2165,8 @@ "CIS M365 5.0 (6.3.1)", "exo_outlookaddins", "NIST CSF 2.0 (PR.AA-05)", - "NIST CSF 2.0 (PR.PS-05)" + "NIST CSF 2.0 (PR.PS-05)", + "ZTNA21817" ], "helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.", "docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.", @@ -2381,7 +2439,24 @@ "CIS M365 5.0 (2.1.1)", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "ORCA105", + "ORCA106", + "ORCA107", + "ORCA112", + "ORCA113", + "ORCA114", + "ORCA116", + "ORCA119", + "ORCA156", + "ORCA179", + "ORCA226", + "ORCA236", + "ORCA237", + "ORCA238", + "CISAMSEXO151", + "CISAMSEXO152", + "CISAMSEXO153" ], "helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", "addedComponent": [ @@ -2435,7 +2510,30 @@ "mdo_antiphishingpolicies", "mdo_phishthresholdlevel", "CIS M365 5.0 (2.1.7)", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "ORCA104", + "ORCA115", + "ORCA180", + "ORCA220", + "ORCA221", + "ORCA222", + "ORCA223", + "ORCA228", + "ORCA229", + "ORCA230", + "ORCA233", + "ORCA234", + "ORCA235", + "ORCA239", + "ORCA242", + "ORCA243", + "ORCA244", + "ZTNA21784", + "ZTNA21817", + "ZTNA21819", + "CISAMSEXO111", + "CISAMSEXO112", + "CISAMSEXO113" ], "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mail tips.", "addedComponent": [ @@ -2656,7 +2754,9 @@ "mdo_safedocuments", "mdo_commonattachmentsfilter", "mdo_safeattachmentpolicy", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "ORCA158", + "ORCA227" ], "helpText": "This creates a Safe Attachment policy", "addedComponent": [ @@ -2807,7 +2907,16 @@ "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "ORCA121", + "ORCA124", + "ORCA232", + "ZTNA21817", + "ZTNA21819", + "CISAMSEXO95", + "CISAMSEXO101", + "CISAMSEXO102", + "CISAMSEXO103" ], "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", "addedComponent": [ @@ -2935,7 +3044,25 @@ { "name": "standards.SpamFilterPolicy", "cat": "Defender Standards", - "tag": [], + "tag": [ + "ORCA100", + "ORCA101", + "ORCA102", + "ORCA103", + "ORCA104", + "ORCA123", + "ORCA139", + "ORCA140", + "ORCA141", + "ORCA142", + "ORCA143", + "ORCA224", + "ORCA231", + "ORCA241", + "CISAMSEXO141", + "CISAMSEXO142", + "CISAMSEXO143" + ], "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": [ @@ -3769,7 +3896,7 @@ { "name": "standards.intuneDeviceReg", "cat": "Intune Standards", - "tag": ["CISA (MS.AAD.17.1v1)"], + "tag": ["CISA (MS.AAD.17.1v1)", "ZTNA21801", "ZTNA21802"], "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users", "executiveText": "Limits how many devices each employee can register for corporate access, preventing excessive device proliferation while accommodating legitimate business needs. This helps maintain security oversight and prevents potential abuse of device registration privileges.", "addedComponent": [ @@ -3790,7 +3917,7 @@ { "name": "standards.intuneRequireMFA", "cat": "Intune Standards", - "tag": [], + "tag": ["ZTNA21782", "ZTNA21796", "ZTNA21872"], "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.", "executiveText": "Requires employees to use multi-factor authentication when registering devices for corporate access, adding an extra security layer to prevent unauthorized device enrollment. This helps ensure only legitimate users can connect their devices to company systems.", "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra", @@ -3940,7 +4067,7 @@ { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"], + "tag": ["CIS M365 5.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)", "ZTNA21817"], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.", "addedComponent": [], @@ -3982,7 +4109,7 @@ { "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.9)", "CISA (MS.SPO.1.5v1)"], + "tag": ["CIS M365 5.0 (7.2.9)", "CISA (MS.SPO.1.5v1)", "ZTNA21803", "ZTNA21804", "ZTNA21858"], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.", "addedComponent": [ @@ -4007,7 +4134,7 @@ { "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.10)", "CISA (MS.SPO.1.6v1)"], + "tag": ["CIS M365 5.0 (7.2.10)", "CISA (MS.SPO.1.6v1)", "ZTNA21803", "ZTNA21804"], "helpText": "Ensure re-authentication with verification code is restricted", "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.", "addedComponent": [ @@ -4032,7 +4159,13 @@ { "name": "standards.DefaultSharingLink", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.7)", "CIS M365 5.0 (7.2.11)", "CISA (MS.SPO.1.4v1)"], + "tag": [ + "CIS M365 5.0 (7.2.7)", + "CIS M365 5.0 (7.2.11)", + "CISA (MS.SPO.1.4v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Configure the SharePoint default sharing link type and permission. This setting controls both the type of sharing link created by default and the permission level assigned to those links.", "docsDescription": "Sets the default sharing link type (Direct or Internal) and permission (View) in SharePoint and OneDrive. Direct sharing means links only work for specific people, while Internal sharing means links work for anyone in the organization. Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link, reducing the risk of unintentionally granting edit privileges.", "executiveText": "Configures SharePoint default sharing links to implement the principle of least privilege for document sharing. This security measure reduces the risk of accidental data modification while maintaining collaboration functionality, requiring users to explicitly select Edit permissions when necessary. The sharing type setting controls whether links are restricted to specific recipients or available to the entire organization. This reduces the risk of accidental data exposure through link sharing.", @@ -4135,7 +4268,9 @@ "CIS M365 5.0 (7.2.1)", "spo_legacy_auth", "CISA (MS.AAD.3.1v1)", - "NIST CSF 2.0 (PR.IR-01)" + "NIST CSF 2.0 (PR.IR-01)", + "ZTNA21776", + "ZTNA21797" ], "helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.", "docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.", @@ -4151,7 +4286,13 @@ { "name": "standards.sharingCapability", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.AAD.14.1v1)", "CISA (MS.SPO.1.1v1)"], + "tag": [ + "CIS M365 5.0 (7.2.3)", + "CISA (MS.AAD.14.1v1)", + "CISA (MS.SPO.1.1v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Defines the organization's default policy for sharing files and folders in SharePoint and OneDrive, balancing collaboration needs with security requirements. This fundamental setting determines whether employees can share with external users, anonymous links, or only internal colleagues.", "addedComponent": [ @@ -4190,7 +4331,13 @@ { "name": "standards.DisableReshare", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"], + "tag": [ + "CIS M365 5.0 (7.2.5)", + "CISA (MS.AAD.14.2v1)", + "CISA (MS.SPO.1.2v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Prevents external users from sharing company documents with additional people, maintaining control over document distribution and preventing unauthorized access expansion. This security measure ensures that external sharing remains within intended boundaries set by internal employees.", @@ -4254,7 +4401,7 @@ { "name": "standards.unmanagedSync", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"], + "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA24824"], "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.", "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)", "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.", @@ -4288,7 +4435,13 @@ { "name": "standards.sharingDomainRestriction", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"], + "tag": [ + "CIS M365 5.0 (7.2.6)", + "CISA (MS.AAD.14.3v1)", + "CISA (MS.SPO.1.3v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.", "executiveText": "Controls which external domains employees can share files with, enabling secure collaboration with trusted partners while blocking sharing with unauthorized organizations. This targeted approach maintains necessary business relationships while preventing data exposure to unknown entities.", "addedComponent": [ @@ -5149,6 +5302,12 @@ "valueField": "GUID", "queryKey": "ListTransportRulesTemplates" } + }, + { + "type": "switch", + "label": "Overwrite existing transport rules", + "name": "overwrite", + "defaultValue": true } ] }, @@ -5375,6 +5534,20 @@ "powershellEquivalent": "Set-OwaMailboxPolicy -Identity \"OwaMailboxPolicy-Default\" -ConditionalAccessPolicy ReadOnlyPlusAttachmentsBlocked", "recommendedBy": ["Microsoft Zero Trust", "CIPP"] }, + { + "name": "standards.LegacyEmailReportAddins", + "deprecated": true, + "cat": "Exchange Standards", + "tag": [], + "helpText": "Removes legacy Report Phishing and Report Message Outlook add-ins.", + "executiveText": "The legacy Report Phishing and Report Message Outlook add-ins are security issues with the add-in which makes them unsafe for the organization.", + "label": "Remove legacy Outlook Report add-ins", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-08-26", + "powershellEquivalent": "None", + "recommendedBy": ["Microsoft"] + }, { "name": "standards.DeployCheckChromeExtension", "cat": "Intune Standards", @@ -5401,13 +5574,6 @@ "label": "Enable CIPP reporting", "defaultValue": true }, - { - "type": "textField", - "name": "standards.DeployCheckChromeExtension.cippServerUrl", - "label": "CIPP Server URL", - "placeholder": "https://YOUR-CIPP-SERVER-URL", - "required": false - }, { "type": "textField", "name": "standards.DeployCheckChromeExtension.customRulesUrl", diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index f92712a89127..b268d6680cd5 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -1,13 +1,19 @@ import { usePathname, useRouter } from "next/navigation"; import { Box, Divider, Stack, Tab, Tabs } from "@mui/material"; +import { useSearchParams } from "next/navigation"; export const TabbedLayout = (props) => { const { tabOptions, children } = props; const router = useRouter(); const pathname = usePathname(); + const searchParams = useSearchParams(); const handleTabsChange = (event, value) => { - router.push(value); + // Preserve existing query parameters when changing tabs + const currentParams = new URLSearchParams(searchParams.toString()); + const queryString = currentParams.toString(); + const newPath = queryString ? `${value}?${queryString}` : value; + router.push(newPath); }; const currentTab = tabOptions.find((option) => option.path === pathname); diff --git a/src/layouts/config.js b/src/layouts/config.js index 8923b99832d1..d735b015c94c 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -73,6 +73,11 @@ export const nativeMenuItems = [ path: "/identity/administration/jit-admin", permissions: ["Identity.Role.*"], }, + { + title: "JIT Admin Templates", + path: "/identity/administration/jit-admin-templates", + permissions: ["Identity.Role.*"], + }, { title: "Offboarding Wizard", path: "/identity/administration/offboarding-wizard", @@ -711,6 +716,11 @@ export const nativeMenuItems = [ path: "/email/reports/mailbox-cas-settings", permissions: ["Exchange.Mailbox.*"], }, + { + title: "Mailbox Permissions", + path: "/email/reports/mailbox-permissions", + permissions: ["Exchange.Mailbox.Read"], + }, { title: "Anti-Phishing Filters", path: "/email/reports/antiphishing-filters", diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js index 7ecb9dc3c440..c9ef006bea2d 100644 --- a/src/layouts/side-nav.js +++ b/src/layouts/side-nav.js @@ -142,6 +142,12 @@ export const SideNav = (props) => { imagesrc: theme === "light" ? "/sponsors/RoB-light.png" : "/sponsors/RoB.png", priority: 1, }, + { + link: "https://www.relentlesssolutions.com/", + imagesrc: + theme === "light" ? "/sponsors/relentless-light.png" : "/sponsors/relentless-dark.png", + priority: 1, + }, ]; const randomSponsorImage = () => { diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index eaf46486abe6..2596035f733b 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -46,7 +46,9 @@ const Page = () => { ]; const offCanvas = { - children: (extendedData) => , + children: (extendedData) => ( + + ), size: "xl", actions: actions, }; diff --git a/src/pages/cipp/settings/index.js b/src/pages/cipp/settings/index.js index e6e5447ba12f..487e9a1cc719 100644 --- a/src/pages/cipp/settings/index.js +++ b/src/pages/cipp/settings/index.js @@ -11,6 +11,7 @@ import CippCacheSettings from "/src/components/CippSettings/CippCacheSettings"; import CippBackupSettings from "/src/components/CippSettings/CippBackupSettings"; import CippBrandingSettings from "/src/components/CippSettings/CippBrandingSettings"; import CippBackupRetentionSettings from "/src/components/CippSettings/CippBackupRetentionSettings"; +import CippJitAdminSettings from "/src/components/CippSettings/CippJitAdminSettings"; const Page = () => { return ( @@ -36,6 +37,9 @@ const Page = () => { + + + ); diff --git a/src/pages/cipp/super-admin/jit-admin-settings.js b/src/pages/cipp/super-admin/jit-admin-settings.js new file mode 100644 index 000000000000..ff7228e017fd --- /dev/null +++ b/src/pages/cipp/super-admin/jit-admin-settings.js @@ -0,0 +1,146 @@ +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import tabOptions from "./tabOptions"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { useForm } from "react-hook-form"; +import { Typography, Alert } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { ApiGetCall } from "../../../api/ApiCall"; +import { useEffect } from "react"; + +const Page = () => { + const pageTitle = "JIT Admin Settings"; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + MaxDuration: "", + }, + }); + + const jitSettings = ApiGetCall({ + url: "/api/ExecJITAdminSettings?Action=Get", + queryKey: "jitAdminSettings", + }); + + useEffect(() => { + if (jitSettings.isSuccess && jitSettings.data) { + formControl.reset({ + MaxDuration: jitSettings.data?.MaxDuration || [], + }); + } + }, [jitSettings.isSuccess, jitSettings.data]); + + return ( + ({ + Action: "Set", + MaxDuration: values.MaxDuration || null, + })} + > + + + + Configure maximum allowed duration for Just-In-Time (JIT) admin accounts. This setting + helps enforce security policies by preventing technicians from creating JIT admin + accounts with excessively long lifespans. Validation is performed on the backend when + creating JIT admin accounts. + + + + + + Set the maximum duration in ISO 8601 format, or leave empty for no limit (default). The + backend will validate that the difference between the start and end dates of any JIT + admin account does not exceed the configured maximum duration. + + + + + { + // Allow empty value (no limit) + if (typeof value !== "string" || value.trim() === "") { + return true; + } + const iso8601Regex = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; + if (!iso8601Regex.test(value)) { + return "Invalid format. Use PT1H, P1D, P7D, P28D, etc."; + } + return true; + }, + }, + }} + formControl={formControl} + helperText="ISO 8601 format: PT1H (1 hour), P1D (1 day), P7D (1 week), P28D (4 weeks). Leave empty for no limit." + /> + + + + + + Important Notes: + + +
    +
  • Leave empty for no limit on JIT admin account duration (default behavior)
  • +
  • + The duration is calculated from the start date to the expiration date of the JIT + admin +
  • +
  • + If a technician attempts to exceed this limit, the backend will reject the + request with an error message +
  • +
  • This setting applies globally to all tenants and all JIT admin creations
  • +
+
+
+
+ + + + Example: If maximum duration is set to P28D (4 weeks), and a + technician tries to create a JIT admin account lasting 1.5 months, the backend will + reject the request with an error: "Requested JIT Admin duration (56 days) exceeds the + maximum allowed duration of P28D (28 days)". + + +
+
+ ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/dashboardv1.js b/src/pages/dashboardv1.js new file mode 100644 index 000000000000..a2054d962e5e --- /dev/null +++ b/src/pages/dashboardv1.js @@ -0,0 +1,457 @@ +import Head from "next/head"; +import { useEffect, useState } from "react"; +import { Box, Container, Button, Card, CardContent } from "@mui/material"; +import { Grid } from "@mui/system"; +import { CippInfoBar } from "../components/CippCards/CippInfoBar"; +import { CippChartCard } from "../components/CippCards/CippChartCard"; +import { CippPropertyListCard } from "../components/CippCards/CippPropertyListCard"; +import { Layout as DashboardLayout } from "../layouts/index.js"; +import { useSettings } from "../hooks/use-settings"; +import { getCippFormatting } from "../utils/get-cipp-formatting.js"; +import Portals from "../data/portals"; +import { BulkActionsMenu } from "../components/bulk-actions-menu.js"; +import { CippUniversalSearch } from "../components/CippCards/CippUniversalSearch.jsx"; +import { ApiGetCall } from "../api/ApiCall.jsx"; +import { CippCopyToClipBoard } from "../components/CippComponents/CippCopyToClipboard.jsx"; +import { ExecutiveReportButton } from "../components/ExecutiveReportButton.js"; + +const Page = () => { + const settings = useSettings(); + const { currentTenant } = settings; + const [domainVisible, setDomainVisible] = useState(false); + + const organization = ApiGetCall({ + url: "/api/ListOrg", + queryKey: `${currentTenant}-ListOrg`, + data: { tenantFilter: currentTenant }, + }); + + const dashboard = ApiGetCall({ + url: "/api/ListuserCounts", + data: { tenantFilter: currentTenant }, + queryKey: `${currentTenant}-ListuserCounts`, + }); + + const sharepoint = ApiGetCall({ + url: "/api/ListSharepointQuota", + queryKey: `${currentTenant}-ListSharepointQuota`, + data: { tenantFilter: currentTenant }, + }); + + const standards = ApiGetCall({ + url: "/api/ListStandardTemplates", + queryKey: `${currentTenant}-ListStandardTemplates`, + }); + + const driftApi = ApiGetCall({ + url: "/api/listTenantDrift", + data: { + TenantFilter: currentTenant, + }, + queryKey: `TenantDrift-${currentTenant}`, + }); + + const partners = ApiGetCall({ + url: "/api/ListGraphRequest", + queryKey: `${currentTenant}-ListPartners`, + data: { + Endpoint: "policies/crossTenantAccessPolicy/partners", + tenantFilter: currentTenant, + ReverseTenantLookup: true, + }, + }); + + const currentTenantInfo = ApiGetCall({ + url: "/api/ListTenants", + queryKey: `ListTenants`, + }); + + // Top bar data + const tenantInfo = [ + { name: "Tenant Name", data: organization.data?.displayName }, + { + name: "Tenant ID", + data: ( + <> + + + ), + }, + { + name: "Default Domain", + data: ( + <> + domain.isDefault === true)?.name + } + type="chip" + /> + + ), + }, + { + name: "AD Sync Enabled", + data: getCippFormatting(organization.data?.onPremisesSyncEnabled, "dirsync"), + }, + ]; + + // Process drift data for chart - filter by current tenant and aggregate + const processDriftDataForTenant = (driftData, currentTenant) => { + if (!driftData) { + return { + alignedCount: 0, + acceptedDeviationsCount: 0, + currentDeviationsCount: 0, + customerSpecificDeviations: 0, + hasData: false, + }; + } + + const rawDriftData = driftData || []; + const tenantDriftData = Array.isArray(rawDriftData) + ? rawDriftData.filter((item) => item.tenantFilter === currentTenant) + : []; + + const hasData = tenantDriftData.length > 0; + + // Aggregate data across all standards for this tenant + const aggregatedData = tenantDriftData.reduce( + (acc, item) => { + acc.acceptedDeviationsCount += item.acceptedDeviationsCount || 0; + acc.currentDeviationsCount += item.currentDeviationsCount || 0; + acc.alignedCount += item.alignedCount || 0; + acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; + return acc; + }, + { + acceptedDeviationsCount: 0, + currentDeviationsCount: 0, + alignedCount: 0, + customerSpecificDeviations: 0, + } + ); + + return { ...aggregatedData, hasData }; + }; + + function getActionCountsForTenant(standardsData, currentTenant) { + if (!standardsData) { + return { + remediateCount: 0, + alertCount: 0, + reportCount: 0, + total: 0, + }; + } + + const applicableTemplates = standardsData.filter((template) => { + const tenantFilterArr = Array.isArray(template?.tenantFilter) ? template.tenantFilter : []; + const excludedTenantsArr = Array.isArray(template?.excludedTenants) + ? template.excludedTenants + : []; + + const tenantInFilter = + tenantFilterArr.length > 0 && tenantFilterArr.some((tf) => tf.value === currentTenant); + + const allTenantsTemplate = + tenantFilterArr.some((tf) => tf.value === "AllTenants") && + (excludedTenantsArr.length === 0 || + !excludedTenantsArr.some((et) => et.value === currentTenant)); + + return tenantInFilter || allTenantsTemplate; + }); + + // Combine standards from all applicable templates: + let combinedStandards = {}; + for (const template of applicableTemplates) { + for (const [standardKey, standardValue] of Object.entries(template.standards)) { + combinedStandards[standardKey] = standardValue; + } + } + + // Count each action type: + let remediateCount = 0; + let alertCount = 0; + let reportCount = 0; + + for (const [, standard] of Object.entries(combinedStandards)) { + let actions = standard.action || []; + if (!Array.isArray(actions)) { + actions = [actions]; + } + actions.forEach((actionObj) => { + if (actionObj?.value === "Remediate") { + remediateCount++; + } else if (actionObj?.value === "Alert") { + alertCount++; + } else if (actionObj?.value === "Report") { + reportCount++; + } + }); + } + + const total = Object.keys(combinedStandards).length; + + return { remediateCount, alertCount, reportCount, total }; + } + + const driftData = processDriftDataForTenant(driftApi.data, currentTenant); + const { remediateCount, alertCount, reportCount, total } = getActionCountsForTenant( + standards.data, + currentTenant + ); + + const [PortalMenuItems, setPortalMenuItems] = useState([]); + const [partnersVisible, setPartnersVisible] = useState(false); + + const formatStorageSize = (sizeInMB) => { + if (sizeInMB >= 1024) { + return `${(sizeInMB / 1024).toFixed(2)}GB`; + } + return `${sizeInMB}MB`; + }; + + // Function to filter portals based on user preferences + const getFilteredPortals = () => { + const defaultLinks = { + M365_Portal: true, + Exchange_Portal: true, + Entra_Portal: true, + Teams_Portal: true, + Azure_Portal: true, + Intune_Portal: true, + SharePoint_Admin: true, + Security_Portal: true, + Compliance_Portal: true, + Power_Platform_Portal: true, + Power_BI_Portal: true, + }; + + let portalLinks; + if (settings.UserSpecificSettings?.portalLinks) { + portalLinks = { ...defaultLinks, ...settings.UserSpecificSettings.portalLinks }; + } else if (settings.portalLinks) { + portalLinks = { ...defaultLinks, ...settings.portalLinks }; + } else { + portalLinks = defaultLinks; + } + + // Filter the portals based on user settings + return Portals.filter((portal) => { + const settingKey = portal.name; + return settingKey ? portalLinks[settingKey] === true : true; + }); + }; + + useEffect(() => { + if (currentTenantInfo.isSuccess) { + const tenantLookup = currentTenantInfo.data?.find( + (tenant) => tenant.defaultDomainName === currentTenant + ); + + // Get filtered portals based on user preferences + const filteredPortals = getFilteredPortals(); + + const menuItems = filteredPortals.map((portal) => ({ + label: portal.label, + target: "_blank", + link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), + icon: portal.icon, + })); + setPortalMenuItems(menuItems); + } + }, [ + currentTenantInfo.isSuccess, + currentTenant, + settings.portalLinks, + settings.UserSpecificSettings, + ]); + + return ( + <> + + Dashboard + + + + + + + + + + + {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} + + + + + + + + + + + + + + + + + + + + + {/* Converted Domain Names to Property List */} + + ({ + label: "", + value: domain.name, + }))} + actionButton={ + organization.data?.verifiedDomains?.length > 3 && ( + + ) + } + /> + + + + ({ + label: partner.TenantInfo?.displayName, + value: partner.TenantInfo?.defaultDomainName, + }))} + actionButton={ + partners.data?.Results?.length > 3 && ( + + ) + } + /> + + + + + plan.capabilityStatus === "Enabled" && + ["exchange", "AADPremiumService", "WindowsDefenderATP"].includes( + plan.service + ) + ) + .reduce((uniqueServices, curr) => { + const serviceLabel = + curr.service === "exchange" + ? "Exchange" + : curr.service === "AADPremiumService" + ? "AAD Premium" + : curr.service === "Windows Defender" + ? "Windows Defender" + : curr.service; + + if (!uniqueServices.includes(serviceLabel)) { + uniqueServices.push(serviceLabel); + } + return uniqueServices; + }, []) + .join(", "), + }, + ]} + /> + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/dashboardv2/devices/index.js b/src/pages/dashboardv2/devices/index.js index a4c96d300f0d..09738bb8b994 100644 --- a/src/pages/dashboardv2/devices/index.js +++ b/src/pages/dashboardv2/devices/index.js @@ -1,28 +1,137 @@ -import { Container, Typography, Card, CardContent, CardHeader, Box } from "@mui/material"; +import React from "react"; +import { + Container, + Typography, + Card, + CardContent, + CardHeader, + Box, + Stack, + Chip, +} from "@mui/material"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; +import { useSettings } from "/src/hooks/use-settings"; +import { ApiGetCall } from "/src/api/ApiCall.jsx"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { CippTestDetailOffCanvas } from "/src/components/CippTestDetail/CippTestDetailOffCanvas"; +import { useRouter } from "next/router"; const Page = () => { + const settings = useSettings(); + const { currentTenant } = settings; + const router = useRouter(); + // Only use default if router is ready and reportId is still not present + const selectedReport = + router.isReady && !router.query.reportId ? "ztna" : router.query.reportId || "ztna"; + + const testsApi = ApiGetCall({ + url: "/api/ListTests", + data: { tenantFilter: currentTenant, reportId: selectedReport }, + queryKey: `${currentTenant}-ListTests-${selectedReport}`, + waiting: !!currentTenant && !!selectedReport, + }); + + const DevicesTests = + testsApi.data?.TestResults?.filter((test) => test.TestType === "Devices") || []; + + const getStatusColor = (status) => { + switch (status?.toLowerCase()) { + case "passed": + return "success"; + case "failed": + return "error"; + case "investigate": + return "warning"; + case "skipped": + return "default"; + default: + return "default"; + } + }; + + const getRiskColor = (risk) => { + switch (risk?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } + }; + + const getImpactColor = (impact) => { + switch (impact?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } + }; + + const offCanvas = { + size: "lg", + children: (row) => , + }; + + const filters = [ + { + filterName: "Passed", + value: [{ id: "Status", value: "Passed" }], + type: "column", + }, + { + filterName: "Failed", + value: [{ id: "Status", value: "Failed" }], + type: "column", + }, + { + filterName: "Investigate", + value: [{ id: "Status", value: "Investigate" }], + type: "column", + }, + { + filterName: "Skipped", + value: [{ id: "Status", value: "Skipped" }], + type: "column", + }, + { + filterName: "High Risk", + value: [{ id: "Risk", value: "High" }], + type: "column", + }, + { + filterName: "Medium Risk", + value: [{ id: "Risk", value: "Medium" }], + type: "column", + }, + { + filterName: "Low Risk", + value: [{ id: "Risk", value: "Low" }], + type: "column", + }, + ]; + return ( - - - - - - Device Test Results - - - This tab will display detailed device test results and recommendations. - - - Review device compliance policies, enrollment restrictions, and management - configurations to enhance your device security posture. - - - - + ); }; diff --git a/src/pages/dashboardv2/identity/index.js b/src/pages/dashboardv2/identity/index.js index 78b76b13e131..4f6b66856f9a 100644 --- a/src/pages/dashboardv2/identity/index.js +++ b/src/pages/dashboardv2/identity/index.js @@ -1,28 +1,86 @@ -import { Container, Typography, Card, CardContent, CardHeader, Box } from "@mui/material"; +import { Container } from "@mui/material"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; +import { useSettings } from "/src/hooks/use-settings"; +import { ApiGetCall } from "/src/api/ApiCall.jsx"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { CippTestDetailOffCanvas } from "/src/components/CippTestDetail/CippTestDetailOffCanvas"; +import { useRouter } from "next/router"; const Page = () => { + const settings = useSettings(); + const { currentTenant } = settings; + const router = useRouter(); + // Only use default if router is ready and reportId is still not present + const selectedReport = + router.isReady && !router.query.reportId ? "ztna" : router.query.reportId || "ztna"; + + const testsApi = ApiGetCall({ + url: "/api/ListTests", + data: { tenantFilter: currentTenant, reportId: selectedReport }, + queryKey: `${currentTenant}-ListTests-${selectedReport}`, + waiting: !!currentTenant && !!selectedReport, + }); + + const identityTests = + testsApi.data?.TestResults?.filter((test) => test.TestType === "Identity") || []; + + const offCanvas = { + size: "lg", + children: (row) => , + }; + + const filters = [ + { + filterName: "Passed", + value: [{ id: "Status", value: "Passed" }], + type: "column", + }, + { + filterName: "Failed", + value: [{ id: "Status", value: "Failed" }], + type: "column", + }, + { + filterName: "Investigate", + value: [{ id: "Status", value: "Investigate" }], + type: "column", + }, + { + filterName: "Skipped", + value: [{ id: "Status", value: "Skipped" }], + type: "column", + }, + { + filterName: "High Risk", + value: [{ id: "Risk", value: "High" }], + type: "column", + }, + { + filterName: "Medium Risk", + value: [{ id: "Risk", value: "Medium" }], + type: "column", + }, + { + filterName: "Low Risk", + value: [{ id: "Risk", value: "Low" }], + type: "column", + }, + ]; + return ( - - - - - - Identity Test Results - - - This tab will display detailed identity test results and recommendations. - - - Configure your identity policies and authentication methods to improve your zero trust - posture. - - - - + ); }; diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 3b29d86dcaac..5f236257b5c4 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -1,56 +1,196 @@ -import { - Box, - Card, - CardContent, - CardHeader, - Container, - Typography, - Avatar, - Divider, - Tooltip, -} from "@mui/material"; +import { Box, Card, CardContent, Container, Button, Tooltip } from "@mui/material"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import { useForm, useWatch } from "react-hook-form"; import { Grid } from "@mui/system"; -import { - BarChart, - Bar, - PieChart, - Pie, - Cell, - RadialBarChart, - RadialBar, - PolarAngleAxis, - XAxis, - YAxis, - ResponsiveContainer, - Tooltip as RechartsTooltip, - LabelList, -} from "recharts"; +import { useSettings } from "/src/hooks/use-settings"; +import { ApiGetCall } from "/src/api/ApiCall.jsx"; +import Portals from "/src/data/portals"; +import { BulkActionsMenu } from "/src/components/bulk-actions-menu.js"; +import { ExecutiveReportButton } from "/src/components/ExecutiveReportButton.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; import { dashboardDemoData } from "/src/data/dashboardv2-demo-data"; -import { CaSankey } from "/src/components/CippComponents/CaSankey"; -import { CaDeviceSankey } from "/src/components/CippComponents/CaDeviceSankey"; -import { AuthMethodSankey } from "/src/components/CippComponents/AuthMethodSankey"; -import { DesktopDevicesSankey } from "/src/components/CippComponents/DesktopDevicesSankey"; -import { MobileSankey } from "/src/components/CippComponents/MobileSankey"; +import { SecureScoreCard } from "/src/components/CippComponents/SecureScoreCard"; +import { MFACard } from "/src/components/CippComponents/MFACard"; +import { AuthMethodCard } from "/src/components/CippComponents/AuthMethodCard"; +import { LicenseCard } from "/src/components/CippComponents/LicenseCard"; +import { TenantInfoCard } from "/src/components/CippComponents/TenantInfoCard"; +import { TenantMetricsGrid } from "/src/components/CippComponents/TenantMetricsGrid"; +import { AssessmentCard } from "/src/components/CippComponents/AssessmentCard"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; +import { CippAddTestReportDrawer } from "/src/components/CippComponents/CippAddTestReportDrawer"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { - People as UsersIcon, - Person as UserIcon, - PersonOutline as GuestIcon, - Group as GroupIcon, - Apps as AppsIcon, Devices as DevicesIcon, - PhoneAndroid as ManagedIcon, - Security as SecurityIcon, - Business as BuildingIcon, CheckCircle as CheckCircleIcon, - Laptop as MonitorIcon, Work as BriefcaseIcon, + Assessment as AssessmentIcon, + Refresh as RefreshIcon, } from "@mui/icons-material"; const Page = () => { - const reportData = dashboardDemoData; + const settings = useSettings(); + const router = useRouter(); + const { currentTenant } = settings; + const [portalMenuItems, setPortalMenuItems] = useState([]); + const [deleteDialog, setDeleteDialog] = useState({ open: false }); + const [refreshDialog, setRefreshDialog] = useState({ open: false }); + + // Get reportId from query params or default to "ztna" + // Only use default if router is ready and reportId is still not present + const selectedReport = + router.isReady && !router.query.reportId ? "ztna" : router.query.reportId || "ztna"; + + const formControl = useForm({ + mode: "onChange", + }); + + const reportIdValue = useWatch({ control: formControl.control }); + + // Fetch available reports + const reportsApi = ApiGetCall({ + url: "/api/ListTestReports", + queryKey: "ListTestReports", + }); + + const reports = reportsApi.data || []; + + // Update form when selectedReport changes (from URL) + useEffect(() => { + if (selectedReport && router.isReady && reports.length > 0) { + const matchingReport = reports.find((r) => r.id === selectedReport); + if (matchingReport) { + formControl.setValue("reportId", { + value: matchingReport.id, + label: matchingReport.name, + }); + } + } + }, [selectedReport, router.isReady, reports]); + + // Update URL when form value changes (e.g., user selects different report from dropdown) + useEffect(() => { + console.log("reportIdValue changed:", reportIdValue); + if (reportIdValue?.reportId?.value && reportIdValue.reportId.value !== selectedReport) { + router.push( + { + pathname: router.pathname, + query: { ...router.query, reportId: reportIdValue.reportId.value }, + }, + undefined, + { shallow: true } + ); + } + }, [reportIdValue]); + + const organization = ApiGetCall({ + url: "/api/ListOrg", + queryKey: `${currentTenant}-ListOrg`, + data: { tenantFilter: currentTenant }, + }); + + const testsApi = ApiGetCall({ + url: "/api/ListTests", + data: { tenantFilter: currentTenant, reportId: selectedReport }, + queryKey: `${currentTenant}-ListTests-${selectedReport}`, + waiting: !!currentTenant && !!selectedReport, + }); + + const currentTenantInfo = ApiGetCall({ + url: "/api/ListTenants", + queryKey: `ListTenants`, + }); + + const reportData = + testsApi.isSuccess && testsApi.data?.TenantCounts + ? { + ExecutedAt: testsApi.data?.LatestReportTimeStamp || null, + TenantName: organization.data?.displayName || "", + Domain: currentTenant || "", + TestResultSummary: { + IdentityPassed: testsApi.data.TestCounts?.Identity?.Passed || 0, + IdentityTotal: testsApi.data.TestCounts?.Identity?.Total || 0, + DevicesPassed: testsApi.data.TestCounts?.Devices?.Passed || 0, + DevicesTotal: testsApi.data.TestCounts?.Devices?.Total || 0, + DataPassed: 0, + DataTotal: 0, + }, + SecureScore: testsApi.data.SecureScore || [], + TenantInfo: { + TenantOverview: { + UserCount: testsApi.data.TenantCounts.Users || 0, + GuestCount: testsApi.data.TenantCounts.Guests || 0, + GroupCount: testsApi.data.TenantCounts.Groups || 0, + ApplicationCount: testsApi.data.TenantCounts.ServicePrincipals || 0, + DeviceCount: testsApi.data.TenantCounts.Devices || 0, + ManagedDeviceCount: testsApi.data.TenantCounts.ManagedDevices || 0, + }, + MFAState: testsApi.data.MFAState, + OverviewCaDevicesAllUsers: dashboardDemoData.TenantInfo.OverviewCaDevicesAllUsers, + OverviewAuthMethodsPrivilegedUsers: + dashboardDemoData.TenantInfo.OverviewAuthMethodsPrivilegedUsers, + DeviceOverview: dashboardDemoData.TenantInfo.DeviceOverview, + }, + } + : dashboardDemoData; + + // Function to filter portals based on user preferences + const getFilteredPortals = () => { + const defaultLinks = { + M365_Portal: true, + Exchange_Portal: true, + Entra_Portal: true, + Teams_Portal: true, + Azure_Portal: true, + Intune_Portal: true, + SharePoint_Admin: true, + Security_Portal: true, + Compliance_Portal: true, + Power_Platform_Portal: true, + Power_BI_Portal: true, + }; + + let portalLinks; + if (settings.UserSpecificSettings?.portalLinks) { + portalLinks = { ...defaultLinks, ...settings.UserSpecificSettings.portalLinks }; + } else if (settings.portalLinks) { + portalLinks = { ...defaultLinks, ...settings.portalLinks }; + } else { + portalLinks = defaultLinks; + } + + // Filter the portals based on user settings + return Portals.filter((portal) => { + const settingKey = portal.name; + return settingKey ? portalLinks[settingKey] === true : true; + }); + }; + + useEffect(() => { + if (currentTenantInfo.isSuccess) { + const tenantLookup = currentTenantInfo.data?.find( + (tenant) => tenant.defaultDomainName === currentTenant + ); + + // Get filtered portals based on user preferences + const filteredPortals = getFilteredPortals(); + + const menuItems = filteredPortals.map((portal) => ({ + label: portal.label, + target: "_blank", + link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), + icon: portal.icon, + })); + setPortalMenuItems(menuItems); + } + }, [ + currentTenantInfo.isSuccess, + currentTenant, + settings.portalLinks, + settings.UserSpecificSettings, + ]); const formatNumber = (num) => { if (!num && num !== 0) return "0"; @@ -60,313 +200,134 @@ const Page = () => { return num.toLocaleString(); }; - const metricDescriptions = { - users: "Total number of users in your tenant", - guests: "External users with guest access", - groups: "Microsoft 365 and security groups", - apps: "Registered applications", - devices: "All devices accessing tenant resources", - managed: "Devices enrolled in Intune", - }; - return ( - {/* Tenant Overview Section - 3 Column Layout */} - - {/* Column 1: Tenant Information */} - + + + + + + + + + + + + + + + - - - Tenant - - } - sx={{ pb: 1.5 }} - /> - - - - - Name - - - {reportData.TenantName || "Not Available"} - - - - - Tenant ID - - - {reportData.TenantId || "Not Available"} - - - - - Primary Domain - - - {reportData.Domain || "Not Available"} - - + + + ({ + label: r.name, + value: r.id, + description: r.description, + }))} + placeholder="Choose a report" + /> + + + + + + {/* Tenant Overview Section - 3 Column Layout */} + + {/* Column 1: Tenant Information */} + + + {/* Column 2: Tenant Metrics - 2x3 Grid */} - - - - - - - - - - Users - - - {formatNumber(reportData.TenantInfo.TenantOverview.UserCount)} - - - - - - - - - - - - - - Guests - - - {formatNumber(reportData.TenantInfo.TenantOverview.GuestCount)} - - - - - - - - - - - - - - Groups - - - {formatNumber(reportData.TenantInfo.TenantOverview.GroupCount)} - - - - - - - - - - - - - - Apps - - - {formatNumber(reportData.TenantInfo.TenantOverview.ApplicationCount)} - - - - - - - - - - - - - - Devices - - - {formatNumber(reportData.TenantInfo.TenantOverview.DeviceCount)} - - - - - - - - - - - - - - Managed - - - {formatNumber(reportData.TenantInfo.TenantOverview.ManagedDeviceCount)} - - - - - - + {/* Column 3: Assessment Results */} - - - - Assessment - - } - sx={{ pb: 1.5 }} - /> - - - - - - Identity - - - {reportData.TestResultSummary.IdentityPassed}/ - {reportData.TestResultSummary.IdentityTotal} - - tests - - - - - - Devices - - - {reportData.TestResultSummary.DevicesPassed}/ - {reportData.TestResultSummary.DevicesTotal} - - tests - - - - - - - - - - - - - - - + @@ -376,625 +337,61 @@ const Page = () => { {/* Left Column */} - {/* Privileged users auth methods */} - - - - Privileged users auth methods - - } - sx={{ pb: 1 }} - /> - - - {reportData.TenantInfo.OverviewAuthMethodsPrivilegedUsers?.nodes && ( - - )} - - - {reportData.TenantInfo.OverviewAuthMethodsPrivilegedUsers?.description || - "No description available"} - - - - - {/* All Users Auth Methods */} - - - - All users auth methods -
- } - sx={{ pb: 1 }} - /> - - - {reportData.TenantInfo.OverviewAuthMethodsAllUsers?.nodes && ( - - )} - - - {reportData.TenantInfo.OverviewAuthMethodsAllUsers?.description || - "No description available"} - - - + + {/* Right Column */} - {/* User Authentication */} - - - - User authentication - - } - sx={{ pb: 1 }} - /> - - - {reportData.TenantInfo.OverviewCaMfaAllUsers?.nodes && ( - - )} - - - {reportData.TenantInfo.OverviewCaMfaAllUsers?.description || - "No description available"} - - - - - {/* Device Sign-ins */} - - - - Device sign-ins - - } - sx={{ pb: 1 }} - /> - - - {reportData.TenantInfo.OverviewCaDevicesAllUsers?.nodes && ( - - )} - - - {reportData.TenantInfo.OverviewCaDevicesAllUsers?.description || - "No description available"} - - - + + + - {/* Devices Section */} - - - {/* Device Summary Chart */} - - - - - Device summary - - } - sx={{ pb: 1 }} - /> - - - - - - - - - - - - - - - - - - Desktops - - - {Math.round( - ((reportData.TenantInfo.DeviceOverview.ManagedDevices.desktopCount || 0) / - (reportData.TenantInfo.DeviceOverview.ManagedDevices.totalCount || 1)) * - 100 - )} - % - - - - - - Mobiles - - - {Math.round( - ((reportData.TenantInfo.DeviceOverview.ManagedDevices.mobileCount || 0) / - (reportData.TenantInfo.DeviceOverview.ManagedDevices.totalCount || 1)) * - 100 - )} - % - - - - - - - - {/* Device Compliance */} - - - - - Device compliance - - } - sx={{ pb: 1 }} - /> - - - - - - - - - - - - - - - - Compliant - - - - {Math.round( - (reportData.TenantInfo.DeviceOverview.DeviceCompliance - .compliantDeviceCount / - (reportData.TenantInfo.DeviceOverview.DeviceCompliance - .compliantDeviceCount + - reportData.TenantInfo.DeviceOverview.DeviceCompliance - .nonCompliantDeviceCount)) * - 100 - )} - % - - - - - - - - Non-compliant - - - - {Math.round( - (reportData.TenantInfo.DeviceOverview.DeviceCompliance - .nonCompliantDeviceCount / - (reportData.TenantInfo.DeviceOverview.DeviceCompliance - .compliantDeviceCount + - reportData.TenantInfo.DeviceOverview.DeviceCompliance - .nonCompliantDeviceCount)) * - 100 - )} - % - - - - - - - - {/* Device Ownership */} - - - - - Device ownership - - } - sx={{ pb: 1 }} - /> - - - - - - - - - - - - - - - - Corporate - - - - {Math.round( - (reportData.TenantInfo.DeviceOverview.DeviceOwnership.corporateCount / - (reportData.TenantInfo.DeviceOverview.DeviceOwnership.corporateCount + - reportData.TenantInfo.DeviceOverview.DeviceOwnership.personalCount)) * - 100 - )} - % - - - - - - - - Personal - - - - {Math.round( - (reportData.TenantInfo.DeviceOverview.DeviceOwnership.personalCount / - (reportData.TenantInfo.DeviceOverview.DeviceOwnership.corporateCount + - reportData.TenantInfo.DeviceOverview.DeviceOwnership.personalCount)) * - 100 - )} - % - - - - - - - - {/* Desktop Devices - Full Width */} - - - - - Desktop devices - - } - sx={{ pb: 1 }} - /> - - - {reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.nodes && ( - - )} - - - {reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.description || - "No description available"} - - - - - - - - Entra joined - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.nodes || []; - const entraJoined = - nodes.find((n) => n.target === "Entra joined")?.value || 0; - const windowsDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "Windows" - )?.value || 0; - const macOSDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "macOS" - )?.value || 0; - const total = windowsDevices + macOSDevices; - return Math.round((entraJoined / (total || 1)) * 100); - })()} - % - - - - - - Entra hybrid joined - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.nodes || []; - const entraHybrid = - nodes.find((n) => n.target === "Entra hybrid joined")?.value || 0; - const windowsDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "Windows" - )?.value || 0; - const macOSDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "macOS" - )?.value || 0; - const total = windowsDevices + macOSDevices; - return Math.round((entraHybrid / (total || 1)) * 100); - })()} - % - - - - - - Entra registered - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.nodes || []; - const entraRegistered = - nodes.find((n) => n.target === "Entra registered")?.value || 0; - const windowsDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "Windows" - )?.value || 0; - const macOSDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "macOS" - )?.value || 0; - const total = windowsDevices + macOSDevices; - return Math.round((entraRegistered / (total || 1)) * 100); - })()} - % - - - - - - + {/* Delete Report Dialog */} + - {/* Mobile Devices - Full Width */} - - - - - Mobile devices - - } - sx={{ pb: 1 }} - /> - - - {reportData.TenantInfo.DeviceOverview.MobileSummary?.nodes && ( - - )} - - - {reportData.TenantInfo.DeviceOverview.MobileSummary?.description || - "No description available"} - - - - - - - - Android compliant - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.MobileSummary?.nodes || []; - const androidCompliant = nodes - .filter( - (n) => n.source?.includes("Android") && n.target === "Compliant" - ) - .reduce((sum, n) => sum + (n.value || 0), 0); - const androidTotal = - nodes.find( - (n) => n.source === "Mobile devices" && n.target === "Android" - )?.value || 0; - return androidTotal > 0 - ? Math.round((androidCompliant / androidTotal) * 100) - : 0; - })()} - % - - - - - - iOS compliant - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.MobileSummary?.nodes || []; - const iosCompliant = nodes - .filter((n) => n.source?.includes("iOS") && n.target === "Compliant") - .reduce((sum, n) => sum + (n.value || 0), 0); - const iosTotal = - nodes.find((n) => n.source === "Mobile devices" && n.target === "iOS") - ?.value || 0; - return iosTotal > 0 ? Math.round((iosCompliant / iosTotal) * 100) : 0; - })()} - % - - - - - - Total devices - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.MobileSummary?.nodes || []; - const androidTotal = - nodes.find( - (n) => n.source === "Mobile devices" && n.target === "Android" - )?.value || 0; - const iosTotal = - nodes.find((n) => n.source === "Mobile devices" && n.target === "iOS") - ?.value || 0; - return androidTotal + iosTotal; - })()} - - - - - - - - - + {/* Refresh Data Dialog */} + ); }; Page.getLayout = (page) => ( - + {page} ); diff --git a/src/pages/dashboardv2/tabOptions.json b/src/pages/dashboardv2/tabOptions.json index 4c2bb6411b4d..952c392e5c89 100644 --- a/src/pages/dashboardv2/tabOptions.json +++ b/src/pages/dashboardv2/tabOptions.json @@ -10,5 +10,9 @@ { "label": "Devices", "path": "/dashboardv2/devices" + }, + { + "label": "Previous Dashboard Experience", + "path": "/dashboardv1" } ] diff --git a/src/pages/email/reports/mailbox-permissions/index.js b/src/pages/email/reports/mailbox-permissions/index.js new file mode 100644 index 000000000000..54ec41158bce --- /dev/null +++ b/src/pages/email/reports/mailbox-permissions/index.js @@ -0,0 +1,117 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useState } from "react"; +import { + Button, + FormControlLabel, + Switch, + Alert, + SvgIcon, + IconButton, + Tooltip, +} from "@mui/material"; +import { useSettings } from "../../../../hooks/use-settings"; +import { Stack } from "@mui/system"; +import { Sync, Info } from "@mui/icons-material"; +import { useDialog } from "../../../../hooks/use-dialog"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; + +const Page = () => { + const [byUser, setByUser] = useState(true); + const currentTenant = useSettings().currentTenant; + const syncDialog = useDialog(); + + const isAllTenants = currentTenant === "AllTenants"; + + const columns = byUser + ? [ + ...(isAllTenants ? ["Tenant"] : []), + "User", + "UserMailboxType", + "Permissions", + "MailboxCacheTimestamp", + "PermissionCacheTimestamp", + ] + : [ + ...(isAllTenants ? ["Tenant"] : []), + "MailboxUPN", + "MailboxDisplayName", + "MailboxType", + "Permissions", + "MailboxCacheTimestamp", + "PermissionCacheTimestamp", + ]; + + // Compute apiData based on byUser directly (no useState needed) + const apiData = { + UseReportDB: true, + ByUser: byUser, + }; + + const pageActions = [ + + + + + + + + setByUser(e.target.checked)} color="primary" /> + } + label="Group by User" + labelPlacement="start" + /> + , + ]; + + return ( + <> + {currentTenant && currentTenant !== "" ? ( + + ) : ( + Please select a tenant to view mailbox permissions. + )} + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/groups/edit.jsx b/src/pages/identity/administration/groups/edit.jsx index bde4e212c864..304a1792d51b 100644 --- a/src/pages/identity/administration/groups/edit.jsx +++ b/src/pages/identity/administration/groups/edit.jsx @@ -330,6 +330,7 @@ const EditGroup = () => { }, })) || [] } + sortOptions={true} /> @@ -353,6 +354,7 @@ const EditGroup = () => { }, })) || [] } + sortOptions={true} /> @@ -374,6 +376,7 @@ const EditGroup = () => { addedFields: { id: m.id }, })) || [] } + sortOptions={true} /> diff --git a/src/pages/identity/administration/jit-admin-templates/add.jsx b/src/pages/identity/administration/jit-admin-templates/add.jsx new file mode 100644 index 000000000000..baf303eff438 --- /dev/null +++ b/src/pages/identity/administration/jit-admin-templates/add.jsx @@ -0,0 +1,287 @@ +import { Box, Divider, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; +import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector"; +import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; +import gdaproles from "/src/data/GDAPRoles.json"; +import { useSettings } from "../../../../hooks/use-settings"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + const watchedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); + const isAllTenants = watchedTenant?.value === "AllTenants" || watchedTenant === "AllTenants"; + + return ( + <> + + + + + Template Information + + + + + + + + + + + Default JIT Admin Settings + + + + ({ label: role.Name, value: role.ObjectId }))} + formControl={formControl} + required={true} + validators={{ + required: "At least one default role is required", + validate: (options) => { + if (!options?.length) { + return "At least one default role is required"; + } + return true; + }, + }} + /> + + + + { + if (!value) return true; // Optional field + const durationValue = typeof value === "object" && value.value ? value.value : value; + const iso8601Regex = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; + if (!iso8601Regex.test(durationValue)) { + return "Invalid format. Use PT1H, P1D, PT2H30M, etc."; + } + return true; + }, + }} + /> + + + + + + + + + + + + + + + + + + + + + User Creation Settings + + + + + {isAllTenants + ? "AllTenants templates can only use 'New User' option (no further options are configurable)" + : "Choose whether this template creates a new user or assigns to existing user"} + + + + + + + + {isAllTenants + ? "Pre-fill user details (optional, for AllTenants templates)" + : "Pre-fill user details (optional, only for specific tenant templates)"} + + + + + + + + + + + + {!isAllTenants && ( + + + + )} + + + + {!isAllTenants && ( + <> + + + Select default user (optional, only for specific tenant templates) + + + + + + + )} + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/jit-admin-templates/edit.jsx b/src/pages/identity/administration/jit-admin-templates/edit.jsx new file mode 100644 index 000000000000..fdbcf63b650e --- /dev/null +++ b/src/pages/identity/administration/jit-admin-templates/edit.jsx @@ -0,0 +1,313 @@ +import { Box, Divider, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; +import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector"; +import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; +import gdaproles from "/src/data/GDAPRoles.json"; +import { useSettings } from "../../../../hooks/use-settings"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useEffect } from "react"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { id } = router.query; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + const watchedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); + const isAllTenants = watchedTenant?.value === "AllTenants" || watchedTenant === "AllTenants"; + + // Get the template data + const template = ApiGetCall({ + url: `/api/ListJITAdminTemplates?GUID=${id}`, + queryKey: `JITAdminTemplate-${id}`, + waiting: !!id, + }); + + // Populate form when template data is loaded + useEffect(() => { + if (template.isSuccess && template.data?.[0]) { + const templateData = template.data[0]; + formControl.reset({ + ...templateData, + GUID: id, + }); + } + }, [template.isSuccess, template.data]); + + return ( + <> + + + + + Template Information + + + + + + + + + + + Default JIT Admin Settings + + + + ({ label: role.Name, value: role.ObjectId }))} + formControl={formControl} + required={true} + validators={{ + required: "At least one default role is required", + validate: (options) => { + if (!options?.length) { + return "At least one default role is required"; + } + return true; + }, + }} + /> + + + + { + if (!value) return true; // Optional field + const durationValue = typeof value === "object" && value.value ? value.value : value; + const iso8601Regex = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; + if (!iso8601Regex.test(durationValue)) { + return "Invalid format. Use PT1H, P1D, PT2H30M, etc."; + } + return true; + }, + }} + /> + + + + + + + + + + + + + + + + + + + + + User Creation Settings + + + + + + + + + + {isAllTenants + ? "Pre-fill user details (optional, for AllTenants templates)" + : "Pre-fill user details (optional, only for specific tenant templates)"} + + + + + + + + + + + + {!isAllTenants && ( + + + + )} + + + + {!isAllTenants && ( + <> + + + Select default user (optional, only for specific tenant templates) + + + + + + + )} + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/jit-admin-templates/index.js b/src/pages/identity/administration/jit-admin-templates/index.js new file mode 100644 index 000000000000..d5d2e798c7a8 --- /dev/null +++ b/src/pages/identity/administration/jit-admin-templates/index.js @@ -0,0 +1,140 @@ +import { Button } from "@mui/material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { AddBox, Delete, GitHub, Edit } from "@mui/icons-material"; +import Link from "next/link"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; +import { getCippTranslation } from "../../../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; +import { useSettings } from "../../../../hooks/use-settings"; + +const Page = () => { + const pageTitle = "JIT Admin Templates"; + const tenantFilter = useSettings()?.currentTenant; + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const actions = [ + { + label: "Edit Template", + icon: , + link: "/identity/administration/jit-admin-templates/edit?id=[GUID]", + }, + { + 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/RemoveJITAdminTemplate", + icon: , + data: { + ID: "GUID", + }, + confirmText: "Do you want to delete the template?", + multiPost: false, + }, + ]; + + const offCanvas = { + children: (data) => { + const keys = Object.keys(data).filter( + (key) => !key.includes("@odata") && !key.includes("@data") + ); + const properties = []; + keys.forEach((key) => { + if (data[key] && data[key].length > 0) { + properties.push({ + label: getCippTranslation(key), + value: getCippFormatting(data[key], key), + }); + } + }); + return ( + + ); + }, + }; + + return ( + }> + Add JIT Admin Template + + } + offCanvas={offCanvas} + simpleColumns={[ + "templateName", + "defaultForTenant", + "tenantFilter", + "defaultDuration.label", + "defaultRoles", + "generateTAPByDefault", + "defaultExpireAction.label", + "defaultNotificationActions", + "reasonTemplate" + ]} + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index 48e9bf2255a0..c1e60624caa4 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -3,14 +3,190 @@ 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 { useForm, useWatch } 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"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useEffect, useState } from "react"; + const Page = () => { const formControl = useForm({ mode: "onChange" }); + const selectedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); + const [selectedTemplate, setSelectedTemplate] = useState(null); + + const jitAdminTemplates = ApiGetCall({ + url: selectedTenant + ? `/api/ListJITAdminTemplates?TenantFilter=${selectedTenant.value}` + : undefined, + queryKey: selectedTenant ? `JITAdminTemplates-${selectedTenant.value}` : "JITAdminTemplates", + refetchOnMount: false, + refetchOnReconnect: false, + waiting: !!selectedTenant, + }); + + const watcher = useWatch({ control: formControl.control }); + + // Simple duration parser for basic ISO 8601 durations + const parseDuration = (duration) => { + if (!duration) return null; + const matches = duration.match( + /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/ + ); + if (!matches) return null; + return { + years: parseInt(matches[1] || 0), + months: parseInt(matches[2] || 0), + weeks: parseInt(matches[3] || 0), + days: parseInt(matches[4] || 0), + hours: parseInt(matches[5] || 0), + minutes: parseInt(matches[6] || 0), + seconds: parseInt(matches[7] || 0), + }; + }; + + const addDurationToDate = (date, duration) => { + if (!date || !duration) return null; + const parsed = parseDuration(duration); + if (!parsed) return null; + + const result = new Date(date); + result.setFullYear(result.getFullYear() + parsed.years); + result.setMonth(result.getMonth() + parsed.months); + result.setDate(result.getDate() + parsed.weeks * 7); + result.setDate(result.getDate() + parsed.days); + result.setHours(result.getHours() + parsed.hours); + result.setMinutes(result.getMinutes() + parsed.minutes); + result.setSeconds(result.getSeconds() + parsed.seconds); + return result; + }; + + // Auto-select default template for tenant + // Priority: tenant-specific default > AllTenants default + useEffect(() => { + if (jitAdminTemplates.isSuccess && !watcher.jitAdminTemplate) { + const templates = jitAdminTemplates.data || []; + + // First, try to find a tenant-specific default template + let defaultTemplate = templates.find( + (template) => + template.defaultForTenant === true && + template.tenantFilter !== "AllTenants" && + template.tenantFilter === selectedTenant?.value + ); + + // If not found, fall back to AllTenants default template + if (!defaultTemplate) { + defaultTemplate = templates.find( + (template) => template.defaultForTenant === true && template.tenantFilter === "AllTenants" + ); + } + + if (defaultTemplate) { + formControl.setValue("jitAdminTemplate", { + label: defaultTemplate.templateName, + value: defaultTemplate.GUID, + addedFields: defaultTemplate, + }); + setSelectedTemplate(defaultTemplate); + } + } + }, [jitAdminTemplates.isSuccess, selectedTenant]); + + // Only set template-driven fields when the template actually changes + const [lastTemplate, setLastTemplate] = useState(null); + useEffect(() => { + const template = watcher.jitAdminTemplate?.addedFields; + if (!template || template.GUID === lastTemplate) return; + setSelectedTemplate(template); + setLastTemplate(template.GUID); + + // Helpers + const roundDown15 = (date) => { + const d = new Date(date); + d.setMilliseconds(0); + d.setSeconds(0); + d.setMinutes(Math.floor(d.getMinutes() / 15) * 15); + return d; + }; + const roundUp15 = (date) => { + const d = new Date(date); + d.setMilliseconds(0); + d.setSeconds(0); + let min = d.getMinutes(); + d.setMinutes(min % 15 === 0 ? min : Math.ceil(min / 15) * 15); + if (d.getMinutes() === 60) { + d.setHours(d.getHours() + 1); + d.setMinutes(0); + } + return d; + }; + + // Set all template-driven fields + formControl.setValue("adminRoles", template.defaultRoles || [], { shouldDirty: true }); + formControl.setValue("expireAction", template.defaultExpireAction || null, { + shouldDirty: true, + }); + formControl.setValue("postExecution", template.defaultNotificationActions || [], { + shouldDirty: true, + }); + formControl.setValue("UseTAP", template.generateTAPByDefault ?? false, { shouldDirty: true }); + formControl.setValue("reason", template.reasonTemplate || "", { shouldDirty: true }); + + // User action and user details + if (template.defaultUserAction) { + formControl.setValue("userAction", template.defaultUserAction, { shouldDirty: true }); + } + if (template.defaultFirstName) { + formControl.setValue("firstName", template.defaultFirstName, { shouldDirty: true }); + } + if (template.defaultLastName) { + formControl.setValue("lastName", template.defaultLastName, { shouldDirty: true }); + } + if (template.defaultUserName) { + formControl.setValue("userName", template.defaultUserName, { shouldDirty: true }); + } + if (template.defaultDomain) { + formControl.setValue("domain", template.defaultDomain, { shouldDirty: true }); + } + if (template.defaultExistingUser) { + formControl.setValue("existingUser", template.defaultExistingUser, { shouldDirty: true }); + } + + // Dates + if (template.defaultDuration) { + const duration = + typeof template.defaultDuration === "object" && template.defaultDuration !== null + ? template.defaultDuration.value + : template.defaultDuration; + const start = roundDown15(new Date()); + const unixStart = Math.floor(start.getTime() / 1000); + formControl.setValue("startDate", unixStart, { shouldDirty: true }); + const end = roundUp15(addDurationToDate(start, duration)); + const unixEnd = Math.floor(end.getTime() / 1000); + formControl.setValue("endDate", unixEnd, { shouldDirty: true }); + } + }, [watcher.jitAdminTemplate, lastTemplate]); + + // Recalculate end date when start date changes and template has default duration + useEffect(() => { + if (watcher.startDate && selectedTemplate?.defaultDuration) { + const durationValue = + typeof selectedTemplate.defaultDuration === "object" && + selectedTemplate.defaultDuration !== null + ? selectedTemplate.defaultDuration.value + : selectedTemplate.defaultDuration; + const startDateDate = new Date(watcher.startDate * 1000); + const endDateObj = addDurationToDate(startDateDate, durationValue); + if (endDateObj) { + const unixEnd = Math.floor(endDateObj.getTime() / 1000); + formControl.setValue("endDate", unixEnd); + } + } + }, [watcher.startDate]); + return ( <> { validators={{ required: "A tenant must be selected" }} /> + + ({ + label: template.templateName, + value: template.GUID, + addedFields: template, + })) + : [] + } + formControl={formControl} + /> + { label="Expiration Action" name="expireAction" multiple={false} + creatable={false} required={true} options={[ { label: "Delete User", value: "DeleteUser" }, @@ -216,6 +412,7 @@ const Page = () => { label="Notification Action" name="postExecution" multiple={true} + creatable={false} options={[ { label: "Webhook", value: "Webhook" }, { label: "Email", value: "email" }, diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 61013199368c..258a46db771e 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -107,6 +107,12 @@ const Page = () => { waiting: waiting, }); + const junkEmailConfigRequest = ApiGetCall({ + url: `/api/ListUserTrustedBlockedSenders?UserId=${userId}&userPrincipalName=${graphUserRequest.data?.[0]?.userPrincipalName}&tenantFilter=${userSettingsDefaults.currentTenant}`, + queryKey: `TrustedBlockedSenders-${userId}`, + waiting: waiting && !!graphUserRequest.data?.[0]?.userPrincipalName, + }); + const groupsList = ApiGetCall({ url: "/api/ListGraphRequest", data: { @@ -1086,6 +1092,81 @@ const Page = () => { }, ]; + const junkEmailConfigActions = [ + { + label: "Remove Entry", + type: "POST", + icon: , + url: "/api/RemoveTrustedBlockedSender", + customDataformatter: (row, action, formData) => { + return { + userPrincipalName: row?.UserPrincipalName, + typeProperty: row?.TypeProperty, + value: row?.Value, + tenantFilter: userSettingsDefaults.currentTenant, + }; + }, + confirmText: + "Are you sure you want to remove [Value] from the [Type] list for [UserPrincipalName]?", + multiPost: false, + relatedQueryKeys: `JunkEmailConfig-${userId}`, + }, + ]; + + const junkEmailConfigCard = [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: junkEmailConfigRequest.isFetching ? ( + + ) : junkEmailConfigRequest.data?.length !== 0 ? ( + + ) : ( + + ), + }, + text: "Trusted and Blocked Senders/Domains", + subtext: junkEmailConfigRequest.data?.length + ? "Trusted/Blocked senders and domains are configured for this user" + : "No trusted or blocked senders/domains entries for this user", + statusColor: "green.main", + table: { + title: "Trusted and Blocked Senders/Domains", + hideTitle: true, + data: junkEmailConfigRequest.data || [], + refreshFunction: () => junkEmailConfigRequest.refetch(), + isFetching: junkEmailConfigRequest.isFetching, + simpleColumns: ["Type", "Value"], + actions: junkEmailConfigActions, + offCanvas: { + children: (data) => { + return ( + + ); + }, + }, + }, + }, + ]; + const proxyAddressActions = [ { label: "Make Primary", @@ -1268,6 +1349,11 @@ const Page = () => { items={mailboxRulesCard} isCollapsible={true} /> + { const pageTitle = "MFA Report"; const apiUrl = "/api/ListMFAUsers"; - const simpleColumns = [ - "UPN", - "AccountEnabled", - "isLicensed", - "MFARegistration", - "PerUser", - "CoveredBySD", - "CoveredByCA", - "MFAMethods", - "CAPolicies", - "IsAdmin", - ]; + const currentTenant = useSettings().currentTenant; + const syncDialog = useDialog(); + + const isAllTenants = currentTenant === "AllTenants"; + + const apiData = { + UseReportDB: true, + }; + const simpleColumns = isAllTenants + ? [ + "Tenant", + "UPN", + "AccountEnabled", + "isLicensed", + "MFARegistration", + "PerUser", + "CoveredBySD", + "CoveredByCA", + "MFAMethods", + "CAPolicies", + "IsAdmin", + "CacheTimestamp", + ] + : [ + "UPN", + "AccountEnabled", + "isLicensed", + "MFARegistration", + "PerUser", + "CoveredBySD", + "CoveredByCA", + "MFAMethods", + "CAPolicies", + "IsAdmin", + "CacheTimestamp", + ]; const filters = [ { filterName: "Enabled, licensed users", @@ -48,8 +77,8 @@ const Page = () => { { filterName: "Admin Users", value: [{ id: "IsAdmin", value: "Yes" }], - type: "column" - } + type: "column", + }, ]; const actions = [ @@ -78,14 +107,53 @@ const Page = () => { }, ]; + const pageActions = [ + + + + + + + + , + ]; + return ( - + <> + + + ); }; diff --git a/src/pages/index.js b/src/pages/index.js index 576bf8d504ff..fd02b3802bc9 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,470 +1,3 @@ -import Head from "next/head"; -import { useEffect, useState } from "react"; -import { Box, Container, Button, Card, CardContent } from "@mui/material"; -import { Grid } from "@mui/system"; -import { CippInfoBar } from "../components/CippCards/CippInfoBar"; -import { CippChartCard } from "../components/CippCards/CippChartCard"; -import { CippPropertyListCard } from "../components/CippCards/CippPropertyListCard"; -import { Layout as DashboardLayout } from "../layouts/index.js"; -import { useSettings } from "../hooks/use-settings"; -import { getCippFormatting } from "../utils/get-cipp-formatting.js"; -import Portals from "../data/portals"; -import { BulkActionsMenu } from "../components/bulk-actions-menu.js"; -import { CippUniversalSearch } from "../components/CippCards/CippUniversalSearch.jsx"; -import { ApiGetCall } from "../api/ApiCall.jsx"; -import { CippCopyToClipBoard } from "../components/CippComponents/CippCopyToClipboard.jsx"; -import { ExecutiveReportButton } from "../components/ExecutiveReportButton.js"; +import DashboardV2 from "./dashboardv2"; -const Page = () => { - const settings = useSettings(); - const { currentTenant } = settings; - const [domainVisible, setDomainVisible] = useState(false); - - const organization = ApiGetCall({ - url: "/api/ListOrg", - queryKey: `${currentTenant}-ListOrg`, - data: { tenantFilter: currentTenant }, - }); - - const dashboard = ApiGetCall({ - url: "/api/ListuserCounts", - data: { tenantFilter: currentTenant }, - queryKey: `${currentTenant}-ListuserCounts`, - }); - - const sharepoint = ApiGetCall({ - url: "/api/ListSharepointQuota", - queryKey: `${currentTenant}-ListSharepointQuota`, - data: { tenantFilter: currentTenant }, - }); - - const standards = ApiGetCall({ - url: "/api/ListStandardTemplates", - queryKey: `${currentTenant}-ListStandardTemplates`, - }); - - const driftApi = ApiGetCall({ - url: "/api/listTenantDrift", - data: { - TenantFilter: currentTenant, - }, - queryKey: `TenantDrift-${currentTenant}`, - }); - - const partners = ApiGetCall({ - url: "/api/ListGraphRequest", - queryKey: `${currentTenant}-ListPartners`, - data: { - Endpoint: "policies/crossTenantAccessPolicy/partners", - tenantFilter: currentTenant, - ReverseTenantLookup: true, - }, - }); - - const currentTenantInfo = ApiGetCall({ - url: "/api/ListTenants", - queryKey: `ListTenants`, - }); - - // Top bar data - const tenantInfo = [ - { name: "Tenant Name", data: organization.data?.displayName }, - { - name: "Tenant ID", - data: ( - <> - - - ), - }, - { - name: "Default Domain", - data: ( - <> - domain.isDefault === true)?.name - } - type="chip" - /> - - ), - }, - { - name: "AD Sync Enabled", - data: getCippFormatting(organization.data?.onPremisesSyncEnabled, "dirsync"), - }, - ]; - - // Process drift data for chart - filter by current tenant and aggregate - const processDriftDataForTenant = (driftData, currentTenant) => { - if (!driftData) { - return { - alignedCount: 0, - acceptedDeviationsCount: 0, - currentDeviationsCount: 0, - customerSpecificDeviations: 0, - hasData: false, - }; - } - - const rawDriftData = driftData || []; - const tenantDriftData = Array.isArray(rawDriftData) - ? rawDriftData.filter((item) => item.tenantFilter === currentTenant) - : []; - - const hasData = tenantDriftData.length > 0; - - // Aggregate data across all standards for this tenant - const aggregatedData = tenantDriftData.reduce( - (acc, item) => { - acc.acceptedDeviationsCount += item.acceptedDeviationsCount || 0; - acc.currentDeviationsCount += item.currentDeviationsCount || 0; - acc.alignedCount += item.alignedCount || 0; - acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; - return acc; - }, - { - acceptedDeviationsCount: 0, - currentDeviationsCount: 0, - alignedCount: 0, - customerSpecificDeviations: 0, - } - ); - - return { ...aggregatedData, hasData }; - }; - - function getActionCountsForTenant(standardsData, currentTenant) { - if (!standardsData) { - return { - remediateCount: 0, - alertCount: 0, - reportCount: 0, - total: 0, - }; - } - - const applicableTemplates = standardsData.filter((template) => { - const tenantFilterArr = Array.isArray(template?.tenantFilter) ? template.tenantFilter : []; - const excludedTenantsArr = Array.isArray(template?.excludedTenants) - ? template.excludedTenants - : []; - - const tenantInFilter = - tenantFilterArr.length > 0 && tenantFilterArr.some((tf) => tf.value === currentTenant); - - const allTenantsTemplate = - tenantFilterArr.some((tf) => tf.value === "AllTenants") && - (excludedTenantsArr.length === 0 || - !excludedTenantsArr.some((et) => et.value === currentTenant)); - - return tenantInFilter || allTenantsTemplate; - }); - - // Combine standards from all applicable templates: - let combinedStandards = {}; - for (const template of applicableTemplates) { - for (const [standardKey, standardValue] of Object.entries(template.standards)) { - combinedStandards[standardKey] = standardValue; - } - } - - // Count each action type: - let remediateCount = 0; - let alertCount = 0; - let reportCount = 0; - - for (const [, standard] of Object.entries(combinedStandards)) { - let actions = standard.action || []; - if (!Array.isArray(actions)) { - actions = [actions]; - } - actions.forEach((actionObj) => { - if (actionObj?.value === "Remediate") { - remediateCount++; - } else if (actionObj?.value === "Alert") { - alertCount++; - } else if (actionObj?.value === "Report") { - reportCount++; - } - }); - } - - const total = Object.keys(combinedStandards).length; - - return { remediateCount, alertCount, reportCount, total }; - } - - const driftData = processDriftDataForTenant(driftApi.data, currentTenant); - const { remediateCount, alertCount, reportCount, total } = getActionCountsForTenant( - standards.data, - currentTenant - ); - - const [PortalMenuItems, setPortalMenuItems] = useState([]); - const [partnersVisible, setPartnersVisible] = useState(false); - - const formatStorageSize = (sizeInMB) => { - if (sizeInMB >= 1024) { - return `${(sizeInMB / 1024).toFixed(2)}GB`; - } - return `${sizeInMB}MB`; - }; - - // Function to filter portals based on user preferences - const getFilteredPortals = () => { - const defaultLinks = { - M365_Portal: true, - Exchange_Portal: true, - Entra_Portal: true, - Teams_Portal: true, - Azure_Portal: true, - Intune_Portal: true, - SharePoint_Admin: true, - Security_Portal: true, - Compliance_Portal: true, - Power_Platform_Portal: true, - Power_BI_Portal: true, - }; - - let portalLinks; - if (settings.UserSpecificSettings?.portalLinks) { - portalLinks = { ...defaultLinks, ...settings.UserSpecificSettings.portalLinks }; - } else if (settings.portalLinks) { - portalLinks = { ...defaultLinks, ...settings.portalLinks }; - } else { - portalLinks = defaultLinks; - } - - // Filter the portals based on user settings - return Portals.filter((portal) => { - const settingKey = portal.name; - return settingKey ? portalLinks[settingKey] === true : true; - }); - }; - - useEffect(() => { - if (currentTenantInfo.isSuccess) { - const tenantLookup = currentTenantInfo.data?.find( - (tenant) => tenant.defaultDomainName === currentTenant - ); - - // Get filtered portals based on user preferences - const filteredPortals = getFilteredPortals(); - - const menuItems = filteredPortals.map((portal) => ({ - label: portal.label, - target: "_blank", - link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), - icon: portal.icon, - })); - setPortalMenuItems(menuItems); - } - }, [ - currentTenantInfo.isSuccess, - currentTenant, - settings.portalLinks, - settings.UserSpecificSettings, - ]); - - return ( - <> - - Dashboard - - - - - - - - - - - {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} - - - - - - - - - - - - - - - - - - - - - {/* Converted Domain Names to Property List */} - - ({ - label: "", - value: domain.name, - }))} - actionButton={ - organization.data?.verifiedDomains?.length > 3 && ( - - ) - } - /> - - - - ({ - label: partner.TenantInfo?.displayName, - value: partner.TenantInfo?.defaultDomainName, - }))} - actionButton={ - partners.data?.Results?.length > 3 && ( - - ) - } - /> - - - - - plan.capabilityStatus === "Enabled" && - ["exchange", "AADPremiumService", "WindowsDefenderATP"].includes( - plan.service - ) - ) - .reduce((uniqueServices, curr) => { - const serviceLabel = - curr.service === "exchange" - ? "Exchange" - : curr.service === "AADPremiumService" - ? "AAD Premium" - : curr.service === "Windows Defender" - ? "Windows Defender" - : curr.service; - - if (!uniqueServices.includes(serviceLabel)) { - uniqueServices.push(serviceLabel); - } - return uniqueServices; - }, []) - .join(", "), - }, - ]} - /> - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +export default DashboardV2; diff --git a/src/pages/tenant/administration/app-consent-requests/index.js b/src/pages/tenant/administration/app-consent-requests/index.js index 5aafe8c75958..3127fdfd8dd1 100644 --- a/src/pages/tenant/administration/app-consent-requests/index.js +++ b/src/pages/tenant/administration/app-consent-requests/index.js @@ -86,6 +86,7 @@ const Page = () => { ]; const simpleColumns = [ + "requestDate", // Request Date "requestUser", // Requester "appDisplayName", // Application Name "appId", // Application ID @@ -116,6 +117,7 @@ const Page = () => { const offCanvas = { extendedInfoFields: [ + "requestDate", // Request Date "requestUser", // Requester "appDisplayName", // Application Name "appId", // Application ID diff --git a/src/pages/tenant/gdap-management/onboarding/start.js b/src/pages/tenant/gdap-management/onboarding/start.js index e6ae11357c1a..6d37fbf75650 100644 --- a/src/pages/tenant/gdap-management/onboarding/start.js +++ b/src/pages/tenant/gdap-management/onboarding/start.js @@ -507,7 +507,7 @@ const Page = () => { { label: "Invite URL", value: getCippFormatting( - "https://admin.microsoft.com/AdminPortal/Home#/partners/invitation/granularAdminRelationships/" + + "https://admin.cloud.microsoft/?#/partners/invitation/granularAdminRelationships/" + currentRelationship.value, "InviteUrl", "url" diff --git a/src/pages/tenant/gdap-management/relationships/relationship/index.js b/src/pages/tenant/gdap-management/relationships/relationship/index.js index c739ca3a0e21..40e01ba7c7e9 100644 --- a/src/pages/tenant/gdap-management/relationships/relationship/index.js +++ b/src/pages/tenant/gdap-management/relationships/relationship/index.js @@ -120,7 +120,7 @@ const Page = () => { properties.push({ label: "Invite URL", value: getCippFormatting( - "https://admin.microsoft.com/AdminPortal/Home#/partners/invitation/granularAdminRelationships/" + + "https://admin.cloud.microsoft/?#/partners/invitation/granularAdminRelationships/" + data?.id, "InviteUrl", "url" diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 71e4865d8771..5b6539767fce 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -145,121 +145,113 @@ const Page = () => { Object.entries(selectedTemplate.standards).forEach(([standardKey, standardConfig]) => { if (standardKey === "IntuneTemplate" && Array.isArray(standardConfig)) { standardConfig.forEach((templateItem, index) => { - console.log("Processing IntuneTemplate item:", templateItem); - if ( - templateItem["TemplateList-Tags"]?.value && - templateItem["TemplateList-Tags"]?.addedFields?.templates - ) { - console.log( - "Found TemplateList-Tags for IntuneTemplate:", - templateItem["TemplateList-Tags"] - ); - console.log( - "Templates to expand:", - templateItem["TemplateList-Tags"].addedFields.templates - ); - templateItem["TemplateList-Tags"].addedFields.templates.forEach( - (expandedTemplate) => { - console.log("Expanding IntuneTemplate:", expandedTemplate); - const itemTemplateId = expandedTemplate.GUID; - const standardId = `standards.IntuneTemplate.${itemTemplateId}`; - const standardInfo = standards.find( - (s) => s.name === `standards.IntuneTemplate` - ); - - // Find the tenant's value for this specific template - const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId - ); - - // Get the standard object and its value from the tenant object - const standardObject = currentTenantObj?.[standardId]; - const directStandardValue = standardObject?.Value; - - // Determine compliance status - let isCompliant = false; - - // For IntuneTemplate, the value is true if compliant, or an object with comparison data if not compliant - if (directStandardValue === true) { - isCompliant = true; - } else if ( - directStandardValue !== undefined && - typeof directStandardValue !== "object" - ) { - isCompliant = true; - } else if (currentTenantStandard) { - isCompliant = currentTenantStandard.value === true; - } + if (!templateItem) return; // Skip null items + + // Check for both addedFields.templates AND rawData.templates + const tagTemplates = + templateItem["TemplateList-Tags"]?.addedFields?.templates || + templateItem["TemplateList-Tags"]?.rawData?.templates; + + if (templateItem["TemplateList-Tags"]?.value && tagTemplates) { + tagTemplates.forEach((expandedTemplate) => { + const itemTemplateId = expandedTemplate.GUID; + const standardId = `standards.IntuneTemplate.${itemTemplateId}`; + const standardInfo = standards.find( + (s) => s.name === `standards.IntuneTemplate` + ); - // Create a standardValue object that contains the template settings - const templateSettings = { - templateId, - Template: - expandedTemplate.displayName || - expandedTemplate.name || - "Unknown Template", - "Assign to": templateItem.AssignTo || "On", - "Excluded Group": templateItem.excludeGroup || "", - "Included Group": templateItem.customGroup || "", - }; - - // Check if this standard is overridden by another template - const tenantTemplateId = standardObject?.TemplateId; - const isOverridden = tenantTemplateId && tenantTemplateId !== templateId; - const overridingTemplateName = isOverridden - ? getTemplateDisplayName(tenantTemplateId) - : null; - - allStandards.push({ - standardId, - standardName: `Intune Template: ${ - expandedTemplate.displayName || expandedTemplate.name || itemTemplateId - } (via ${templateItem["TemplateList-Tags"].value})`, - currentTenantValue: - standardObject !== undefined - ? { - Value: directStandardValue, - LastRefresh: standardObject?.LastRefresh, - TemplateId: tenantTemplateId, - } - : currentTenantStandard?.value, - standardValue: templateSettings, - complianceStatus: isOverridden - ? "Overridden" - : isCompliant - ? "Compliant" - : "Non-Compliant", - isOverridden, - overridingTemplateId: isOverridden ? tenantTemplateId : null, - overridingTemplateName, - complianceDetails: - standardInfo?.docsDescription || standardInfo?.helpText || "", - standardDescription: standardInfo?.helpText || "", - standardImpact: standardInfo?.impact || "Medium Impact", - standardImpactColour: standardInfo?.impactColour || "warning", - templateName: selectedTemplate?.templateName || "Standard Template", - templateActions: (() => { - const actions = templateItem.action || []; - const hasRemediate = actions.some((a) => { - const label = typeof a === "object" ? a?.label || a?.value : a; - return label === "Remediate" || label === "remediate"; - }); - const hasReport = actions.some((a) => { - const label = typeof a === "object" ? a?.label || a?.value : a; - return label === "Report" || label === "report"; - }); - if (hasRemediate && !hasReport) { - return [...actions, "Report"]; - } - return actions; - })(), - autoRemediate: - templateItem.autoRemediate || - templateItem.TemplateList?.autoRemediate || - false, - }); + // Find the tenant's value for this specific template + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + + // Get the standard object and its value from the tenant object + const standardObject = currentTenantObj?.[standardId]; + const directStandardValue = standardObject?.Value; + + // Determine compliance status + let isCompliant = false; + + // For IntuneTemplate, the value is true if compliant, or an object with comparison data if not compliant + if (directStandardValue === true) { + isCompliant = true; + } else if ( + directStandardValue !== undefined && + typeof directStandardValue !== "object" + ) { + isCompliant = true; + } else if (currentTenantStandard) { + isCompliant = currentTenantStandard.value === true; } - ); + + // Create a standardValue object that contains the template settings + const templateSettings = { + templateId, + Template: + expandedTemplate.displayName || expandedTemplate.name || "Unknown Template", + "Assign to": templateItem.AssignTo || "On", + "Excluded Group": templateItem.excludeGroup || "", + "Included Group": templateItem.customGroup || "", + }; + + // Check if this standard is overridden by another template + const tenantTemplateId = standardObject?.TemplateId; + const isOverridden = tenantTemplateId && tenantTemplateId !== templateId; + const overridingTemplateName = isOverridden + ? getTemplateDisplayName(tenantTemplateId) + : null; + + allStandards.push({ + standardId, + standardName: `Intune Template: ${ + expandedTemplate.displayName || expandedTemplate.name || itemTemplateId + } (via ${templateItem["TemplateList-Tags"]?.value})`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, + } + : currentTenantStandard?.value, + standardValue: templateSettings, + complianceStatus: isOverridden + ? "Overridden" + : isCompliant + ? "Compliant" + : "Non-Compliant", + isOverridden, + overridingTemplateId: isOverridden ? tenantTemplateId : null, + overridingTemplateName, + complianceDetails: + standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate?.templateName || "Standard Template", + templateActions: (() => { + const actions = templateItem.action || []; + const hasRemediate = actions.some((a) => { + const label = typeof a === "object" ? a?.label || a?.value : a; + return label === "Remediate" || label === "remediate"; + }); + const hasReport = actions.some((a) => { + const label = typeof a === "object" ? a?.label || a?.value : a; + return label === "Report" || label === "report"; + }); + if (hasRemediate && !hasReport) { + return [...actions, "Report"]; + } + return actions; + })(), + autoRemediate: + templateItem.autoRemediate || + templateItem.TemplateList?.autoRemediate || + false, + }); + }); } else { // Regular TemplateList processing const itemTemplateId = templateItem.TemplateList?.value; @@ -320,6 +312,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: templateSettings, // Use the template settings object instead of true @@ -367,108 +361,100 @@ const Page = () => { ) { // Process each ConditionalAccessTemplate item separately standardConfig.forEach((templateItem, index) => { + if (!templateItem) return; // Skip null items + + // Check for both addedFields.templates AND rawData.templates + const tagTemplates = + templateItem["TemplateList-Tags"]?.addedFields?.templates || + templateItem["TemplateList-Tags"]?.rawData?.templates; + // Check if this item has TemplateList-Tags and expand them - if ( - templateItem["TemplateList-Tags"]?.value && - templateItem["TemplateList-Tags"]?.addedFields?.templates - ) { - console.log( - "Found TemplateList-Tags for ConditionalAccessTemplate:", - templateItem["TemplateList-Tags"] - ); - console.log( - "Templates to expand:", - templateItem["TemplateList-Tags"].addedFields.templates - ); - // Expand TemplateList-Tags into multiple template items - templateItem["TemplateList-Tags"].addedFields.templates.forEach( - (expandedTemplate) => { - console.log("Expanding ConditionalAccessTemplate:", expandedTemplate); - const itemTemplateId = expandedTemplate.GUID; - const standardId = `standards.ConditionalAccessTemplate.${itemTemplateId}`; - 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; - const tenantTemplateId = standardObject?.TemplateId; - const isOverridden = tenantTemplateId && tenantTemplateId !== templateId; - const overridingTemplateName = isOverridden - ? getTemplateDisplayName(tenantTemplateId) - : null; - 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; - } + if (templateItem["TemplateList-Tags"]?.value && tagTemplates) { + tagTemplates.forEach((expandedTemplate) => { + const itemTemplateId = expandedTemplate.GUID; + const standardId = `standards.ConditionalAccessTemplate.${itemTemplateId}`; + const standardInfo = standards.find( + (s) => s.name === `standards.ConditionalAccessTemplate` + ); - // Create a standardValue object that contains the template settings - const templateSettings = { - templateId: itemTemplateId, - Template: - expandedTemplate.displayName || - expandedTemplate.name || - "Unknown Template", - }; - - allStandards.push({ - standardId, - standardName: `Conditional Access Template: ${ - expandedTemplate.displayName || expandedTemplate.name || itemTemplateId - } (via ${templateItem["TemplateList-Tags"].value})`, - currentTenantValue: - standardObject !== undefined - ? { - Value: directStandardValue, - LastRefresh: standardObject?.LastRefresh, - TemplateId: tenantTemplateId, - } - : currentTenantStandard?.value, - standardValue: templateSettings, - complianceStatus: isOverridden - ? "Overridden" - : 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: (() => { - const actions = templateItem.action || []; - const hasRemediate = actions.some((a) => { - const label = typeof a === "object" ? a?.label || a?.value : a; - return label === "Remediate" || label === "remediate"; - }); - const hasReport = actions.some((a) => { - const label = typeof a === "object" ? a?.label || a?.value : a; - return label === "Report" || label === "report"; - }); - if (hasRemediate && !hasReport) { - return [...actions, "Report"]; - } - return actions; - })(), - autoRemediate: - templateItem.autoRemediate || - templateItem.TemplateList?.autoRemediate || - false, - isOverridden, - overridingTemplateId: isOverridden ? tenantTemplateId : null, - overridingTemplateName, - }); + // 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; + const tenantTemplateId = standardObject?.TemplateId; + const isOverridden = tenantTemplateId && tenantTemplateId !== templateId; + const overridingTemplateName = isOverridden + ? getTemplateDisplayName(tenantTemplateId) + : null; + 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: itemTemplateId, + Template: + expandedTemplate.displayName || expandedTemplate.name || "Unknown Template", + }; + + allStandards.push({ + standardId, + standardName: `Conditional Access Template: ${ + expandedTemplate.displayName || expandedTemplate.name || itemTemplateId + } (via ${templateItem["TemplateList-Tags"]?.value})`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, + } + : currentTenantStandard?.value, + standardValue: templateSettings, + complianceStatus: isOverridden + ? "Overridden" + : 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: (() => { + const actions = templateItem.action || []; + const hasRemediate = actions.some((a) => { + const label = typeof a === "object" ? a?.label || a?.value : a; + return label === "Remediate" || label === "remediate"; + }); + const hasReport = actions.some((a) => { + const label = typeof a === "object" ? a?.label || a?.value : a; + return label === "Report" || label === "report"; + }); + if (hasRemediate && !hasReport) { + return [...actions, "Report"]; + } + return actions; + })(), + autoRemediate: + templateItem.autoRemediate || + templateItem.TemplateList?.autoRemediate || + false, + isOverridden, + overridingTemplateId: isOverridden ? tenantTemplateId : null, + overridingTemplateName, + }); + }); } else { // Regular TemplateList processing const itemTemplateId = templateItem.TemplateList?.value; @@ -515,6 +501,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: templateSettings, // Use the template settings object instead of true @@ -637,6 +625,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -692,10 +682,6 @@ const Page = () => { (s) => s.standardId === standardId ); - // Determine compliance status - let isCompliant = false; - let reportingDisabled = !reportingEnabled; - // Check if the standard is directly in the tenant object (like "standards.AuditLog": {...}) const standardIdWithoutPrefix = standardId.replace("standards.", ""); const standardObject = currentTenantObj?.[standardId]; @@ -703,10 +689,23 @@ const Page = () => { // Extract the actual value from the standard object (new data structure includes .Value property) const directStandardValue = standardObject?.Value; - // Special case for boolean standards that are true in the tenant + // Determine compliance - use backend's logic: Value === true OR CurrentValue === ExpectedValue + let isCompliant = false; + let reportingDisabled = !reportingEnabled; + if (directStandardValue === true) { - // If the standard is directly in the tenant and is true, it's compliant + // Boolean true means compliant isCompliant = true; + } else if (standardObject?.CurrentValue && standardObject?.ExpectedValue) { + // Compare CurrentValue and ExpectedValue (backend's comparison logic) + isCompliant = + JSON.stringify(standardObject.CurrentValue) === + JSON.stringify(standardObject.ExpectedValue); + } else if (standardObject?.CurrentValue && standardObject?.ExpectedValue) { + // Compare CurrentValue and ExpectedValue (backend's comparison logic) + isCompliant = + JSON.stringify(standardObject.CurrentValue) === + JSON.stringify(standardObject.ExpectedValue); } else if (directStandardValue !== undefined) { // For non-boolean values, use strict equality isCompliant = @@ -746,6 +745,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: standardSettings, @@ -962,51 +963,7 @@ const Page = () => { // Prepare title and subtitle for HeaderedTabbedLayout const title = selectedTemplate?.templateName || selectedTemplate?.displayName || "Tenant Report"; - const subtitle = [ - { - icon: , - text: ( - - { - const query = { ...router.query }; - if (selectedTemplate && selectedTemplate.value) { - query.templateId = selectedTemplate.value; - } else { - delete query.templateId; - } - router.replace( - { - pathname: router.pathname, - query: query, - }, - undefined, - { shallow: true } - ); - }} - sx={{ minWidth: 300 }} - placeholder="Select a template..." - /> - {templateId && ( - - )} - - ), - }, - ]; + const subtitle = []; // Actions for the header const actions = [ @@ -1123,7 +1080,34 @@ const Page = () => { mt: 2, }} > - + + { + const query = { ...router.query }; + if (selectedTemplate && selectedTemplate.value) { + query.templateId = selectedTemplate.value; + } else { + delete query.templateId; + } + router.replace( + { + pathname: router.pathname, + query: query, + }, + undefined, + { shallow: true } + ); + }} + sx={{ width: 300 }} + placeholder="Select template..." + /> { }, }} /> + {templateId && ( + + )} - handleMenuClose(item.id)} - > - handleAction("accept-customer-specific", item.id)}> - - Accept Deviation - Customer Specific - - handleAction("accept", item.id)}> - - Accept Deviation - - {supportsDelete && ( - handleAction("deny-delete", item.id)}> - - Deny Deviation - Delete Policy - - )} - handleAction("deny-remediate", item.id)}> - - Deny Deviation - Remediate to align with template - - - + cardLabelBoxActions: ( + ), }; }); // Add action buttons to accepted deviation items const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => { - // Check if this is a template that supports delete action - const supportsDelete = - (item.standardName?.includes("ConditionalAccessTemplate") || - item.standardName?.includes("IntuneTemplate")) && - item.expectedValue === "This policy only exists in the tenant, not in the template."; - return { ...item, - actionButton: ( - <> - - handleMenuClose(`accepted-${item.id}`)} - > - {supportsDelete && ( - handleDeviationAction("deny-delete", item)}> - - Deny - Delete Policy - - )} - handleDeviationAction("deny-remediate", item)}> - - Deny - Remediate to align with template - - handleDeviationAction("accept-customer-specific", item)}> - - Accept - Customer Specific - - - + cardLabelBoxActions: ( + ), }; }); // Add action buttons to customer specific deviation items const customerSpecificDeviationItemsWithActions = customerSpecificDeviationItems.map((item) => { - // Check if this is a template that supports delete action - const supportsDelete = - (item.standardName?.includes("ConditionalAccessTemplate") || - item.standardName?.includes("IntuneTemplate")) && - item.expectedValue === "This policy only exists in the tenant, not in the template."; - return { ...item, - actionButton: ( - <> - - handleMenuClose(`customer-${item.id}`)} - > - {supportsDelete && ( - handleDeviationAction("deny-delete", item)}> - - Deny - Delete - - )} - handleDeviationAction("deny-remediate", item)}> - - Deny - Remediate to align with template - - handleDeviationAction("accept", item)}> - - Accept - - - - ), - }; - }); - - // Add action buttons to denied deviation items - const deniedDeviationItemsWithActions = deniedDeviationItems.map((item) => ({ - ...item, - actionButton: ( - <> + cardLabelBoxActions: ( - handleMenuClose(`denied-${item.id}`)} - > - handleDeviationAction("accept", item)}> - - Accept - - handleDeviationAction("accept-customer-specific", item)}> - - Accept - Customer Specific - - - + ), + }; + }); + + // Add action buttons to denied deviation items + const deniedDeviationItemsWithActions = deniedDeviationItems.map((item) => ({ + ...item, + cardLabelBoxActions: ( + ), })); // Calculate compliance metrics for badges + // Accepted and Customer Specific deviations count as compliant since they are user-approved + // Denied deviations are included in total but not in compliant count (they haven't been fixed yet) const totalPolicies = processedDriftData.alignedCount + processedDriftData.currentDeviationsCount + processedDriftData.acceptedDeviationsCount + + processedDriftData.customerSpecificDeviations + + processedDriftData.deniedDeviationsCount; + + const compliantCount = + processedDriftData.alignedCount + + processedDriftData.acceptedDeviationsCount + processedDriftData.customerSpecificDeviations; + // Alignment Score: Only actual compliance (excluding license-missing items) const compliancePercentage = - totalPolicies > 0 ? Math.round((processedDriftData.alignedCount / totalPolicies) * 100) : 0; + totalPolicies > 0 ? Math.round((compliantCount / totalPolicies) * 100) : 0; + + // Calculate missing license percentage + const missingLicensePercentage = + totalPolicies > 0 ? Math.round((licenseSkippedItems.length / totalPolicies) * 100) : 0; - const missingLicensePercentage = 0; // This would need to be calculated from actual license data + // Total Score: Alignment + License Missing (represents addressable compliance) const combinedScore = compliancePercentage + missingLicensePercentage; + // Helper function to get category from standardName + const getCategory = (standardName) => { + if (!standardName) return "Other Standards"; + if (standardName.includes("ConditionalAccessTemplate")) return "Conditional Access Policies"; + if (standardName.includes("IntuneTemplate")) return "Intune Policies"; + + // For other standards, look up category in standards.json + const standard = standardsData.find((s) => s.name === standardName); + if (standard && standard.cat) { + return standard.cat; + } + + return "Other Standards"; + }; + + // Apply search and sort filters + const applyFilters = (items) => { + let filtered = [...items]; + + if (searchQuery) { + filtered = filtered.filter( + (item) => + item.text?.toLowerCase().includes(searchQuery.toLowerCase()) || + item.subtext?.toLowerCase().includes(searchQuery.toLowerCase()) || + item.standardName?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + } + + if (sortBy === "name") { + filtered.sort((a, b) => (a.text || "").localeCompare(b.text || "")); + } else if (sortBy === "status") { + filtered.sort((a, b) => (a.statusText || "").localeCompare(b.statusText || "")); + } else if (sortBy === "category") { + // Sort by category, then by name within each category + filtered.sort((a, b) => { + const catA = getCategory(a.standardName); + const catB = getCategory(b.standardName); + if (catA !== catB) { + return catA.localeCompare(catB); + } + return (a.text || "").localeCompare(b.text || ""); + }); + } + + return filtered; + }; + + const filteredDeviationItems = applyFilters(deviationItemsWithActions); + const filteredAcceptedItems = applyFilters(acceptedDeviationItemsWithActions); + const filteredCustomerSpecificItems = applyFilters(customerSpecificDeviationItemsWithActions); + const filteredDeniedItems = applyFilters(deniedDeviationItemsWithActions); + const filteredAlignedItems = applyFilters(allAlignedItems); + const filteredLicenseSkippedItems = applyFilters(licenseSkippedItems); + + // Helper function to render items grouped by category when category sort is active + const renderItemsByCategory = (items) => { + if (sortBy !== "category" || items.length === 0) { + return ( + + ); + } + + // Group items by category and collect unique categories + const groupedItems = {}; + items.forEach((item) => { + const category = getCategory(item.standardName); + if (!groupedItems[category]) { + groupedItems[category] = []; + } + groupedItems[category].push(item); + }); + + // Sort categories alphabetically + const categories = Object.keys(groupedItems).sort(); + + return ( + + {categories.map((category) => { + if (groupedItems[category].length === 0) return null; + return ( + + + {category} + + + + ); + })} + + ); + }; + // Simple filter for drift templates const driftTemplateOptions = standardsApi.data ? standardsApi.data @@ -768,76 +1149,7 @@ const ManageDriftPage = () => { ? driftTemplateOptions.find((option) => option.value === templateId) || null : null; const title = "Manage Drift"; - const subtitle = [ - { - icon: , - text: ( - { - const query = { ...router.query }; - if (selectedTemplate && selectedTemplate.value) { - query.templateId = selectedTemplate.value; - } else { - delete query.templateId; - } - router.replace( - { - pathname: router.pathname, - query: query, - }, - undefined, - { shallow: true } - ); - }} - sx={{ minWidth: 300 }} - placeholder="Select a drift template..." - /> - ), - }, - // 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" - } - /> - - ), - }, - ] - : []), - ]; + const subtitle = []; return ( { ) : ( - {/* Left side - Chart */} + {/* Left side - Alignment Score & Filters */} - + + {/* Stats Card */} + + + + + Aligned + + + + + + Current + + + + + + Accepted + + + + + + Customer Specific + + + + + + Denied + + + + + + Skipped (No License) + + + + + + + Total + + + + + + Alignment Score + + + + + + Total Score + + = 80 + ? "warning" + : combinedScore >= 30 + ? "warning" + : "error" + } + variant="outlined" + /> + + + + + {/* Filters Card */} + + + { + const query = { ...router.query }; + if (selectedTemplate && selectedTemplate.value) { + query.templateId = selectedTemplate.value; + } else { + delete query.templateId; + } + router.replace( + { + pathname: router.pathname, + query: query, + }, + undefined, + { shallow: true } + ); + }} + placeholder="Select a drift template..." + /> + + setSearchQuery(e.target.value)} + /> + + + + setSortBy(newValue?.value || "name")} + multiple={false} + /> + + + {/* Right side - Deviation Management */} - + {/* Current Deviations Section */} - - {/* Header with bulk actions */} - - Current Deviations - - {/* Bulk Actions Dropdown */} - - setBulkActionsAnchorEl(null)} - > - handleBulkAction("accept-all-customer-specific")}> - - Accept All Deviations - Customer Specific - - handleBulkAction("accept-all")}> - - Accept All Deviations - - {/* Only show delete option if there are template deviations that support deletion */} - {processedDriftData.currentDeviations.some( - (deviation) => - (deviation.standardName?.includes("ConditionalAccessTemplate") || - deviation.standardName?.includes("IntuneTemplate")) && - deviation.expectedValue === - "This policy only exists in the tenant, not in the template." - ) && ( - handleBulkAction("deny-all-delete")}> - - Deny All Deviations - Delete - - )} - handleBulkAction("deny-all-remediate")}> - - Deny All Deviations - Remediate to align with template - - - - Remove Drift Customization - - + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "current")) && ( + + {/* Header with bulk actions */} + + New Deviations + {selectedItems.length > 0 && ( + + {/* Bulk Actions Dropdown */} + + setBulkActionsAnchorEl(null)} + > + handleBulkAction("accept-all-customer-specific")} + > + + Accept All Deviations - Customer Specific + + handleBulkAction("accept-all")}> + + Accept All Deviations + + {/* Only show delete option if there are template deviations that support deletion */} + {processedDriftData.currentDeviations.some( + (deviation) => + (deviation.standardName?.includes("ConditionalAccessTemplate") || + deviation.standardName?.includes("IntuneTemplate")) && + deviation.expectedValue === + "This policy only exists in the tenant, not in the template." + ) && ( + handleBulkAction("deny-all-delete")}> + + Deny All Deviations - Delete + + )} + handleBulkAction("deny-all-remediate")}> + + Deny All Deviations - Remediate to align with template + + + + Remove Drift Customization + + + + )} + {renderItemsByCategory(filteredDeviationItems)} - - + )} {/* Accepted Deviations Section */} - {acceptedDeviationItemsWithActions.length > 0 && ( - - - Accepted Deviations - - - - )} + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "accepted")) && + filteredAcceptedItems.length > 0 && ( + + + Accepted Deviations + + {renderItemsByCategory(filteredAcceptedItems)} + + )} {/* Customer Specific Deviations Section */} - {customerSpecificDeviationItemsWithActions.length > 0 && ( - - - Accepted Deviations - Customer Specific - - - - )} + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "customerspecific")) && + filteredCustomerSpecificItems.length > 0 && ( + + + Accepted Deviations - Customer Specific + + {renderItemsByCategory(filteredCustomerSpecificItems)} + + )} {/* Denied Deviations Section */} - {deniedDeviationItemsWithActions.length > 0 && ( + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "denied")) && + filteredDeniedItems.length > 0 && ( + + + Denied Deviations + + {renderItemsByCategory(filteredDeniedItems)} + + )} + + {/* Compliant Standards Section - Only shown when filtered by All or Compliant */} + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "compliant")) && + filteredAlignedItems.length > 0 && ( + + + Compliant Standards + + + + )} + + {/* License Skipped Section - Always at the end */} + {filteredLicenseSkippedItems.length > 0 && ( - Denied Deviations + Skipped - No License Available + + + These standards were skipped because the required licenses are not available + for this tenant. { api={{ url: "/api/ExecUpdateDriftDeviation", type: "POST", - data: { - deviations: "deviations", - }, + postEntireRow: true, confirmText: `Are you sure you'd like to ${actionData.action?.text || "update"} ${ actionData.action?.type === "single" ? "this deviation" @@ -1038,6 +1559,179 @@ const ManageDriftPage = () => { /> )} + {/* Render all Menu components outside of card structure */} + {deviationItemsWithActions.map((item) => { + const supportsDelete = + (item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate")) && + item.expectedValue === "This policy only exists in the tenant, not in the template."; + return ( + handleMenuClose(item.id)} + > + { + handleDeviationAction("accept-customer-specific", item); + handleMenuClose(item.id); + }} + > + + Accept Deviation - Customer Specific + + { + handleDeviationAction("accept", item); + handleMenuClose(item.id); + }} + > + + Accept Deviation + + {supportsDelete && ( + { + handleDeviationAction("deny-delete", item); + handleMenuClose(item.id); + }} + > + + Deny Deviation - Delete Policy + + )} + { + handleDeviationAction("deny-remediate", item); + handleMenuClose(item.id); + }} + > + + Deny Deviation - Remediate to align with template + + + ); + })} + + {acceptedDeviationItemsWithActions.map((item) => { + const supportsDelete = + (item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate")) && + item.expectedValue === "This policy only exists in the tenant, not in the template."; + return ( + handleMenuClose(`accepted-${item.id}`)} + > + {supportsDelete && ( + { + handleDeviationAction("deny-delete", item); + handleMenuClose(`accepted-${item.id}`); + }} + > + + Deny - Delete Policy + + )} + { + handleDeviationAction("deny-remediate", item); + handleMenuClose(`accepted-${item.id}`); + }} + > + + Deny - Remediate to align with template + + { + handleDeviationAction("accept-customer-specific", item); + handleMenuClose(`accepted-${item.id}`); + }} + > + + Accept - Customer Specific + + + ); + })} + + {customerSpecificDeviationItemsWithActions.map((item) => { + const supportsDelete = + (item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate")) && + item.expectedValue === "This policy only exists in the tenant, not in the template."; + return ( + handleMenuClose(`customer-${item.id}`)} + > + {supportsDelete && ( + { + handleDeviationAction("deny-delete", item); + handleMenuClose(`customer-${item.id}`); + }} + > + + Deny - Delete + + )} + { + handleDeviationAction("deny-remediate", item); + handleMenuClose(`customer-${item.id}`); + }} + > + + Deny - Remediate to align with template + + { + handleDeviationAction("accept", item); + handleMenuClose(`customer-${item.id}`); + }} + > + + Accept + + + ); + })} + + {deniedDeviationItemsWithActions.map((item) => ( + handleMenuClose(`denied-${item.id}`)} + > + { + handleDeviationAction("accept", item); + handleMenuClose(`denied-${item.id}`); + }} + > + + Accept + + { + handleDeviationAction("accept-customer-specific", item); + handleMenuClose(`denied-${item.id}`); + }} + > + + Accept - Customer Specific + + + ))} + {/* Hidden ExecutiveReportButton that gets triggered programmatically */} { enabled: !!templateId && !!tenantFilter, }); + // API call to get all Intune templates for displayName lookup + const intuneTemplatesApi = ApiGetCall({ + url: "/api/ListIntuneTemplates", + queryKey: "ListIntuneTemplates", + }); + // Find the current template from standards data const currentTemplate = (standardsApi.data || []).find( (template) => template.GUID === templateId @@ -67,42 +73,67 @@ const PoliciesDeployedPage = () => { // 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; + const comparisonItem = comparisonData[comparisonKey]; + const value = comparisonItem?.Value; + // If value is true, it's deployed and compliant if (value === true) { return "Deployed"; - } else { - // Check if there's drift data for this standard to get the deviation status - const driftData = Array.isArray(driftApi.data) ? 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 - ); + } + + // Check if ExpectedValue and CurrentValue match (like drift.js does) + if (comparisonItem?.ExpectedValue && comparisonItem?.CurrentValue) { + try { + const expectedStr = JSON.stringify(comparisonItem.ExpectedValue); + const currentStr = JSON.stringify(comparisonItem.CurrentValue); + if (expectedStr === currentStr) { + return "Deployed"; + } + } catch (e) { + console.error("Error comparing values:", e); } + } + + // If value is explicitly false, it means not deployed (not a deviation) + if (value === false) { + return "Not Deployed"; + } - const deviation = driftData.find((item) => - searchKeys.some( - (key) => - item.standardName === key || - item.policyName === key || - item.standardName?.includes(key) || - item.policyName?.includes(key) - ) + // If value is null/undefined, check drift data for deviation status + const driftData = Array.isArray(driftApi.data) ? 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 ); + } - if (deviation && deviation.Status) { - return `Deviation - ${deviation.Status}`; - } + 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}`; + } + + // Only return "Deviation - New" if we have comparison data but value is null + if (comparisonItem) { return "Deviation - New"; } + + return "Not Configured"; }; // Helper function to get display name from drift data @@ -131,7 +162,20 @@ const PoliciesDeployedPage = () => { ) ); - return deviation?.standardDisplayName || null; + // If found in drift data, return the display name + if (deviation?.standardDisplayName) { + return deviation.standardDisplayName; + } + + // If not found in drift data and this is an Intune template, look it up in the Intune templates API + if (templateType === "IntuneTemplate" && templateValue && intuneTemplatesApi.data) { + const template = intuneTemplatesApi.data.find((t) => t.GUID === templateValue); + if (template?.Displayname) { + return template.Displayname; + } + } + + return null; }; // Helper function to get last refresh date @@ -195,111 +239,145 @@ const PoliciesDeployedPage = () => { (templateStandards.IntuneTemplate || []).forEach((template, index) => { console.log("Processing IntuneTemplate in policies-deployed:", template); + // Check if this template has TemplateList-Tags (try both property formats) + const templateListTags = template["TemplateList-Tags"] || template.TemplateListTags; + // Check if this template has TemplateList-Tags and expand them - if ( - template["TemplateList-Tags"]?.value && - template["TemplateList-Tags"]?.addedFields?.templates - ) { + if (templateListTags?.value && templateListTags?.addedFields?.templates) { console.log( "Found TemplateList-Tags for IntuneTemplate in policies-deployed:", - template["TemplateList-Tags"] + templateListTags ); - console.log("Templates to expand:", template["TemplateList-Tags"].addedFields.templates); + console.log("Templates to expand:", templateListTags.addedFields.templates); // Expand TemplateList-Tags into multiple template items - template["TemplateList-Tags"].addedFields.templates.forEach( - (expandedTemplate, expandedIndex) => { - console.log("Expanding IntuneTemplate in policies-deployed:", expandedTemplate); - const standardKey = `IntuneTemplate.${expandedTemplate.GUID}`; - const driftDisplayName = getDisplayNameFromDrift( - standardKey, - expandedTemplate.GUID, - "IntuneTemplate" - ); - const packageTagName = template["TemplateList-Tags"].value; - const templateName = - expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; - - intunePolices.push({ - id: intunePolices.length + 1, - name: `${driftDisplayName || templateName} (via ${packageTagName})`, - category: "Intune Template", - platform: "Multi-Platform", - status: getStatus(standardKey, expandedTemplate.GUID, "IntuneTemplate"), - lastModified: getLastRefresh(standardKey), - assignedGroups: template.AssignTo || "N/A", - templateValue: expandedTemplate.GUID, - }); - } - ); + templateListTags.addedFields.templates.forEach((expandedTemplate, expandedIndex) => { + console.log("Expanding IntuneTemplate in policies-deployed:", expandedTemplate); + const standardKey = `IntuneTemplate.${expandedTemplate.GUID}`; + const driftDisplayName = getDisplayNameFromDrift( + standardKey, + expandedTemplate.GUID, + "IntuneTemplate" + ); + const packageTagName = templateListTags.value; + const templateName = + expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; + + intunePolices.push({ + id: intunePolices.length + 1, + name: `${driftDisplayName || templateName} (via ${packageTagName})`, + category: "Intune Template", + platform: "Multi-Platform", + status: getStatus(standardKey, expandedTemplate.GUID, "IntuneTemplate"), + lastModified: getLastRefresh(standardKey), + assignedGroups: template.AssignTo || "N/A", + templateValue: expandedTemplate.GUID, + }); + }); } else { // Regular TemplateList processing - const standardKey = `IntuneTemplate.${template.TemplateList?.value}`; - const driftDisplayName = getDisplayNameFromDrift( - standardKey, - template.TemplateList?.value, - "IntuneTemplate" - ); - const templateLabel = getTemplateLabel(template.TemplateList?.value, "IntuneTemplate"); + const templateGuid = template.TemplateList?.value; + const standardKey = `IntuneTemplate.${templateGuid}`; + const driftDisplayName = getDisplayNameFromDrift(standardKey, templateGuid, "IntuneTemplate"); + + // Try multiple fallbacks for the name + let templateName = driftDisplayName; + if (!templateName) { + const templateLabel = getTemplateLabel(templateGuid, "IntuneTemplate"); + if (templateLabel !== "Unknown Template") { + templateName = `Intune - ${templateLabel}`; + } + } + // If still no name, try looking up directly in intuneTemplatesApi by GUID + if (!templateName && templateGuid && intuneTemplatesApi.data) { + const intuneTemplate = intuneTemplatesApi.data.find((t) => t.GUID === templateGuid); + if (intuneTemplate?.Displayname) { + templateName = intuneTemplate.Displayname; + } + } + // Final fallback + if (!templateName) { + templateName = `Intune - ${templateGuid || "Unknown Template"}`; + } intunePolices.push({ id: intunePolices.length + 1, - name: driftDisplayName || `Intune - ${templateLabel}`, + name: templateName, category: "Intune Template", platform: "Multi-Platform", - status: getStatus(standardKey, template.TemplateList?.value, "IntuneTemplate"), + status: getStatus(standardKey, templateGuid, "IntuneTemplate"), lastModified: getLastRefresh(standardKey), assignedGroups: template.AssignTo || "N/A", - templateValue: template.TemplateList?.value, + templateValue: templateGuid, }); } }); + // Add any templates from comparison data that weren't in template standards (e.g., from tags) + // Check for IntuneTemplate entries in comparison data + Object.keys(comparisonData).forEach((key) => { + if (key.startsWith("standards.IntuneTemplate.")) { + const guid = key.replace("standards.IntuneTemplate.", ""); + // Check if this GUID is already in our list + const alreadyExists = intunePolices.some((p) => p.templateValue === guid); + if (!alreadyExists && comparisonData[key]?.Value === true) { + const standardKey = `IntuneTemplate.${guid}`; + const driftDisplayName = getDisplayNameFromDrift(standardKey, guid, "IntuneTemplate"); + + intunePolices.push({ + id: intunePolices.length + 1, + name: driftDisplayName || `Intune - ${guid}`, + category: "Intune Template", + platform: "Multi-Platform", + status: getStatus(standardKey, guid, "IntuneTemplate"), + lastModified: getLastRefresh(standardKey), + assignedGroups: "N/A", + templateValue: guid, + }); + } + } + }); + // Process Conditional Access Templates const conditionalAccessPolicies = []; (templateStandards.ConditionalAccessTemplate || []).forEach((template, index) => { console.log("Processing ConditionalAccessTemplate in policies-deployed:", template); + // Check if this template has TemplateList-Tags (try both property formats) + const templateListTags = template["TemplateList-Tags"] || template.TemplateListTags; + // Check if this template has TemplateList-Tags and expand them - if ( - template["TemplateList-Tags"]?.value && - template["TemplateList-Tags"]?.addedFields?.templates - ) { + if (templateListTags?.value && templateListTags?.addedFields?.templates) { console.log( "Found TemplateList-Tags for ConditionalAccessTemplate in policies-deployed:", - template["TemplateList-Tags"] + templateListTags ); - console.log("Templates to expand:", template["TemplateList-Tags"].addedFields.templates); + console.log("Templates to expand:", templateListTags.addedFields.templates); // Expand TemplateList-Tags into multiple template items - template["TemplateList-Tags"].addedFields.templates.forEach( - (expandedTemplate, expandedIndex) => { - console.log( - "Expanding ConditionalAccessTemplate in policies-deployed:", - expandedTemplate - ); - const standardKey = `ConditionalAccessTemplate.${expandedTemplate.GUID}`; - const driftDisplayName = getDisplayNameFromDrift( - standardKey, - expandedTemplate.GUID, - "ConditionalAccessTemplate" - ); - const packageTagName = template["TemplateList-Tags"].value; - const templateName = - expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; - - conditionalAccessPolicies.push({ - id: conditionalAccessPolicies.length + 1, - name: `${driftDisplayName || templateName} (via ${packageTagName})`, - state: template.state || "Unknown", - conditions: "Conditional Access Policy", - controls: "Access Control", - lastModified: getLastRefresh(standardKey), - status: getStatus(standardKey, expandedTemplate.GUID, "ConditionalAccessTemplate"), - templateValue: expandedTemplate.GUID, - }); - } - ); + templateListTags.addedFields.templates.forEach((expandedTemplate, expandedIndex) => { + console.log("Expanding ConditionalAccessTemplate in policies-deployed:", expandedTemplate); + const standardKey = `ConditionalAccessTemplate.${expandedTemplate.GUID}`; + const driftDisplayName = getDisplayNameFromDrift( + standardKey, + expandedTemplate.GUID, + "ConditionalAccessTemplate" + ); + const packageTagName = templateListTags.value; + const templateName = + expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; + + conditionalAccessPolicies.push({ + id: conditionalAccessPolicies.length + 1, + name: `${driftDisplayName || templateName} (via ${packageTagName})`, + state: template.state || "Unknown", + conditions: "Conditional Access Policy", + controls: "Access Control", + lastModified: getLastRefresh(standardKey), + status: getStatus(standardKey, expandedTemplate.GUID, "ConditionalAccessTemplate"), + templateValue: expandedTemplate.GUID, + }); + }); } else { // Regular TemplateList processing const standardKey = `ConditionalAccessTemplate.${template.TemplateList?.value}`; @@ -325,6 +403,35 @@ const PoliciesDeployedPage = () => { }); } }); + + // Add any CA templates from comparison data that weren't in template standards + Object.keys(comparisonData).forEach((key) => { + if (key.startsWith("standards.ConditionalAccessTemplate.")) { + const guid = key.replace("standards.ConditionalAccessTemplate.", ""); + // Check if this GUID is already in our list + const alreadyExists = conditionalAccessPolicies.some((p) => p.templateValue === guid); + if (!alreadyExists && comparisonData[key]?.Value === true) { + const standardKey = `ConditionalAccessTemplate.${guid}`; + const driftDisplayName = getDisplayNameFromDrift( + standardKey, + guid, + "ConditionalAccessTemplate" + ); + + conditionalAccessPolicies.push({ + id: conditionalAccessPolicies.length + 1, + name: driftDisplayName || `Conditional Access - ${guid}`, + state: "Unknown", + conditions: "Conditional Access Policy", + controls: "Access Control", + lastModified: getLastRefresh(standardKey), + status: getStatus(standardKey, guid, "ConditionalAccessTemplate"), + templateValue: guid, + }); + } + } + }); + // Simple filter for all templates (no type filtering) const templateOptions = standardsApi.data ? standardsApi.data.map((template) => ({ @@ -363,40 +470,7 @@ const PoliciesDeployedPage = () => { currentTenant, }); const title = "View Deployed Policies"; - const subtitle = [ - { - icon: , - text: ( - { - const query = { ...router.query }; - if (selectedTemplate && selectedTemplate.value) { - query.templateId = selectedTemplate.value; - } else { - delete query.templateId; - } - router.replace( - { - pathname: router.pathname, - query: query, - }, - undefined, - { shallow: true } - ); - }} - sx={{ minWidth: 300 }} - placeholder="Select a template..." - /> - ), - }, - ]; + const subtitle = []; return ( { > - + {/* Filters Section */} + + { + const query = { ...router.query }; + if (selectedTemplate && selectedTemplate.value) { + query.templateId = selectedTemplate.value; + } else { + delete query.templateId; + } + router.replace( + { + pathname: router.pathname, + query: query, + }, + undefined, + { shallow: true } + ); + }} + sx={{ width: 300 }} + placeholder="Select template..." + /> + + + {/* Standards Section */} }> diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index 5a1d385a4c4c..a544ff4c5aa4 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -22,9 +22,7 @@ const Page = () => { // Use useMemo to derive userRoles directly const userRoles = useMemo(() => { if (orgData.isSuccess && orgData.data?.clientPrincipal?.userRoles) { - return orgData.data.clientPrincipal.userRoles.filter( - (role) => !blockedRoles.includes(role) - ); + return orgData.data.clientPrincipal.userRoles.filter((role) => !blockedRoles.includes(role)); } return []; }, [orgData.isSuccess, orgData.data?.clientPrincipal?.userRoles]); @@ -54,7 +52,10 @@ const Page = () => { 0 diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 36eb740bbe15..f47559d34499 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -181,9 +181,11 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr "NotBefore", "NotAfter", "latestDataCollection", + "requestDate", // App Consent Requests + "reviewedDate", // App Consent Requests ]; - const matchDateTime = /([dD]ate[tT]ime|[Ee]xpiration)/; + const matchDateTime = /([dD]ate[tT]ime|[Ee]xpiration|[Tt]imestamp)/; if (timeAgoArray.includes(cellName) || matchDateTime.test(cellName)) { return isText && canReceive === false ? ( new Date(data).toLocaleString() // This runs if canReceive is false and isText is true @@ -796,6 +798,38 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr /> ); } + if (cellName === "Status" || cellName === "Risk" || cellName === "UserImpact") { + let color = "default"; + let label = data; + + switch (data.toLowerCase()) { + case "success": + color = "success"; + break; + case "passed": + color = "success"; + break; + case "failed": + case "high": + color = "error"; + break; + case "in progress": + color = "info"; + break; + case "not started": + color = "default"; + break; + case "investigate": + case "medium": + case "warning": + case "skipped": + color = "warning"; + break; + default: + color = "default"; + } + return isText ? label : ; + } // ISO 8601 Duration Formatting // Add property names here to automatically format ISO 8601 duration strings (e.g., "PT1H23M30S") diff --git a/yarn.lock b/yarn.lock index e1d176d44d7a..5e07ec26f491 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2466,13 +2466,6 @@ "@types/linkify-it" "^5" "@types/mdurl" "^2" -"@types/mdast@^3.0.0": - version "3.0.15" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" - integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== - dependencies: - "@types/unist" "^2" - "@types/mdast@^4.0.0": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" @@ -3678,11 +3671,6 @@ dfa@^1.2.0: resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q== -diff@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -4929,11 +4917,6 @@ is-boolean-object@^1.2.1: call-bound "^1.0.3" has-tostringtag "^1.0.2" -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - is-bun-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" @@ -5273,11 +5256,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -kleur@^4.0.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" @@ -5436,33 +5414,15 @@ math-intrinsics@^1.1.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -mdast-util-find-and-replace@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" - integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== +mdast-util-find-and-replace@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df" + integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" escape-string-regexp "^5.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.0.0" - -mdast-util-from-markdown@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" - integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - decode-named-character-reference "^1.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" mdast-util-from-markdown@^2.0.0: version "2.0.2" @@ -5482,63 +5442,70 @@ mdast-util-from-markdown@^2.0.0: micromark-util-types "^2.0.0" unist-util-stringify-position "^4.0.0" -mdast-util-gfm-autolink-literal@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" - integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== +mdast-util-gfm-autolink-literal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz#abd557630337bd30a6d5a4bd8252e1c2dc0875d5" + integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" ccount "^2.0.0" - mdast-util-find-and-replace "^2.0.0" - micromark-util-character "^1.0.0" + devlop "^1.0.0" + mdast-util-find-and-replace "^3.0.0" + micromark-util-character "^2.0.0" -mdast-util-gfm-footnote@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" - integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== +mdast-util-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz#7778e9d9ca3df7238cc2bd3fa2b1bf6a65b19403" + integrity sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - micromark-util-normalize-identifier "^1.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" -mdast-util-gfm-strikethrough@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" - integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== +mdast-util-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" + integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" - integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== +mdast-util-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" + integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" markdown-table "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.3.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm-task-list-item@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" - integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== +mdast-util-gfm-task-list-item@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" + integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" - integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== +mdast-util-gfm@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz#2cdf63b92c2a331406b0fb0db4c077c1b0331751" + integrity sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ== dependencies: - mdast-util-from-markdown "^1.0.0" - mdast-util-gfm-autolink-literal "^1.0.0" - mdast-util-gfm-footnote "^1.0.0" - mdast-util-gfm-strikethrough "^1.0.0" - mdast-util-gfm-table "^1.0.0" - mdast-util-gfm-task-list-item "^1.0.0" - mdast-util-to-markdown "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-gfm-autolink-literal "^2.0.0" + mdast-util-gfm-footnote "^2.0.0" + mdast-util-gfm-strikethrough "^2.0.0" + mdast-util-gfm-table "^2.0.0" + mdast-util-gfm-task-list-item "^2.0.0" + mdast-util-to-markdown "^2.0.0" mdast-util-mdx-expression@^2.0.0: version "2.0.1" @@ -5582,14 +5549,6 @@ mdast-util-mdxjs-esm@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" -mdast-util-phrasing@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" - integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== - dependencies: - "@types/mdast" "^3.0.0" - unist-util-is "^5.0.0" - mdast-util-phrasing@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" @@ -5613,20 +5572,6 @@ mdast-util-to-hast@^13.0.0: unist-util-visit "^5.0.0" vfile "^6.0.0" -mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" - integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^3.0.0" - mdast-util-to-string "^3.0.0" - micromark-util-decode-string "^1.0.0" - unist-util-visit "^4.0.0" - zwitch "^2.0.0" - mdast-util-to-markdown@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" @@ -5642,13 +5587,6 @@ mdast-util-to-markdown@^2.0.0: unist-util-visit "^5.0.0" zwitch "^2.0.0" -mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" - integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" @@ -5691,28 +5629,6 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" - integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-factory-destination "^1.0.0" - micromark-factory-label "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-factory-title "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-html-tag-name "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - micromark-core-commonmark@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" @@ -5735,93 +5651,84 @@ micromark-core-commonmark@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-extension-gfm-autolink-literal@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" - integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== +micromark-extension-gfm-autolink-literal@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz#6286aee9686c4462c1e3552a9d505feddceeb935" + integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== dependencies: - micromark-util-character "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-footnote@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" - integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== - dependencies: - micromark-core-commonmark "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-strikethrough@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" - integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== +micromark-extension-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz#4dab56d4e398b9853f6fe4efac4fc9361f3e0750" + integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" - integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== +micromark-extension-gfm-strikethrough@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz#86106df8b3a692b5f6a92280d3879be6be46d923" + integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw== dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-tagfilter@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" - integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== +micromark-extension-gfm-table@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz#fac70bcbf51fe65f5f44033118d39be8a9b5940b" + integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg== dependencies: - micromark-util-types "^1.0.0" + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-task-list-item@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" - integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== +micromark-extension-gfm-tagfilter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" + integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" - integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== - dependencies: - micromark-extension-gfm-autolink-literal "^1.0.0" - micromark-extension-gfm-footnote "^1.0.0" - micromark-extension-gfm-strikethrough "^1.0.0" - micromark-extension-gfm-table "^1.0.0" - micromark-extension-gfm-tagfilter "^1.0.0" - micromark-extension-gfm-task-list-item "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-destination@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" - integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== +micromark-extension-gfm-task-list-item@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz#bcc34d805639829990ec175c3eea12bb5b781f2c" + integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw== dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" + integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== + dependencies: + micromark-extension-gfm-autolink-literal "^2.0.0" + micromark-extension-gfm-footnote "^2.0.0" + micromark-extension-gfm-strikethrough "^2.0.0" + micromark-extension-gfm-table "^2.0.0" + micromark-extension-gfm-tagfilter "^2.0.0" + micromark-extension-gfm-task-list-item "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" micromark-factory-destination@^2.0.0: version "2.0.1" @@ -5832,16 +5739,6 @@ micromark-factory-destination@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-label@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" - integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - micromark-factory-label@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" @@ -5852,14 +5749,6 @@ micromark-factory-label@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - micromark-factory-space@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" @@ -5868,16 +5757,6 @@ micromark-factory-space@^2.0.0: micromark-util-character "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-title@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" - integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-factory-title@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" @@ -5888,16 +5767,6 @@ micromark-factory-title@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-whitespace@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" - integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-factory-whitespace@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" @@ -5908,14 +5777,6 @@ micromark-factory-whitespace@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-character@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-character@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" @@ -5924,13 +5785,6 @@ micromark-util-character@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-chunked@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" - integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-chunked@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" @@ -5938,15 +5792,6 @@ micromark-util-chunked@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" -micromark-util-classify-character@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" - integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-classify-character@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" @@ -5956,14 +5801,6 @@ micromark-util-classify-character@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-combine-extensions@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" - integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-combine-extensions@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" @@ -5972,13 +5809,6 @@ micromark-util-combine-extensions@^2.0.0: micromark-util-chunked "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" - integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-decode-numeric-character-reference@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" @@ -5986,16 +5816,6 @@ micromark-util-decode-numeric-character-reference@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" -micromark-util-decode-string@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" - integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-decode-string@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" @@ -6006,33 +5826,16 @@ micromark-util-decode-string@^2.0.0: micromark-util-decode-numeric-character-reference "^2.0.0" micromark-util-symbol "^2.0.0" -micromark-util-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" - integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== - micromark-util-encode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== -micromark-util-html-tag-name@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" - integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== - micromark-util-html-tag-name@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== -micromark-util-normalize-identifier@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" - integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-normalize-identifier@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" @@ -6040,13 +5843,6 @@ micromark-util-normalize-identifier@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" -micromark-util-resolve-all@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" - integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== - dependencies: - micromark-util-types "^1.0.0" - micromark-util-resolve-all@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" @@ -6054,15 +5850,6 @@ micromark-util-resolve-all@^2.0.0: dependencies: micromark-util-types "^2.0.0" -micromark-util-sanitize-uri@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" - integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-sanitize-uri@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" @@ -6072,16 +5859,6 @@ micromark-util-sanitize-uri@^2.0.0: micromark-util-encode "^2.0.0" micromark-util-symbol "^2.0.0" -micromark-util-subtokenize@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" - integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - micromark-util-subtokenize@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" @@ -6092,49 +5869,16 @@ micromark-util-subtokenize@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-symbol@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== - micromark-util-symbol@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== - micromark-util-types@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== -micromark@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" - integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.0" - micromark-core-commonmark "^1.0.1" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - micromark@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" @@ -6204,11 +5948,6 @@ monaco-editor@^0.53.0: dependencies: "@types/trusted-types" "^1.0.6" -mri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -7247,15 +6986,17 @@ relative-time-format@^1.1.7: resolved "https://registry.yarnpkg.com/relative-time-format/-/relative-time-format-1.1.11.tgz#b193d5192434e7c1c6a53e362f811c68a4f18c45" integrity sha512-TH+oV/w77hjaB9xCzoFYJ/Icmr/12+02IAoCI/YGS2UBTbjCbBjHGEBxGnVy4EJvOR1qadGzyFRI6hGaJJG93Q== -remark-gfm@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" - integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== +remark-gfm@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b" + integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-gfm "^2.0.0" - micromark-extension-gfm "^2.0.0" - unified "^10.0.0" + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" remark-parse@^11.0.0: version "11.0.0" @@ -7278,6 +7019,15 @@ remark-rehype@^11.0.0: unified "^11.0.0" vfile "^6.0.0" +remark-stringify@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" + integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-to-markdown "^2.0.0" + unified "^11.0.0" + remove-accents@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" @@ -7353,13 +7103,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -sade@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" - safe-array-concat@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" @@ -8027,19 +7770,6 @@ unicode-trie@^2.0.0: pako "^0.2.5" tiny-inflate "^1.0.0" -unified@^10.0.0: - version "10.1.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" - integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== - dependencies: - "@types/unist" "^2.0.0" - bail "^2.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^4.0.0" - trough "^2.0.0" - vfile "^5.0.0" - unified@^11.0.0: version "11.0.5" resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" @@ -8053,13 +7783,6 @@ unified@^11.0.0: trough "^2.0.0" vfile "^6.0.0" -unist-util-is@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" - integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.1.tgz#d0a3f86f2dd0db7acd7d8c2478080b5c67f9c6a9" @@ -8074,13 +7797,6 @@ unist-util-position@^5.0.0: dependencies: "@types/unist" "^3.0.0" -unist-util-stringify-position@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" - integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" @@ -8088,14 +7804,6 @@ unist-util-stringify-position@^4.0.0: dependencies: "@types/unist" "^3.0.0" -unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" - integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz#777df7fb98652ce16b4b7cd999d0a1a40efa3a02" @@ -8104,15 +7812,6 @@ unist-util-visit-parents@^6.0.0: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" -unist-util-visit@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" - integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.1.1" - unist-util-visit@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" @@ -8191,16 +7890,6 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" -uvu@^0.5.0: - version "0.5.6" - resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" - integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - vfile-location@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" @@ -8209,14 +7898,6 @@ vfile-location@^5.0.0: "@types/unist" "^3.0.0" vfile "^6.0.0" -vfile-message@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" - integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" @@ -8225,16 +7906,6 @@ vfile-message@^4.0.0: "@types/unist" "^3.0.0" unist-util-stringify-position "^4.0.0" -vfile@^5.0.0: - version "5.3.7" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" - integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - vfile@^6.0.0: version "6.0.3" resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab"