From 38a305098b49c384d506508f49d233e313c92699 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 11 Oct 2024 09:45:21 -0400 Subject: [PATCH 1/7] handle json conversion issues in BPA --- .../Tenant/Standards/Invoke-ListBPA.ps1 | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 index d597a8d6bb87..e583908da1e1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 @@ -40,8 +40,11 @@ Function Invoke-ListBPA { $row = $_ $JSONFields | ForEach-Object { $jsonContent = $row.$_ - if ($jsonContent -ne $null -and $jsonContent -ne 'FAILED') { - $row.$_ = $jsonContent | ConvertFrom-Json -Depth 15 + if (![string]::IsNullOrEmpty($jsonContent) -and $jsonContent -ne 'FAILED') { + try { + $row.$_ = $jsonContent | ConvertFrom-Json -Depth 15 + } catch { + } } } $row.PSObject.Properties | ForEach-Object { @@ -61,8 +64,11 @@ Function Invoke-ListBPA { $row = $_ $JSONFields | ForEach-Object { $jsonContent = $row.$_ - if ($jsonContent -ne $null -and $jsonContent -ne 'FAILED') { - $row.$_ = $jsonContent | ConvertFrom-Json -Depth 15 + if (![string]::IsNullOrEmpty($jsonContent) -and $jsonContent -ne 'FAILED') { + try { + $row.$_ = $jsonContent | ConvertFrom-Json -Depth 15 + } catch { + } } } $row | Where-Object -Property PartitionKey -In $Tenants.customerId From 44fb679b1124589ee1a16dff64fef3cfb1f43cf4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 11 Oct 2024 11:00:17 -0400 Subject: [PATCH 2/7] NinjaOne API validation & fixes Update standards.json file Add hostname check to ExecExtensionsConfig Add hostname check to NinjaOneTenantSync --- Config/standards.json | 350 ++++++++++++++++-- .../Invoke-ExecExtensionsConfig.ps1 | 14 +- .../NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 149 ++++---- 3 files changed, 412 insertions(+), 101 deletions(-) diff --git a/Config/standards.json b/Config/standards.json index da8c98b3a014..4b12a7181b10 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -93,7 +93,7 @@ "value": "default" }, { - "label": "Parial-screen background", + "label": "Partial-screen background", "value": "verticalSplit" } ] @@ -349,7 +349,7 @@ "name": "standards.TAP", "cat": "Entra (AAD) Standards", "tag": ["lowimpact"], - "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select is a TAP is single use or multi-logon.", + "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.", "addedComponent": [ { @@ -584,7 +584,7 @@ { "name": "standards.OauthConsentLowSec", "cat": "Entra (AAD) Standards", - "tag": ["mediumimpact"], + "tag": ["mediumimpact", "IntegratedApps"], "helpText": "Sets the default oauth consent level so users can consent to applications that have low risks.", "docsDescription": "Allows users to consent to applications with low assigned risk.", "label": "Allow users to consent to applications with low security risk (Prevent OAuth phishing. Lower impact, less secure)", @@ -648,7 +648,7 @@ "name": "standards.DisableEmail", "cat": "Entra (AAD) Standards", "tag": ["highimpact"], - "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead promts them to create a Microsoft account.", + "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.", "addedComponent": [], "label": "Disables Email as an MFA method", "impact": "High Impact", @@ -1278,6 +1278,19 @@ "powershellEquivalent": "Get-Mailbox & Update-MgUser", "recommendedBy": ["CIS"] }, + { + "name": "standards.EXODisableAutoForwarding", + "cat": "Exchange Standards", + "tag": ["highimpact", "CIS", "mdo_autoforwardingmode", "mdo_blockmailforward"], + "helpText": "Disables the ability for users to automatically forward e-mails to external recipients.", + "docsDescription": "Disables the ability for users to automatically forward e-mails to external recipients. This is to prevent data exfiltration. Please check if there are any legitimate use cases for this feature before implementing, like forwarding invoices and such.", + "addedComponent": [], + "label": "Disable automatic forwarding to external recipients", + "impact": "High Impact", + "impactColour": "danger", + "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy -AutoForwardingMode 'Off'", + "recommendedBy": ["CIS"] + }, { "name": "standards.QuarantineRequestAlert", "cat": "Defender Standards", @@ -1300,12 +1313,7 @@ { "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", - "tag": [ - "lowimpact", - "CIS", - "mdo_safelinksforemail", - "mdo_safelinksforOfficeApps" - ], + "tag": ["lowimpact", "CIS", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps"], "helpText": "This creates a safelink policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", "addedComponent": [ { @@ -1341,7 +1349,8 @@ "mdo_highconfidencephishaction", "mdo_phisspamacation", "mdo_spam_notifications_only_for_admins", - "mdo_antiphishingpolicies" + "mdo_antiphishingpolicies", + "mdo_phishthresholdlevel" ], "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mailtips.", "addedComponent": [ @@ -1619,13 +1628,7 @@ { "name": "standards.MalwareFilterPolicy", "cat": "Defender Standards", - "tag": [ - "lowimpact", - "CIS", - "mdo_zapspam", - "mdo_zapphish", - "mdo_zapmalware" - ], + "tag": ["lowimpact", "CIS", "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware"], "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", "addedComponent": [ { @@ -1643,6 +1646,11 @@ } ] }, + { + "type": "input", + "name": "standards.MalwareFilterPolicy.OptionalFileTypes", + "label": "Optional File Types, Comma separated" + }, { "type": "Select", "label": "QuarantineTag", @@ -1695,18 +1703,24 @@ "tag": ["mediumimpact"], "helpText": "This standard creates a Spam filter policy similar to the default strict policy.", "addedComponent": [ + { + "type": "number", + "label": "Bulk email threshold (Default 7)", + "name": "standards.SpamFilterPolicy.BulkThreshold", + "default": 7 + }, { "type": "Select", "label": "Spam Action", "name": "standards.SpamFilterPolicy.SpamAction", "values": [ - { - "label": "Move message to Junk Email folder", - "value": "MoveToJmf" - }, { "label": "Quarantine the message", "value": "Quarantine" + }, + { + "label": "Move message to Junk Email folder", + "value": "MoveToJmf" } ] }, @@ -1729,6 +1743,21 @@ } ] }, + { + "type": "Select", + "label": "High Confidence Spam Action", + "name": "standards.SpamFilterPolicy.HighConfidenceSpamAction", + "values": [ + { + "label": "Quarantine the message", + "value": "Quarantine" + }, + { + "label": "Move message to Junk Email folder", + "value": "MoveToJmf" + } + ] + }, { "type": "Select", "label": "High Confidence Spam Quarantine Tag", @@ -1748,6 +1777,21 @@ } ] }, + { + "type": "Select", + "label": "Bulk Spam Action", + "name": "standards.SpamFilterPolicy.BulkSpamAction", + "values": [ + { + "label": "Quarantine the message", + "value": "Quarantine" + }, + { + "label": "Move message to Junk Email folder", + "value": "MoveToJmf" + } + ] + }, { "type": "Select", "label": "Bulk Quarantine Tag", @@ -1767,6 +1811,21 @@ } ] }, + { + "type": "Select", + "label": "Phish Spam Action", + "name": "standards.SpamFilterPolicy.PhishSpamAction", + "values": [ + { + "label": "Quarantine the message", + "value": "Quarantine" + }, + { + "label": "Move message to Junk Email folder", + "value": "MoveToJmf" + } + ] + }, { "type": "Select", "label": "Phish Quarantine Tag", @@ -1926,14 +1985,22 @@ "name": "standards.DeletedUserRentention", "cat": "SharePoint Standards", "tag": ["lowimpact"], - "helpText": "Sets the retention period for deleted users OneDrive to the specified number of years. The default is 1 year.", - "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected time in years and data can be retrieved from it.", + "helpText": "Sets the retention period for deleted users OneDrive to the specified period of time. The default is 30 days.", + "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected amount of time that data can be retrieved from it.", "addedComponent": [ { "type": "Select", "name": "standards.DeletedUserRentention.Days", - "label": "Retention in years (Default 1)", + "label": "Retention time (Default 30 days)", "values": [ + { + "label": "30 days", + "value": "30" + }, + { + "label": "90 days", + "value": "90" + }, { "label": "1 year", "value": "365" @@ -2089,23 +2156,62 @@ "name": "standards.DisableAddShortcutsToOneDrive", "cat": "SharePoint Standards", "tag": ["mediumimpact"], - "helpText": "When the feature is disabled the option Add shortcut to OneDrive will be removed. Any folders that have already been added will remain on the user's computer.", - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, - "addedComponent": [], - "label": "Disable Add Shortcuts To OneDrive", + "helpText": "If disabled, the button Add shortcut to OneDrive will be removed and users in the tenant will no longer be able to add new shortcuts to their OneDrive. Existing shortcuts will remain functional", + "addedComponent": [ + { + "type": "Select", + "label": "Add Shortcuts To OneDrive button state", + "name": "standards.DisableAddShortcutsToOneDrive.state", + "values": [ + { + "label": "Disabled", + "value": "true" + }, + { + "label": "Enabled", + "value": "false" + } + ] + } + ], + "label": "Set Add Shortcuts To OneDrive button state", "impact": "Medium Impact", "impactColour": "warning", - "powershellEquivalent": "Graph API or Portal", + "powershellEquivalent": "Set-SPOTenant -DisableAddShortcutsToOneDrive $true or $false", + "recommendedBy": [] + }, + { + "name": "standards.SPSyncButtonState", + "cat": "SharePoint Standards", + "tag": ["mediumimpact"], + "helpText": "If disabled, users in the tenant will no longer be able to use the Sync button to sync SharePoint content on all sites. However, existing synced content will remain functional on the user's computer.", + "addedComponent": [ + { + "type": "Select", + "label": "SharePoint Sync Button state", + "name": "standards.SPSyncButtonState.state", + "values": [ + { + "label": "Disabled", + "value": "true" + }, + { + "label": "Enabled", + "value": "false" + } + ] + } + ], + "label": "Set SharePoint sync button state", + "impact": "Medium Impact", + "impactColour": "warning", + "powershellEquivalent": "Set-SPOTenant -HideSyncButtonOnTeamSite $true or $false", "recommendedBy": [] }, { "name": "standards.DisableSharePointLegacyAuth", "cat": "SharePoint Standards", - "tag": ["mediumimpact", "CIS"], + "tag": ["mediumimpact", "CIS", "spo_legacy_auth"], "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.", "addedComponent": [], @@ -2255,5 +2361,179 @@ "impactColour": "danger", "powershellEquivalent": "Update-MgAdminSharepointSetting", "recommendedBy": [] + }, + { + "name": "standards.TeamsGlobalMeetingPolicy", + "cat": "Teams Standards", + "tag": ["lowimpact"], + "helpText": "Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl", + "addedComponent": [ + { + "type": "Select", + "name": "standards.TeamsGlobalMeetingPolicy.DesignatedPresenterRoleMode", + "label": "Default value of the `Who can present?`", + "values": [ + { + "label": "EveryoneUserOverride", + "value": "EveryoneUserOverride" + }, + { + "label": "EveryoneInCompanyUserOverride", + "value": "EveryoneInCompanyUserOverride" + }, + { + "label": "EveryoneInSameAndFederatedCompanyUserOverride", + "value": "EveryoneInSameAndFederatedCompanyUserOverride" + }, + { + "label": "OrganizerOnlyUserOverride", + "value": "OrganizerOnlyUserOverride" + } + ] + } + ], + "label": "Define Global Meeting Policy for Teams", + "impact": "Low Impact", + "impactColour": "info", + "powershellEquivalent": "Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting $false -AllowAnonymousUsersToStartMeeting $false -AutoAdmittedUsers EveryoneInCompanyExcludingGuests -AllowPSTNUsersToBypassLobby $false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode $DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl $false", + "recommendedBy": ["CIS 3.0"] + }, + { + "name": "standards.TeamsEmailIntegration", + "cat": "Teams Standards", + "tag": ["lowimpact"], + "helpText": "Should users be allowed to send emails directly to a channel email addresses?", + "docsDescription": "Teams channel email addresses are an optional feature that allows users to email the Teams channel directly.", + "addedComponent": [ + { + "type": "boolean", + "name": "standards.TeamsEmailIntegration.AllowEmailIntoChannel", + "label": "Allow channel emails" + } + ], + "label": "Disallow emails to be sent to channel email addresses", + "impact": "Low Impact", + "impactColour": "info", + "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false", + "recommendedBy": ["CIS 3.0"] + }, + { + "name": "standards.TeamsExternalFileSharing", + "cat": "Teams Standards", + "tag": ["lowimpact"], + "helpText": "Ensure external file sharing in Teams is enabled for only approved cloud storage services.", + "addedComponent": [ + { + "type": "boolean", + "name": "standards.TeamsExternalFileSharing.AllowGoogleDrive", + "label": "Allow Google Drive" + }, + { + "type": "boolean", + "name": "standards.TeamsExternalFileSharing.AllowShareFile", + "label": "Allow ShareFile" + }, + { + "type": "boolean", + "name": "standards.TeamsExternalFileSharing.AllowBox", + "label": "Allow Box" + }, + { + "type": "boolean", + "name": "standards.TeamsExternalFileSharing.AllowDropBox", + "label": "Allow Dropbox" + }, + { + "type": "boolean", + "name": "standards.TeamsExternalFileSharing.AllowEgnyte", + "label": "Allow Egnyte" + } + ], + "label": "Define approved cloud storage services for external file sharing in Teams", + "impact": "Low Impact", + "impactColour": "info", + "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowGoogleDrive $false -AllowShareFile $false -AllowBox $false -AllowDropBox $false -AllowEgnyte $false", + "recommendedBy": ["CIS 3.0"] + }, + { + "name": "standards.TeamsExternalAccessPolicy", + "cat": "Teams Standards", + "tag": ["mediumimpact"], + "helpText": "Sets the properties of the Global external access policy.", + "docsDescription": "Sets the properties of the Global external access policy. External access policies determine whether or not your users can: 1) communicate with users who have Session Initiation Protocol (SIP) accounts with a federated organization; 2) communicate with users who are using custom applications built with Azure Communication Services; 3) access Skype for Business Server over the Internet, without having to log on to your internal network; 4) communicate with users who have SIP accounts with a public instant messaging (IM) provider such as Skype; and, 5) communicate with people who are using Teams with an account that's not managed by an organization.", + "addedComponent": [ + { + "type": "boolean", + "name": "standards.TeamsExternalAccessPolicy.EnableFederationAccess", + "label": "Allow communication from trusted organizations" + }, + { + "type": "boolean", + "name": "standards.TeamsExternalAccessPolicy.EnablePublicCloudAccess", + "label": "Allow user to communicate with Skype users" + }, + { + "type": "boolean", + "name": "standards.TeamsExternalAccessPolicy.EnableTeamsConsumerAccess", + "label": "Allow communication with unmanaged Teams accounts" + } + ], + "label": "External Access Settings for Microsoft Teams", + "impact": "Medium Impact", + "impactColour": "warning", + "powershellEquivalent": "Set-CsExternalAccessPolicy", + "recommendedBy": [] + }, + { + "name": "standards.TeamsFederationConfiguration", + "cat": "Teams Standards", + "tag": ["mediumimpact"], + "helpText": "Sets the properties of the Global federation configuration.", + "docsDescription": "Sets the properties of the Global federation configuration. Federation configuration settings determine whether or not your users can communicate with users who have SIP accounts with a federated organization.", + "addedComponent": [ + { + "type": "boolean", + "name": "standards.TeamsFederationConfiguration.AllowTeamsConsumer", + "label": "Allow users to communicate with other organizations" + }, + { + "type": "boolean", + "name": "standards.TeamsFederationConfiguration.AllowPublicUsers", + "label": "Allow users to communicate with Skype Users" + }, + { + "type": "Select", + "name": "standards.TeamsFederationConfiguration.DomainControl", + "label": "Communication Mode", + "values": [ + { + "label": "Allow all external domains", + "value": "AllowAllExternal" + }, + { + "label": "Block all external domains", + "value": "BlockAllExternal" + }, + { + "label": "Allow specific external domains", + "value": "AllowSpecificExternal" + }, + { + "label": "Block specific external domains", + "value": "BlockSpecificExternal" + } + ] + }, + { + "type": "input", + "name": "standards.TeamsFederationConfiguration.DomainList", + "label": "Domains, Comma separated" + } + ], + "label": "Federation Configuration for Microsoft Teams", + "impact": "Medium Impact", + "impactColour": "warning", + "powershellEquivalent": "Set-CsTenantFederationConfiguration", + "recommendedBy": [] } ] diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionsConfig.ps1 index ea6f4f16205d..9291fc5ea880 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionsConfig.ps1 @@ -31,8 +31,20 @@ Function Invoke-ExecExtensionsConfig { # Check if NinjaOne URL is set correctly and the instance has at least version 5.6 if ($Body.NinjaOne) { + $AllowedNinjaHostnames = @( + 'app.ninjarmmm.com', + 'eu.ninjarmmm.com', + 'oc.ninjarmmm.com', + 'ca.ninjarmmm.com', + 'us2.ninjarmm.com' + ) + $SetNinjaHostname = $Body.NinjaOne.Instance -replace '/ws', '' -replace 'https://', '' + if ($AllowedNinjaHostnames -notcontains $SetNinjaHostname) { + throw "NinjaOne URL is not allowed. Allowed hostnames are: $($AllowedNinjaHostnames -join ', ')" + } + try { - [version]$Version = (Invoke-WebRequest -Method GET -Uri "https://$(($Body.NinjaOne.Instance -replace '/ws','') -replace 'https://','')/app-version.txt" -ea stop).content + [version]$Version = (Invoke-WebRequest -Method GET -Uri "$SetNinjaHostname/app-version.txt" -ea stop).content } catch { throw "Failed to connect to NinjaOne check your Instance is set correctly eg 'app.ninjarmmm.com'" } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 03d1bed97445..7ba7ee9f6b40 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -5,7 +5,7 @@ function Invoke-NinjaOneTenantSync { ) try { $StartQueueTime = Get-Date - Write-Host "$(Get-Date) - Starting NinjaOne Sync" + Write-Information "$(Get-Date) - Starting NinjaOne Sync" # Stagger start # Check Global Rate Limiting @@ -22,7 +22,7 @@ function Invoke-NinjaOneTenantSync { $StartDate = try { Get-Date($CurrentItem.lastStartTime) } catch { $Null } $EndDate = try { Get-Date($CurrentItem.lastEndTime) } catch { $Null } - if (($null -ne $CurrentItem.lastStartTime) -and ($StartDate -gt (Get-Date).AddMinutes(-10)) -and ( $Null -eq $CurrentItem.lastEndTime -or ($StartDate -gt $EndDate))) { + if (($null -ne $CurrentItem.lastStartTime) -and ($StartDate -gt (Get-Date).ToUniversalTime().AddMinutes(-10)) -and ( $Null -eq $CurrentItem.lastEndTime -or ($StartDate -gt $EndDate))) { Throw "NinjaOne Sync for Tenant $($MappedTenant.RowKey) is still running, please wait 10 minutes and try again." } @@ -43,7 +43,7 @@ function Invoke-NinjaOneTenantSync { $Customer = Get-Tenants -IncludeErrors | Where-Object { $_.customerId -eq $MappedTenant.RowKey } - Write-Host "Processing: $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" + Write-Information "Processing: $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Processing NinjaOne Synchronization for $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" -Sev 'Info' @@ -59,6 +59,18 @@ function Invoke-NinjaOneTenantSync { $Table = Get-CIPPTable -TableName Extensionsconfig $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json).NinjaOne + $AllowedNinjaHostnames = @( + 'app.ninjarmmm.com', + 'eu.ninjarmmm.com', + 'oc.ninjarmmm.com', + 'ca.ninjarmmm.com', + 'us2.ninjarmm.com' + ) + + if ($AllowedNinjaHostnames -notcontains $Configuration.Instance) { + throw "NinjaOne URL is invalid. Allowed hostnames are: $($AllowedNinjaHostnames -join ', ')" + } + # Pull the list of field Mappings so we know which fields to render. $MappedFields = [pscustomobject]@{} $CIPPMapping = Get-CIPPTable -TableName CippMapping @@ -79,7 +91,7 @@ function Invoke-NinjaOneTenantSync { } while ($ResultCount.count -eq $PageSize) - Write-Host 'Fetched NinjaOne Devices' + Write-Information 'Fetched NinjaOne Devices' [System.Collections.Generic.List[PSCustomObject]]$NinjaOneUserDocs = @() @@ -183,7 +195,7 @@ function Invoke-NinjaOneTenantSync { $NinjaDoc | Add-Member -NotePropertyName 'ParsedFields' -NotePropertyValue $ParsedFields -Force } - Write-Host 'Fetched NinjaOne User Docs' + Write-Information 'Fetched NinjaOne User Docs' } [System.Collections.Generic.List[PSCustomObject]]$NinjaOneLicenseDocs = @() @@ -253,7 +265,7 @@ function Invoke-NinjaOneTenantSync { $NinjaLic | Add-Member -NotePropertyName 'ParsedFields' -NotePropertyValue $ParsedFields -Force } - Write-Host 'Fetched NinjaOne License Docs' + Write-Information 'Fetched NinjaOne License Docs' } @@ -339,7 +351,7 @@ function Invoke-NinjaOneTenantSync { Throw "Failed to fetch bulk company data: $_" } - Write-Host 'Fetched Bulk M365 Data' + Write-Information 'Fetched Bulk M365 Data' $Users = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Users' @@ -399,7 +411,7 @@ function Invoke-NinjaOneTenantSync { $MemberReturn = $null } - Write-Host 'Fetched M365 Roles' + Write-Information 'Fetched M365 Roles' $Roles = foreach ($Result in $MemberReturn) { [PSCustomObject]@{ @@ -457,7 +469,7 @@ function Invoke-NinjaOneTenantSync { $PolicyReturn = $null } - Write-Host 'Fetched M365 Device Compliance' + Write-Information 'Fetched M365 Device Compliance' $DeviceComplianceDetails = foreach ($Result in $PolicyReturn) { [pscustomobject]@{ @@ -487,7 +499,7 @@ function Invoke-NinjaOneTenantSync { $GroupMembersReturn = $null } - Write-Host 'Fetched M365 Group Membership' + Write-Information 'Fetched M365 Group Membership' $Groups = foreach ($Result in $GroupMembersReturn) { [pscustomobject]@{ @@ -596,7 +608,7 @@ function Invoke-NinjaOneTenantSync { $MailboxStatsFull = $null } - Write-Host 'Fetched M365 Additional Data' + Write-Information 'Fetched M365 Additional Data' $FetchEnd = Get-Date @@ -877,7 +889,7 @@ function Invoke-NinjaOneTenantSync { New-CIPPGraphSubscription -TenantFilter $TenantFilter -TypeofSubscription 'updated' -BaseURL $CIPPUrl -Resource 'devices' -EventType 'DeviceUpdate' -ExecutingUser 'NinjaOneSync' } - Write-Host 'Processed Devices' + Write-Information 'Processed Devices' ########## Create / Update User Objects @@ -1353,25 +1365,25 @@ function Invoke-NinjaOneTenantSync { try { # Create New Users if (($NinjaUserCreation | Measure-Object).count -ge 100) { - Write-Host 'Creating NinjaOne Users' + Write-Information 'Creating NinjaOne Users' [System.Collections.Generic.List[PSCustomObject]]$CreatedUsers = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ("[$($NinjaUserCreation.body -join ',')]") -EA Stop).content | ConvertFrom-Json -Depth 100 Remove-AzDataTableEntity @UsersUpdateTable -Entity $NinjaUserCreation [System.Collections.Generic.List[PSCustomObject]]$NinjaUserCreation = @() } } Catch { - Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" + Write-Information "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } try { # Update Users if (($NinjaUserUpdates | Measure-Object).count -ge 100) { - Write-Host 'Updating NinjaOne Users' + Write-Information 'Updating NinjaOne Users' [System.Collections.Generic.List[PSCustomObject]]$UpdatedUsers = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ("[$($NinjaUserUpdates.body -join ',')]") -EA Stop).content | ConvertFrom-Json -Depth 100 Remove-AzDataTableEntity @UsersUpdateTable -Entity $NinjaUserUpdates [System.Collections.Generic.List[PSCustomObject]]$NinjaUserUpdates = @() } } Catch { - Write-Host "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" + Write-Information "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" } @@ -1428,24 +1440,24 @@ function Invoke-NinjaOneTenantSync { try { # Create New Users if (($NinjaUserCreation | Measure-Object).count -ge 1) { - Write-Host 'Creating NinjaOne Users' + Write-Information 'Creating NinjaOne Users' [System.Collections.Generic.List[PSCustomObject]]$CreatedUsers = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ("[$($NinjaUserCreation.body -join ',')]") -EA Stop).content | ConvertFrom-Json -Depth 100 Remove-AzDataTableEntity @UsersUpdateTable -Entity $NinjaUserCreation } } Catch { - Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" + Write-Information "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } try { # Update Users if (($NinjaUserUpdates | Measure-Object).count -ge 1) { - Write-Host 'Updating NinjaOne Users' + Write-Information 'Updating NinjaOne Users' [System.Collections.Generic.List[PSCustomObject]]$UpdatedUsers = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ("[$($NinjaUserUpdates.body -join ',')]") -EA Stop).content | ConvertFrom-Json -Depth 100 Remove-AzDataTableEntity @UsersUpdateTable -Entity $NinjaUserUpdates } } Catch { - Write-Host "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" + Write-Information "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" } ### Relationship Mapping @@ -1512,12 +1524,12 @@ function Invoke-NinjaOneTenantSync { try { # Update Relations if (($Relations | Measure-Object).count -ge 1) { - Write-Host 'Updating Relations' + Write-Information 'Updating Relations' $Null = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/related-items/entity/NODE/$($LinkDevice.NinjaDevice.id)/relations" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' -Body ($Relations | ConvertTo-Json -Depth 100 -AsArray) -EA Stop - Write-Host 'Completed Update' + Write-Information 'Completed Update' } } Catch { - Write-Host "Creating Relations Failed: $_" + Write-Information "Creating Relations Failed: $_" } } } @@ -1624,22 +1636,22 @@ function Invoke-NinjaOneTenantSync { try { # Create New Subscriptions if (($NinjaLicenseCreation | Measure-Object).count -ge 1) { - Write-Host 'Creating NinjaOne Licenses' + Write-Information 'Creating NinjaOne Licenses' [System.Collections.Generic.List[PSCustomObject]]$CreatedLicenses = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaLicenseCreation | ConvertTo-Json -Depth 100 -AsArray) -EA Stop).content | ConvertFrom-Json -Depth 100 } } Catch { - Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" + Write-Information "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } try { # Update Subscriptions if (($NinjaLicenseUpdates | Measure-Object).count -ge 1) { - Write-Host 'Updating NinjaOne Licenses' + Write-Information 'Updating NinjaOne Licenses' [System.Collections.Generic.List[PSCustomObject]]$UpdatedLicenses = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaLicenseUpdates | ConvertTo-Json -Depth 100 -AsArray) -EA Stop).content | ConvertFrom-Json -Depth 100 - Write-Host 'Completed Update' + Write-Information 'Completed Update' } } Catch { - Write-Host "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" + Write-Information "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" } [System.Collections.Generic.List[PSCustomObject]]$LicenseDocs = $CreatedLicenses + $UpdatedLicenses @@ -1668,12 +1680,12 @@ function Invoke-NinjaOneTenantSync { try { # Update Relations if (($Relations | Measure-Object).count -ge 1) { - Write-Host 'Updating Relations' + Write-Information 'Updating Relations' $Null = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/related-items/entity/DOCUMENT/$($($MatchedLicDoc.documentId))/relations" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' -Body ($Relations | ConvertTo-Json -Depth 100 -AsArray) -EA Stop - Write-Host 'Completed Update' + Write-Information 'Completed Update' } } Catch { - Write-Host "Creating Relations Failed: $_" + Write-Information "Creating Relations Failed: $_" } #Remove relations @@ -1681,7 +1693,7 @@ function Invoke-NinjaOneTenantSync { try { $RelatedItems = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/related-items/$($DelUser.id)" -Method Delete -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100 } catch { - Write-Host "Failed to remove relation $($DelUser.id) from $($LinkLic.name)" + Write-Information "Failed to remove relation $($DelUser.id) from $($LinkLic.name)" } } } @@ -1696,7 +1708,7 @@ function Invoke-NinjaOneTenantSync { ### M365 Links Section if ($MappedFields.TenantLinks) { - Write-Host 'Tenant Links' + Write-Information 'Tenant Links' $ManagementLinksData = @( @{ @@ -1798,7 +1810,7 @@ function Invoke-NinjaOneTenantSync { if ($MappedFields.TenantSummary) { - Write-Host 'Tenant Summary' + Write-Information 'Tenant Summary' ### Tenant Overview Card $ParsedAdmins = [PSCustomObject]@{} @@ -1820,7 +1832,7 @@ function Invoke-NinjaOneTenantSync { $TenantSummaryCard = Get-NinjaOneInfoCard -Title 'Tenant Details' -Data $TenantDetailsItems -Icon 'fas fa-building' ### Users details card - Write-Host 'User Details' + Write-Information 'User Details' $TotalUsersCount = ($Users | Measure-Object).count $GuestUsersCount = ($Users | Where-Object { $_.UserType -eq 'Guest' } | Measure-Object).count $LicensedUsersCount = ($licensedUsers | Measure-Object).count @@ -1878,7 +1890,7 @@ function Invoke-NinjaOneTenantSync { ### Device Details Card - Write-Host 'Device Details' + Write-Information 'Device Details' $TotalDeviceswCount = ($Devices | Measure-Object).count $ComplianceDevicesCount = ($Devices | Where-Object { $_.complianceState -eq 'compliant' } | Measure-Object).count $WindowsCount = ($Devices | Where-Object { $_.operatingSystem -eq 'Windows' } | Measure-Object).count @@ -1958,7 +1970,7 @@ function Invoke-NinjaOneTenantSync { $DeviceSummaryCardHTML = Get-NinjaOneCard -Title 'Device Details' -Body $DeviceCardBodyHTML -Icon 'fas fa-network-wired' -TitleLink $TitleLink #### Secure Score Card - Write-Host 'Secure Score Details' + Write-Information 'Secure Score Details' $Top5Actions = ($SecureScoreParsed | Where-Object { $_.scoreInPercentage -ne 100 } | Sort-Object 'Score Impact', adjustedRank -Descending) | Select-Object -First 5 # Score Chart @@ -1978,7 +1990,7 @@ function Invoke-NinjaOneTenantSync { try { $SecureScoreHTML = Get-NinjaInLineBarGraph -Title "Secure Score - $([System.Math]::Round((($CurrentSecureScore.currentScore / $MaxSecureScore) * 100),2))%" -Data $Data -KeyInLine -NoCount -NoSort } catch { - $SecureScoreHTML = "No Secure Score Data Available" + $SecureScoreHTML = 'No Secure Score Data Available' } # Recommended Actions HTML @@ -1993,30 +2005,37 @@ function Invoke-NinjaOneTenantSync { ### CIPP Applied Standards Cards - Write-Host 'Applied Standards' - Set-Location (Get-Item $PSScriptRoot).Parent.Parent.Parent.FullName - $StandardsDefinitions = Get-Content 'config/standards.json' | ConvertFrom-Json -Depth 100 + Write-Information 'Applied Standards' + $ModuleBase = Get-Module CIPPExtensions | Select-Object -ExpandProperty ModuleBase + $CIPPRoot = (Get-Item $ModuleBase).Parent.Parent.FullName + Set-Location $CIPPRoot + + try { + $StandardsDefinitions = Get-Content 'config/standards.json' | ConvertFrom-Json -Depth 100 - $Table = Get-CippTable -tablename 'standards' + $Table = Get-CippTable -tablename 'standards' - $Filter = "PartitionKey eq 'standards'" + $Filter = "PartitionKey eq 'standards'" - $AllStandards = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json -Depth 100 + $AllStandards = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json -Depth 100 - $AppliedStandards = ($AllStandards | Where-Object { $_.Tenant -eq $Customer.defaultDomainName -or $_.Tenant -eq 'AllTenants' }) + $AppliedStandards = ($AllStandards | Where-Object { $_.Tenant -eq $Customer.defaultDomainName -or $_.Tenant -eq 'AllTenants' }) - $ParsedStandards = foreach ($Standard in $AppliedStandards) { - [PSCustomObject]$Standards = $Standard.Standards - $Standards.PSObject.Properties | ForEach-Object { - $CheckValue = $_ - if ($CheckValue.value) { - $MatchedStandard = $StandardsDefinitions | Where-Object { ($_.name -split 'standards.')[1] -eq $CheckValue.name } - if (($MatchedStandard | Measure-Object).count -eq 1) { - '
  • ' + $($MatchedStandard.label) + ' (' + ($($Standard.Tenant)) + ')
  • ' + $ParsedStandards = foreach ($Standard in $AppliedStandards) { + [PSCustomObject]$Standards = $Standard.Standards + $Standards.PSObject.Properties | ForEach-Object { + $CheckValue = $_ + if ($CheckValue.value) { + $MatchedStandard = $StandardsDefinitions | Where-Object { ($_.name -split 'standards.')[1] -eq $CheckValue.name } + if (($MatchedStandard | Measure-Object).count -eq 1) { + '
  • ' + $($MatchedStandard.label) + ' (' + ($($Standard.Tenant)) + ')
  • ' + } } } - } + } + } catch { + $ParsedStandards = 'No standards applied or error retrieving standards' } $TitleLink = "https://$CIPPUrl/tenant/standards/list-applied-standards?customerId=$($Customer.customerId)" @@ -2026,7 +2045,7 @@ function Invoke-NinjaOneTenantSync { $CIPPStandardsSummaryCardHTML = Get-NinjaOneCard -Title 'CIPP Applied Standards' -Body $CIPPStandardsBodyHTML -Icon 'fas fa-shield-halved' -TitleLink $TitleLink ### License Card - Write-Host 'License Details' + Write-Information 'License Details' $LicenseTableHTML = $LicensesParsed | Sort-Object 'License Name' | ConvertTo-Html -As Table -Fragment $LicenseTableHTML = '
    ' + (([System.Web.HttpUtility]::HtmlDecode($LicenseTableHTML) -replace '', '') -replace '', '') + '
    ' @@ -2035,7 +2054,7 @@ function Invoke-NinjaOneTenantSync { ### Summary Stats - Write-Host 'Widget Details' + Write-Information 'Widget Details' [System.Collections.Generic.List[PSCustomObject]]$WidgetData = @() @@ -2220,12 +2239,12 @@ function Invoke-NinjaOneTenantSync { - Write-Host 'Summary Details' + Write-Information 'Summary Details' $SummaryDetailsCardHTML = Get-NinjaOneWidgetCard -Data $WidgetData -Icon 'fas fa-building' -SmallCols 2 -MedCols 3 -LargeCols 4 -XLCols 6 -NoCard # Create the Tenant Summary Field - Write-Host 'Complete Tenant Summary' + Write-Information 'Complete Tenant Summary' $TenantSummaryHTML = '
    ' + $SummaryDetailsCardHTML + '
    ' + '
    ' + '
    ' + $TenantSummaryCard + @@ -2243,7 +2262,7 @@ function Invoke-NinjaOneTenantSync { } if ($MappedFields.UsersSummary) { - Write-Host 'User Details Section' + Write-Information 'User Details Section' $UsersTableFornatted = $ParsedUsers | Sort-Object name | Select-Object -First 100 Name, @{n = 'User Principal Name'; e = { $_.UPN } }, @@ -2281,26 +2300,26 @@ function Invoke-NinjaOneTenantSync { - Write-Host 'Posting Details' + Write-Information 'Posting Details' $Token = Get-NinjaOneToken -configuration $Configuration - Write-Host "Ninja Body: $($NinjaOrgUpdate | ConvertTo-Json -Depth 100)" + Write-Information "Ninja Body: $($NinjaOrgUpdate | ConvertTo-Json -Depth 100)" $Result = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/$($MappedTenant.IntegrationId)/custom-fields" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaOrgUpdate | ConvertTo-Json -Depth 100) - Write-Host 'Cleaning Users Cache' + Write-Information 'Cleaning Users Cache' if (($ParsedUsers | Measure-Object).count -gt 0) { Remove-AzDataTableEntity @UsersTable -Entity ($ParsedUsers | Select-Object PartitionKey, RowKey) } - Write-Host 'Cleaning Device Cache' + Write-Information 'Cleaning Device Cache' if (($ParsedDevices | Measure-Object).count -gt 0) { Remove-AzDataTableEntity @DeviceTable -Entity ($ParsedDevices | Select-Object PartitionKey, RowKey) } - Write-Host "Total Fetch Time: $((New-TimeSpan -Start $StartTime -End $FetchEnd).TotalSeconds)" - Write-Host "Completed Total Time: $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds)" + Write-Information "Total Fetch Time: $((New-TimeSpan -Start $StartTime -End $FetchEnd).TotalSeconds)" + Write-Information "Completed Total Time: $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds)" # Set Last End Time $CurrentItem | Add-Member -NotePropertyName lastEndTime -NotePropertyValue ([string]$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ'))) -Force From 28076f57e03770f620d102e11d67a745a63639db Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 11 Oct 2024 11:21:21 -0400 Subject: [PATCH 3/7] Fix complex queries --- .../Public/GraphRequests/Get-GraphRequestList.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index e5687be6900d..d32c1ff87f34 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -96,11 +96,9 @@ function Get-GraphRequestList { $Count = 0 if ($TenantFilter -ne 'AllTenants') { $GraphRequest = @{ - uri = $GraphQuery.ToString() - tenantid = $TenantFilter - } - if ($Parameters.'$filter') { - $GraphRequest.ComplexFilter = $true + uri = $GraphQuery.ToString() + tenantid = $TenantFilter + ComplexFilter = $true } if ($NoPagination.IsPresent) { $GraphRequest.noPagination = $NoPagination.IsPresent From 792895f0a18999ec456ce242499dfdfb53d13e4a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 11 Oct 2024 11:26:27 -0400 Subject: [PATCH 4/7] Update Set-CIPPAssignedPolicy.ps1 --- Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 index 9541e8f7e1d1..08f88bd167c6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 @@ -48,10 +48,10 @@ function Set-CIPPAssignedPolicy { } default { $GroupNames = $GroupName.Split(',') - $GroupIds = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter | ForEach-Object { + $GroupIds = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName&$top=999' -tenantid $TenantFilter | ForEach-Object { $Group = $_ foreach ($SingleName in $GroupNames) { - if ($_.displayname -like $SingleName) { + if ($_.displayName -like $SingleName) { $group.id } } From 3c2022d25110be71a3e740a574cb2d4798d629fd Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 11 Oct 2024 11:54:52 -0400 Subject: [PATCH 5/7] filter queries --- .../AuditLogs/Get-CippAuditLogSearches.ps1 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 index 8c2aa9e9a7ff..c55c13186f22 100644 --- a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 @@ -13,11 +13,26 @@ function Get-CippAuditLogSearches { [Parameter()] [switch]$ReadyToProcess ) - $Queries = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -AsApp $true -tenantid $TenantFilter + if ($ReadyToProcess.IsPresent) { $AuditLogSearchesTable = Get-CippTable -TableName 'AuditLogSearches' $PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "Tenant eq '$TenantFilter' and CippStatus eq 'Pending'" + + $BulkRequests = foreach ($PendingQuery in $PendingQueries) { + @{ + id = $PendingQuery.RowKey + url = 'security/auditLog/queries/' + $PendingQuery.RowKey + method = 'GET' + } + } + if ($BulkRequests.Count -eq 0) { + return @() + } + $Queries = New-GraphBulkRequest -Requests @($BulkRequests) -AsApp $true -TenantId $TenantFilter | Select-Object -ExpandProperty body + $Queries = $Queries | Where-Object { $PendingQueries.RowKey -contains $_.id -and $_.status -eq 'succeeded' } + } else { + $Queries = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -AsApp $true -tenantid $TenantFilter } return $Queries } From 7db8767c6f3cdb314b439882d6023d22efbada75 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 11 Oct 2024 12:17:38 -0400 Subject: [PATCH 6/7] Update Invoke-CIPPStandardPerUserMFA.ps1 --- .../Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 index ecd00d8db6fb..e0aa9df16f0c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 @@ -39,10 +39,14 @@ function Invoke-CIPPStandardPerUserMFA { url = "/users/$id/authentication/requirements" } } - $UsersWithoutMFA = (New-GraphBulkRequest -tenantid $tenant -Requests @($Requests) -asapp $true).body | Where-Object { $_.perUserMfaState -ne 'enforced' } | Select-Object peruserMFAState, @{Name = 'userPrincipalName'; Expression = { [System.Web.HttpUtility]::UrlDecode($_.'@odata.context'.split("'")[1]) } } + if ($Requests) { + $UsersWithoutMFA = (New-GraphBulkRequest -tenantid $tenant -Requests @($Requests) -asapp $true).body | Where-Object { $_.perUserMfaState -ne 'enforced' } | Select-Object peruserMFAState, @{Name = 'userPrincipalName'; Expression = { [System.Web.HttpUtility]::UrlDecode($_.'@odata.context'.split("'")[1]) } } + } else { + $UsersWithoutMFA = @() + } If ($Settings.remediate -eq $true) { - if (($UsersWithoutMFA.userPrincipalName | Measure-Object).Count -gt 0) { + if (($UsersWithoutMFA | Measure-Object).Count -gt 0) { try { $MFAMessage = Set-CIPPPerUserMFA -TenantFilter $Tenant -userId @($UsersWithoutMFA.userPrincipalName) -State 'enforced' Write-LogMessage -API 'Standards' -tenant $tenant -message $MFAMessage -sev Info From 5197e8ad638a1b8aab898471f774c30c3040f3a5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 11 Oct 2024 13:54:11 -0400 Subject: [PATCH 7/7] Audit log limit processing --- .../Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 index 2faa3fac26eb..ef5ea518bcb8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 @@ -36,7 +36,7 @@ function Push-AuditLogTenant { $Configuration = $ConfigEntries | Where-Object { ($_.Tenants -match $TenantFilter -or $_.Tenants -match 'AllTenants') } if ($Configuration) { try { - $LogSearches = Get-CippAuditLogSearches -TenantFilter $TenantFilter -ReadyToProcess + $LogSearches = Get-CippAuditLogSearches -TenantFilter $TenantFilter -ReadyToProcess | Select-Object -First 20 Write-Information ('Audit Logs: Found {0} searches, begin processing' -f $LogSearches.Count) foreach ($Search in $LogSearches) { $SearchEntity = Get-CIPPAzDataTableEntity @LogSearchesTable -Filter "Tenant eq '$($TenantFilter)' and RowKey eq '$($Search.id)'"