diff --git a/CIPPTimers.json b/CIPPTimers.json index e8763e7cf3d9..e527285dd78f 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -23,6 +23,13 @@ "PreferredProcessor": "auditlog", "IsSystem": true }, + { + "Command": "Start-ApplicationOrchestrator", + "Description": "Orchestrator to process application uploads", + "Cron": "0 0 */12 * * *", + "Priority": 2, + "RunOnProcessor": true + }, { "Command": "Start-WebhookOrchestrator", "Description": "Orchestrator to process webhooks", 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/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 1bdbb1daac6e..86affa77ad29 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -34,7 +34,7 @@ function Add-CIPPDelegatedPermission { $RequiredResourceAccess.Add($Resource) } - if ($Tenantfilter -eq $env:TenantID) { + if ($Tenantfilter -eq $env:TenantID -or $Tenantfilter -eq 'PartnerTenant') { $RequiredResourceAccess = $RequiredResourceAccess + ($AdditionalPermissions | Where-Object { $RequiredResourceAccess.resourceAppId -notcontains $_.resourceAppId }) } else { # remove the partner center permission if not pushing to partner tenant @@ -42,20 +42,23 @@ function Add-CIPPDelegatedPermission { } } $Translator = Get-Content '.\PermissionsTranslator.json' | ConvertFrom-Json - $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Tenantfilter -skipTokenCache $true -NoAuthCheck $true + $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=appId,id,displayName&`$top=999" -tenantid $Tenantfilter -skipTokenCache $true -NoAuthCheck $true $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property appId -EQ $ApplicationId $Results = [System.Collections.Generic.List[string]]::new() $CurrentDelegatedScopes = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/oauth2PermissionGrants" -skipTokenCache $true -tenantid $Tenantfilter -NoAuthCheck $true foreach ($App in $RequiredResourceAccess) { + if (!$App) { + continue + } $svcPrincipalId = $ServicePrincipalList | Where-Object -Property appId -EQ $App.resourceAppId if (!$svcPrincipalId) { try { $Body = @{ appId = $App.resourceAppId } | ConvertTo-Json -Compress - $svcPrincipalId = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $Tenantfilter -body $Body -type POST + $svcPrincipalId = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $Tenantfilter -body $Body -type POST -NoAuthCheck $true } catch { $Results.add("Failed to create service principal for $($App.resourceAppId): $(Get-NormalizedError -message $_.Exception.Message)") continue diff --git a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearchResults.ps1 b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearchResults.ps1 index 4113c17dc8fb..d2e9ab074bb9 100644 --- a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearchResults.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearchResults.ps1 @@ -18,6 +18,6 @@ function Get-CippAuditLogSearchResults { ) process { - New-GraphGetRequest -uri ('https://graph.microsoft.com/beta/security/auditLog/queries/{0}/records?$top=999' -f $QueryId) -AsApp $true -tenantid $TenantFilter + New-GraphGetRequest -uri ('https://graph.microsoft.com/beta/security/auditLog/queries/{0}/records?$top=999' -f $QueryId) -AsApp $true -tenantid $TenantFilter -ErrorAction Stop } } diff --git a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 index 8c2aa9e9a7ff..ba21f2dedcb2 100644 --- a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 @@ -13,11 +13,27 @@ 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'" + $15MinutesAgo = (Get-Date).AddMinutes(-15).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "Tenant eq '$TenantFilter' and (CippStatus eq 'Pending' or (CippStatus eq 'Processing' and Timestamp le datetime'$15MinutesAgo'))" | Sort-Object Timestamp + + $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 } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 index a787676e9de2..3c682fb8854d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 @@ -124,7 +124,6 @@ function Push-DomainAnalyserDomain { } catch { $Message = 'SPF Error' Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message $Message -LogData (Get-CippException -Exception $_) -sev Error - return $Message } # Check SPF Record @@ -187,7 +186,7 @@ function Push-DomainAnalyserDomain { } catch { $Message = 'DMARC Error' Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message $Message -LogData (Get-CippException -Exception $_) -sev Error - return $Message + #return $Message } # DNS Sec Check @@ -205,7 +204,7 @@ function Push-DomainAnalyserDomain { } catch { $Message = 'DNSSEC Error' Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message $Message -LogData (Get-CippException -Exception $_) -sev Error - return $Message + #return $Message } # DKIM Check @@ -240,7 +239,7 @@ function Push-DomainAnalyserDomain { } catch { $Message = 'DKIM Exception' Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message $Message -LogData (Get-CippException -Exception $_) -sev Error - return $Message + #return $Message } # Get Microsoft DKIM CNAME selector Records @@ -303,7 +302,6 @@ function Push-DomainAnalyserDomain { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message "MS CNAME DKIM error: $($ErrorMessage.NormalizedError)" -LogData $ErrorMessage -sev Error - return $ErrorMessage.NormalizedError } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 index dd702c8e464e..203428ec580e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 @@ -20,7 +20,7 @@ function Push-DomainAnalyserTenant { return } else { try { - $Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $Tenant.customerId | Where-Object { ($_.id -notlike '*.microsoftonline.com' -and $_.id -NotLike '*.exclaimer.cloud' -and $_.id -Notlike '*.excl.cloud' -and $_.id -NotLike '*.codetwo.online' -and $_.id -NotLike '*.call2teams.com' -and $_.isVerified) } + $Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $Tenant.customerId | Where-Object { ($_.id -notlike '*.microsoftonline.com' -and $_.id -NotLike '*.exclaimer.cloud' -and $_.id -Notlike '*.excl.cloud' -and $_.id -NotLike '*.codetwo.online' -and $_.id -NotLike '*.call2teams.com' -and $_.id -notlike '*signature365.net' -and $_.isVerified) } $TenantDomains = foreach ($d in $Domains) { [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 index 3f2009a0a950..09b7c2c48e27 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 @@ -1,21 +1,23 @@ -function Push-ExecAddMultiTenantApp($QueueItem, $TriggerMetadata) { +function Push-ExecAddMultiTenantApp { <# .FUNCTIONALITY Entrypoint #> + [CmdletBinding()] + param($Item) try { - $Queueitem = $QueueItem | ConvertTo-Json -Depth 10 | ConvertFrom-Json - Write-Host "$($Queueitem | ConvertTo-Json -Depth 10)" - $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Queueitem.Tenant - if ($Queueitem.AppId -Notin $ServicePrincipalList.appId) { - $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $queueitem.tenant -body "{ `"appId`": `"$($Queueitem.appId)`" }" - Write-LogMessage -message "Added $($Queueitem.AppId) to tenant $($Queueitem.Tenant)" -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Info + $Item = $Item | ConvertTo-Json -Depth 10 | ConvertFrom-Json + Write-Host "$($Item | ConvertTo-Json -Depth 10)" + $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Item.Tenant + if ($Item.AppId -Notin $ServicePrincipalList.appId) { + $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Item.tenant -body "{ `"appId`": `"$($Item.appId)`" }" + Write-LogMessage -message "Added $($Item.AppId) to tenant $($Item.Tenant)" -tenant $Item.Tenant -API 'Add Multitenant App' -sev Info } else { - Write-LogMessage -message "This app already exists in tenant $($Queueitem.Tenant). We're adding the required permissions." -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Info + Write-LogMessage -message "This app already exists in tenant $($Item.Tenant). We're adding the required permissions." -tenant $Item.Tenant -API 'Add Multitenant App' -sev Info } - Add-CIPPApplicationPermission -RequiredResourceAccess ($queueitem.applicationResourceAccess) -ApplicationId $queueitem.AppId -Tenantfilter $Queueitem.Tenant - Add-CIPPDelegatedPermission -RequiredResourceAccess ($queueitem.DelegateResourceAccess) -ApplicationId $queueitem.AppId -Tenantfilter $Queueitem.Tenant + Add-CIPPApplicationPermission -RequiredResourceAccess ($Item.applicationResourceAccess) -ApplicationId $Item.AppId -Tenantfilter $Item.Tenant + Add-CIPPDelegatedPermission -RequiredResourceAccess ($Item.DelegateResourceAccess) -ApplicationId $Item.AppId -Tenantfilter $Item.Tenant } catch { - Write-LogMessage -message "Error adding application to tenant $($Queueitem.Tenant) - $($_.Exception.Message)" -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Error + Write-LogMessage -message "Error adding application to tenant $($Item.Tenant) - $($_.Exception.Message)" -tenant $Item.Tenant -API 'Add Multitenant App' -sev Error } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 index 6437940809db..58154aed81e6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 @@ -1,13 +1,14 @@ -function Push-ExecApplicationCopy($QueueItem, $TriggerMetadata) { +function Push-ExecApplicationCopy { <# .FUNCTIONALITY Entrypoint #> + [CmdletBinding()] + param($Item) try { - $Queueitem = $QueueItem | ConvertTo-Json -Depth 10 | ConvertFrom-Json - Write-Host "$($Queueitem | ConvertTo-Json -Depth 10)" - New-CIPPApplicationCopy -App $queueitem.AppId -Tenant $Queueitem.Tenant + Write-Host "$($Item | ConvertTo-Json -Depth 10)" + New-CIPPApplicationCopy -App $Item.AppId -Tenant $Item.Tenant } catch { - Write-LogMessage -message "Error adding application to tenant $($Queueitem.Tenant) - $($_.Exception.Message)" -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Error + Write-LogMessage -message "Error adding application to tenant $($Item.Tenant) - $($_.Exception.Message)" -tenant $Item.Tenant -API 'Add Multitenant App' -sev Error } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index bf9b17bb9dc9..5c18cbe54d21 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -275,7 +275,7 @@ Function Push-ExecOnboardTenantQueue { $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Clearing tenant cache' }) $y = 0 do { - $Tenant = Get-Tenants -TriggerRefresh -IncludeAll | Where-Object { $_.customerId -eq $Relationship.customer.tenantId } | Select-Object -First 1 + $Tenant = Get-Tenants -TriggerRefresh -TenantFilter $Relationship.customer.tenantId | Select-Object -First 1 $y++ Start-Sleep -Seconds 20 } while (!$Tenant -and $y -le 10) 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)'" 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..60ba2aa4adca 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,10 +31,22 @@ Function Invoke-ExecExtensionsConfig { # Check if NinjaOne URL is set correctly and the instance has at least version 5.6 if ($Body.NinjaOne) { + $AllowedNinjaHostnames = @( + 'app.ninjarmm.com', + 'eu.ninjarmm.com', + 'oc.ninjarmm.com', + 'ca.ninjarmm.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'" + throw "Failed to connect to NinjaOne check your Instance is set correctly eg 'app.ninjarmm.com'" } if ($Version -lt [version]'5.6.0.0') { throw 'NinjaOne 5.6.0.0 is required.' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCPVPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCPVPermissions.ps1 index 22f7f02c0307..8fbf7872e3c9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCPVPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCPVPermissions.ps1 @@ -28,14 +28,20 @@ Function Invoke-ExecCPVPermissions { } $GraphRequest = try { - if ($TenantFilter -ne 'PartnerTenant') { + if ($TenantFilter -notin @('PartnerTenant', $env:TenantId)) { Set-CIPPCPVConsent @CPVConsentParams } else { $TenantFilter = $env:TenantID + $Tenant = [PSCustomObject]@{ + displayName = '*Partner Tenant' + defaultDomainName = $env:TenantID + } } Add-CIPPApplicationPermission -RequiredResourceAccess 'CIPPDefaults' -ApplicationId $ENV:ApplicationID -tenantfilter $TenantFilter Add-CIPPDelegatedPermission -RequiredResourceAccess 'CIPPDefaults' -ApplicationId $ENV:ApplicationID -tenantfilter $TenantFilter - Set-CIPPSAMAdminRoles -TenantFilter $TenantFilter + if ($TenantFilter -notin @('PartnerTenant', $env:TenantId)) { + Set-CIPPSAMAdminRoles -TenantFilter $TenantFilter + } $Success = $true } catch { "Failed to update permissions for $($Tenant.displayName): $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAppUpload.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAppUpload.ps1 index 2202e7f58ccd..824722a5e6de 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAppUpload.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAppUpload.ps1 @@ -31,7 +31,6 @@ function Invoke-ExecAppUpload { } } - $Results = [pscustomobject]@{'Results' = 'Started application queue' } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = $Results diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogSearches.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogSearches.ps1 index b72187063e48..561384504174 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogSearches.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogSearches.ps1 @@ -20,7 +20,11 @@ function Invoke-ListAuditLogSearches { } | ConvertTo-Json -Depth 10 -Compress } 'SearchResults' { - $Results = Get-CippAuditLogSearchResults -TenantFilter $Request.Query.TenantFilter -QueryId $Request.Query.SearchId + try { + $Results = Get-CippAuditLogSearchResults -TenantFilter $Request.Query.TenantFilter -QueryId $Request.Query.SearchId + } catch { + $Results = @{ Error = $_.Exception.Message } + } $Body = @{ Results = @($Results) Metadata = @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 index 4cb38d9f9dc8..dd03e24f8b3d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 @@ -17,26 +17,34 @@ function Invoke-ExecAddMultiTenantApp { $ApplicationResourceAccess = @{ ResourceAppId = '00000003-0000-0000-c000-000000000000'; resourceAccess = $ApplicationResources } $Results = try { - if ($request.body.CopyPermissions -eq $true) { + if ($Request.Body.CopyPermissions -eq $true) { $Command = 'ExecApplicationCopy' } else { $Command = 'ExecAddMultiTenantApp' } - if ('allTenants' -in $Request.body.SelectedTenants.defaultDomainName) { + if ('allTenants' -in $Request.Body.SelectedTenants.defaultDomainName) { $TenantFilter = (Get-Tenants).defaultDomainName } else { - $TenantFilter = $Request.body.SelectedTenants.defaultDomainName + $TenantFilter = $Request.Body.SelectedTenants.defaultDomainName } + $TenantCount = ($TenantFilter | Measure-Object).Count + $Queue = New-CippQueueEntry -Name 'Application Approval' -TotalTasks $TenantCount foreach ($Tenant in $TenantFilter) { try { - Push-OutputBinding -Name QueueItem -Value ([pscustomobject]@{ - FunctionName = $Command - Tenant = $tenant - appId = $Request.body.appid - applicationResourceAccess = $ApplicationResourceAccess - delegateResourceAccess = $DelegateResourceAccess - }) + $InputObject = @{ + OrchestratorName = 'ExecMultiTenantAppOrchestrator' + Batch = @([pscustomobject]@{ + FunctionName = $Command + Tenant = $tenant + AppId = $Request.Body.AppId + applicationResourceAccess = $ApplicationResourceAccess + delegateResourceAccess = $DelegateResourceAccess + QueueId = $Queue.RowKey + }) + SkipLog = $true + } + $null = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) "Queued application to tenant $Tenant. See the logbook for deployment details" } catch { "Error queuing application to tenant $Tenant - $($_.Exception.Message)" 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 diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListGraphRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListGraphRequest.ps1 index dbb63b088425..f656bd541400 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListGraphRequest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListGraphRequest.ps1 @@ -116,13 +116,15 @@ function Invoke-ListGraphRequest { $GraphRequestParams.AsApp = $true } - Write-Host ($GraphRequestParams | ConvertTo-Json) - $Metadata = $GraphRequestParams try { $Results = Get-GraphRequestList @GraphRequestParams - + if ($Results.nextLink -and $Request.Query.NoPagination) { + $Metadata['nextLink'] = $Results.nextLink | Select-Object -Last 1 + #Results is an array of objects, so we need to remove the last object before returning + $Results = $Results | Select-Object -First ($Results.Count - 1) + } if ($Request.Query.ListProperties) { $Columns = ($Results | Select-Object -First 1).PSObject.Properties.Name $Results = $Columns | Where-Object { @('Tenant', 'CippStatus') -notcontains $_ } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntunePolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntunePolicy.ps1 index ffd921297a06..8e65b6f27001 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntunePolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntunePolicy.ps1 @@ -24,7 +24,7 @@ Function Invoke-ListIntunePolicy { if ($ID) { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$ID')" -tenantid $tenantfilter } else { - $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenantfilter | Select-Object -Property id, displayName + $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $tenantfilter | Select-Object -Property id, displayName $BulkRequests = [PSCustomObject]@( @{ @@ -56,7 +56,9 @@ Function Invoke-ListIntunePolicy { $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter - $GraphRequest = $BulkResults.body.value | ForEach-Object { + $GraphRequest = $BulkResults | ForEach-Object { + $URLName = $_.Id + $_.body.Value | ForEach-Object { $policyTypeName = switch -Wildcard ($_.'assignments@odata.context') { '*microsoft.graph.windowsIdentityProtectionConfiguration*' { 'Identity Protection' } '*microsoft.graph.windows10EndpointProtectionConfiguration*' { 'Endpoint Protection' } @@ -95,7 +97,7 @@ Function Invoke-ListIntunePolicy { $_ | Add-Member -NotePropertyName PolicyExclude -NotePropertyValue ($PolicyExclude -join ', ') $_ } | Where-Object { $_.DisplayName -ne $null } - + } } $StatusCode = [HttpStatusCode]::OK } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxes.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxes.ps1 index dc2227ee2a29..260241fa972f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxes.ps1 @@ -38,7 +38,7 @@ Function Invoke-ListMailboxes { @{Parameter = 'SoftDeletedMailbox'; Type = 'Bool' } ) - foreach ($Param in $Request.Query.Keys) { + foreach ($Param in $Request.Query.PSObject.Properties.Name) { $CmdParam = $AllowedParameters | Where-Object { $_.Parameter -eq $Param } if ($CmdParam) { switch ($CmdParam.Type) { @@ -48,7 +48,9 @@ Function Invoke-ListMailboxes { } } 'Bool' { - if ([bool]$Request.Query.$Param -eq $true) { + $ParamIsTrue = $false + [bool]::TryParse($Request.Query.$Param, [ref]$ParamIsTrue) | Out-Null + if ($ParamIsTrue -eq $true) { $ExoRequest.cmdParams.$Param = $true } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-ApplicationOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-ApplicationOrchestrator.ps1 index c564d6a70d60..eb4d4386aeac 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-ApplicationOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-ApplicationOrchestrator.ps1 @@ -7,7 +7,7 @@ function Start-ApplicationOrchestrator { Param() Write-LogMessage -API 'IntuneApps' -message 'Started uploading applications to tenants' -sev Info - + Write-Information 'Started uploading applications to tenants' $InputObject = [PSCustomObject]@{ OrchestratorName = 'ApplicationOrchestrator' SkipLog = $true diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 index ed6e09bebf62..a0d340bb6c8a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 @@ -11,13 +11,26 @@ function Start-CIPPProcessorQueue { foreach ($QueueItem in $QueueItems) { if ($PSCmdlet.ShouldProcess("Processing function $($QueueItem.ProcessorFunction)")) { - Remove-AzDataTableEntity @QueueTable -Entity $QueueItem - $Parameters = $QueueItem.Parameters | ConvertFrom-Json -AsHashtable - if (Get-Command -Name $QueueItem.FunctionName -Module CIPPCore -ErrorAction SilentlyContinue) { - & $QueueItem.FunctionName @Parameters + Write-Information "Running queued function $($QueueItem.ProcessorFunction)" + if ($QueueItem.Parameters) { + try { + $Parameters = $QueueItem.Parameters | ConvertFrom-Json -AsHashtable + } catch { + $Parameters = @{} + } + } else { + $Parameters = @{} + } + if (Get-Command -Name $QueueItem.ProcessorFunction -Module CIPPCore -ErrorAction SilentlyContinue) { + try { + Invoke-Command -ScriptBlock { & $QueueItem.ProcessorFunction @Parameters } + } catch { + Write-Warning "Failed to run function $($QueueItem.ProcessorFunction). Error: $($_.Exception.Message)" + } } else { - Write-Warning "Function $($QueueItem.FunctionName) not found" + Write-Warning "Function $($QueueItem.ProcessorFunction) not found" } + Remove-AzDataTableEntity @QueueTable -Entity $QueueItem } } } diff --git a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 index a72d598639a2..b7aed6e35fc0 100644 --- a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 @@ -15,7 +15,7 @@ function Get-CIPPBitlockerKey { return $GraphRequest } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -user $ExecutingUser -API $APIName -message "Could not add OOO for $($userid)" -Sev 'Error' -tenant $TenantFilter -LogData (Get-CippException -Exception $_) - return "Could not add out of office message for $($userid). Error: $ErrorMessage" + Write-LogMessage -user $ExecutingUser -API $APIName -message "Could not retrieve bitlocker recovery key for $($device)" -Sev 'Error' -tenant $TenantFilter -LogData (Get-CippException -Exception $_) + return "Could not retrieve bitlocker recovery key for $($device). Error: $ErrorMessage" } } diff --git a/Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 b/Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 index 011ab9f4552a..eeac2a740ac2 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 @@ -9,7 +9,7 @@ function Get-CIPPLapsPassword { ) try { - $GraphRequest = (New-GraphGetRequest -noauthcheck $true -uri "https://graph.microsoft.com/beta/deviceLocalCredentials/$($device)?`$select=credentials" -tenantid $TenantFilter).credentials | Select-Object -First 1 | ForEach-Object { + $GraphRequest = (New-GraphGetRequest -noauthcheck $true -uri "https://graph.microsoft.com/beta/directory/deviceLocalCredentials/$($device)?`$select=credentials" -tenantid $TenantFilter).credentials | Select-Object -First 1 | ForEach-Object { $PlainText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_.passwordBase64)) $date = $_.BackupDateTime "The password for $($_.AccountName) is $($PlainText) generated at $($date)" @@ -17,8 +17,8 @@ function Get-CIPPLapsPassword { if ($GraphRequest) { return $GraphRequest } else { return "No LAPS password found for $device" } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -user $ExecutingUser -API $APIName -message "Could not add OOO for $($userid). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - return "Could not add out of office message for $($userid). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -user $ExecutingUser -API $APIName -message "Could not retrieve LAPS password for $($device). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + return "Could not retrieve LAPS password for $($device). Error: $($ErrorMessage.NormalizedError)" } } diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-AuthorisedRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-AuthorisedRequest.ps1 index f68050d83d12..f8147728a15c 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-AuthorisedRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-AuthorisedRequest.ps1 @@ -12,7 +12,7 @@ function Get-AuthorisedRequest { if (!$TenantID) { $TenantID = $env:TenantID } - if ($Uri -like 'https://graph.microsoft.com/beta/contracts*' -or $Uri -like '*/customers/*' -or $Uri -eq 'https://graph.microsoft.com/v1.0/me/sendMail' -or $Uri -like '*/tenantRelationships/*') { + if ($Uri -like 'https://graph.microsoft.com/beta/contracts*' -or $Uri -like '*/customers/*' -or $Uri -eq 'https://graph.microsoft.com/v1.0/me/sendMail' -or $Uri -like '*/tenantRelationships/*' -or $Uri -like '*/security/partner/*') { return $true } $Tenants = Get-Tenants -IncludeErrors diff --git a/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 b/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 index 5828884a19d1..27560afaa984 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 @@ -35,8 +35,9 @@ function Write-LogMessage { 'Username' = [string]$username 'Severity' = [string]$sev 'SentAsAlert' = $false - 'PartitionKey' = $PartitionKey - 'RowKey' = ([guid]::NewGuid()).ToString() + 'PartitionKey' = [string]$PartitionKey + 'RowKey' = [string]([guid]::NewGuid()).ToString() + 'FunctionNode' = [string]$env:WEBSITE_SITE_NAME 'LogData' = [string]$LogData } diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index 769796f02504..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 @@ -295,11 +293,6 @@ function Get-GraphRequestList { if ($nextLink) { $GraphRequest.uri = $nextLink } $GraphRequestResults = New-GraphGetRequest @GraphRequest -Caller 'Get-GraphRequestList' -ErrorAction Stop - if ($GraphRequestResults.nextLink) { - #$Metadata['nextLink'] = $GraphRequestResults.nextLink | Select-Object -Last 1 - #GraphRequestResults is an array of objects, so we need to remove the last object before returning - $GraphRequestResults = $GraphRequestResults | Select-Object -First ($GraphRequestResults.Count - 1) - } $GraphRequestResults = $GraphRequestResults | Select-Object *, @{n = 'Tenant'; e = { $TenantFilter } }, @{n = 'CippStatus'; e = { 'Good' } } if ($ReverseTenantLookup -and $GraphRequestResults) { 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 } } diff --git a/Modules/CIPPCore/Public/Set-CIPPCAExclusion.ps1 b/Modules/CIPPCore/Public/Set-CIPPCAExclusion.ps1 index d9b4658a7405..9f66cf3e74c5 100644 --- a/Modules/CIPPCore/Public/Set-CIPPCAExclusion.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPCAExclusion.ps1 @@ -9,7 +9,7 @@ function Set-CIPPCAExclusion { $executingUser ) try { - $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($PolicyId)" -tenantid $TenantFilter + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($PolicyId)" -tenantid $TenantFilter -AsApp $true if ($ExclusionType -eq 'add') { $NewExclusions = [pscustomobject]@{ conditions = [pscustomobject]@{ users = [pscustomobject]@{ @@ -19,7 +19,7 @@ function Set-CIPPCAExclusion { } $RawJson = ConvertTo-Json -Depth 10 -InputObject $NewExclusions if ($PSCmdlet.ShouldProcess($PolicyId, "Add exclusion for $UserID")) { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExististing.id)" -tenantid $tenantfilter -type PATCH -body $RawJSON + New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExististing.id)" -tenantid $tenantfilter -type PATCH -body $RawJSON -AsApp $true } } @@ -32,7 +32,7 @@ function Set-CIPPCAExclusion { } $RawJson = ConvertTo-Json -Depth 10 -InputObject $NewExclusions if ($PSCmdlet.ShouldProcess($PolicyId, "Remove exclusion for $UserID")) { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExististing.id)" -tenantid $tenantfilter -type PATCH -body $RawJSON + New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExististing.id)" -tenantid $tenantfilter -type PATCH -body $RawJSON -AsApp $true } } "Successfully performed $($ExclusionType) exclusion for $username from policy $($PolicyId)" @@ -41,4 +41,4 @@ function Set-CIPPCAExclusion { "Failed to $($ExclusionType) user exclusion for $username from policy $($PolicyId): $($_.Exception.Message)" Write-LogMessage -user $executingUser -API 'Set-CIPPConditionalAccessExclusion' -message "Failed to $($ExclusionType) user exclusion for $username from policy $($PolicyId): $_" -Sev 'Error' -tenant $TenantFilter -LogData (Get-CippException -Exception $_) } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index c58a9a3dd7cf..65396b677490 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -51,10 +51,10 @@ function Invoke-CIPPStandardAntiPhishPolicy { param($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AntiPhishPolicy' - $PolicyName = @('Default Anti-Phishing Policy', 'Office365 AntiPhish Default (Default)') - - $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AntiPhishPolicy' | - Where-Object -Property Name -In $PolicyName | + $PolicyList = @('Default Anti-Phishing Policy', 'Office365 AntiPhish Default (Default)') + $ExistingPolicy = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AntiPhishPolicy' | Where-Object -Property Name -In $PolicyList + $PolicyName = $ExistingPolicy.Name + $CurrentState = $ExistingPolicy | Select-Object Name, Enabled, PhishThresholdLevel, EnableMailboxIntelligence, EnableMailboxIntelligenceProtection, EnableSpoofIntelligence, EnableFirstContactSafetyTips, EnableSimilarUsersSafetyTips, EnableSimilarDomainsSafetyTips, EnableUnusualCharactersSafetyTips, EnableUnauthenticatedSender, EnableViaTag, AuthenticationFailAction, SpoofQuarantineTag, MailboxIntelligenceProtectionAction, MailboxIntelligenceQuarantineTag, TargetedUserProtectionAction, TargetedUserQuarantineTag, TargetedDomainProtectionAction, TargetedDomainQuarantineTag, EnableOrganizationDomainsProtection $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and 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 diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index 33f22fc0c953..9e588dae7a5c 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -35,7 +35,13 @@ function Test-CIPPAuditLogRules { } } #write-warning 'Getting audit records from Graph API' - $SearchResults = Get-CippAuditLogSearchResults -TenantFilter $TenantFilter -QueryId $SearchId + try { + $SearchResults = Get-CippAuditLogSearchResults -TenantFilter $TenantFilter -QueryId $SearchId + } catch { + Write-Warning "Error getting audit logs: $($_.Exception.Message)" + Write-LogMessage -API 'Webhooks' -message "Error getting audit logs for search $($SearchId)" -LogData (Get-CippException -Exception $_) -sev Error -tenant $TenantFilter + throw $_ + } $LogCount = ($SearchResults | Measure-Object).Count $RunGuid = New-Guid Write-Warning "Logs to process: $LogCount - RunGuid: $($RunGuid) - $($TenantFilter)" diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 index 9213a7015b1a..67e54991324e 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 @@ -5,7 +5,6 @@ function Invoke-NinjaOneDeviceWebhook { $Configuration ) try { - Write-LogMessage -user $ExecutingUser -API $APIName -message "Webhook Recieved - Updating NinjaOne Device compliance for $($Data.resourceData.id) in $($Data.tenantId)" -Sev 'Info' -tenant $TenantFilter $MappedFields = [pscustomobject]@{} $CIPPMapping = Get-CIPPTable -TableName CippMapping $Filter = "PartitionKey eq 'NinjaOneFieldMapping'" @@ -14,6 +13,7 @@ function Invoke-NinjaOneDeviceWebhook { } if ($MappedFields.DeviceCompliance) { + Write-LogMessage -user $ExecutingUser -API $APIName -message "Webhook Recieved - Updating NinjaOne Device compliance for $($Data.resourceData.id) in $($Data.tenantId)" -Sev 'Info' -tenant $TenantFilter $tenantfilter = $Data.tenantId $M365DeviceID = $Data.resourceData.id @@ -24,23 +24,36 @@ function Invoke-NinjaOneDeviceWebhook { $Device = Get-CIPPAzDataTableEntity @DeviceMapTable -Filter $DeviceFilter if (($Device | Measure-Object).count -eq 1) { - $Token = Get-NinjaOneToken -configuration $Configuration + try { + $Token = Get-NinjaOneToken -configuration $Configuration - if ($DeviceM365.isCompliant -eq $True) { - $Compliant = 'Compliant' - } else { - $Compliant = 'Non-Compliant' - } - - $ComplianceBody = @{ - "$($MappedFields.DeviceCompliance)" = $Compliant - } | ConvertTo-Json + if (!$Token.access_token) { + Write-LogMessage -API 'NinjaOneSync' -tenant $tenantfilter -user 'CIPP' -message 'Failed to get NinjaOne Token for Device Compliance Update' -Sev 'Error' + return + } - $Null = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device/$($Device.NinjaOneID)/custom-fields" -Method PATCH -Body $ComplianceBody -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' + if ($DeviceM365.isCompliant -eq $True) { + $Compliant = 'Compliant' + } else { + $Compliant = 'Non-Compliant' + } - Write-Host 'Updated NinjaOne Device Compliance' + $ComplianceBody = @{ + "$($MappedFields.DeviceCompliance)" = $Compliant + } | ConvertTo-Json + $Null = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device/$($Device.NinjaOneID)/custom-fields" -Method PATCH -Body $ComplianceBody -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' + Write-Host 'Updated NinjaOne Device Compliance' + } catch { + $Message = if ($_.ErrorDetails.Message) { + Get-NormalizedError -Message $_.ErrorDetails.Message + } else { + $_.Exception.message + } + Write-Error "Failed NinjaOne Device Webhook for: $($Data | ConvertTo-Json -Depth 100) Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" + Write-LogMessage -API 'NinjaOneSync' -user 'CIPP' -message "Failed NinjaOne Device Webhook Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" -Sev 'Error' + } } else { Write-LogMessage -API 'NinjaOneSync' -user 'CIPP' -message "$($DeviceM365.displayName) ($($M365DeviceID)) was not matched in Ninja for $($tenantfilter)" -Sev 'Info' } @@ -59,4 +72,4 @@ function Invoke-NinjaOneDeviceWebhook { -} \ No newline at end of file +} diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 03d1bed97445..d45407edb06e 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.ninjarmm.com', + 'eu.ninjarmm.com', + 'oc.ninjarmm.com', + 'ca.ninjarmm.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' } @@ -299,11 +311,11 @@ function Invoke-NinjaOneTenantSync { method = 'GET' url = '/deviceManagement/deviceCompliancePolicies/' }, - @{ + <#@{ id = 'DeviceApps' method = 'GET' url = '/deviceAppManagement/mobileApps' - }, + },#> @{ id = 'Groups' method = 'GET' @@ -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 - $Table = Get-CippTable -tablename 'standards' + try { + $StandardsDefinitions = Get-Content 'config/standards.json' | ConvertFrom-Json -Depth 100 + + $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 diff --git a/version_latest.txt b/version_latest.txt index f22d756da39d..fa09f584d78e 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -6.5.0 +6.5.2 \ No newline at end of file