Skip to content

Commit 10c73eb

Browse files
authored
Merge pull request #1070 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 5392fd1 + 9c1879c commit 10c73eb

7 files changed

Lines changed: 323 additions & 28 deletions

File tree

Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,15 @@ function Push-BECRun {
2222
Write-Information 'Getting audit logs'
2323
try {
2424
$auditLog = (New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AdminAuditLogConfig').UnifiedAuditLogIngestionEnabled
25-
$7dayslog = if ($auditLog -eq $false) {
25+
$7DaysLog = if ($auditLog -eq $false) {
2626
$ExtractResult = 'AuditLog is disabled. Cannot perform full analysis'
2727
} else {
2828
$sessionid = Get-Random -Minimum 10000 -Maximum 99999
2929
$operations = @(
3030
'Remove-MailboxPermission',
3131
'Add-MailboxPermission',
3232
'UpdateCalendarDelegation',
33-
'AddFolderPermissions',
34-
'MailboxLogin',
35-
'UserLoggedIn'
33+
'AddFolderPermissions'
3634
)
3735
$startDate = (Get-Date).AddDays(-7)
3836
$endDate = (Get-Date)
@@ -51,7 +49,7 @@ function Push-BECRun {
5149
$ExtractResult = 'Successfully extracted logs from auditlog'
5250
}
5351
} catch {
54-
$7dayslog = @()
52+
$7DaysLog = @()
5553
$CippAuditError = Get-CippException -Exception $_
5654
$ExtractResult = "Could not retrieve audit logs: $($CippAuditError.NormalizedError)"
5755
Write-LogMessage -API 'BECRun' -message "Failed to retrieve audit logs for $($UserName): $($CippAuditError.NormalizedError)" -tenant $TenantFilter -sev Warning -LogData $CippAuditError
@@ -83,7 +81,7 @@ function Push-BECRun {
8381
}
8482

8583
try {
86-
$PermissionsLog = ($7dayslog | Where-Object -Property Operations -In 'Remove-MailboxPermission', 'Add-MailboxPermission', 'UpdateCalendarDelegation', 'AddFolderPermissions' ).AuditData | ConvertFrom-Json -ErrorAction Stop | ForEach-Object {
84+
$PermissionsLog = ($7DaysLog | Where-Object -Property Operations -In 'Remove-MailboxPermission', 'Add-MailboxPermission', 'UpdateCalendarDelegation', 'AddFolderPermissions' ).AuditData | ConvertFrom-Json -ErrorAction Stop | ForEach-Object {
8785
$perms = if ($_.Parameters) {
8886
$_.Parameters | ForEach-Object { if ($_.Name -eq 'AccessRights') { $_.Value } }
8987
} else
@@ -100,16 +98,51 @@ function Push-BECRun {
10098
$PermissionsLog = @()
10199
}
102100

101+
Write-Information 'Getting inbox rule changes'
102+
try {
103+
$RuleChangesLog = if ($auditLog -eq $false) { @() } else {
104+
# ponytail: separate user-scoped search - UpdateInboxRules is too high-volume for the tenant-wide query above
105+
$RuleSearchParam = @{
106+
SessionCommand = 'ReturnLargeSet'
107+
Operations = @('New-InboxRule', 'Set-InboxRule', 'Remove-InboxRule', 'UpdateInboxRules')
108+
sessionid = (Get-Random -Minimum 10000 -Maximum 99999)
109+
startDate = $startDate
110+
endDate = $endDate
111+
UserIds = $UserName
112+
}
113+
(New-ExoRequest -tenantid $TenantFilter -cmdlet 'Search-UnifiedAuditLog' -cmdParams $RuleSearchParam -Anchor $UserName).AuditData | ConvertFrom-Json -ErrorAction Stop |
114+
Where-Object { $_.UserId -eq $UserName -or $_.MailboxOwnerUPN -eq $UserName -or $_.ObjectId -like "*$UserName*" } | ForEach-Object {
115+
$RuleName = ($_.Parameters | Where-Object { $_.Name -eq 'Name' }).Value ?? $_.ObjectId
116+
[pscustomobject]@{
117+
Operation = $_.Operation
118+
UserKey = $_.UserId
119+
RuleName = $RuleName
120+
Parameters = ($_.Parameters | Where-Object { $_ -and $_.Name -notin 'Identity', 'Name' } | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join '; '
121+
Date = $_.CreationTime
122+
}
123+
}
124+
}
125+
} catch {
126+
$RuleChangesLog = @()
127+
$CippRuleError = Get-CippException -Exception $_
128+
Write-LogMessage -API 'BECRun' -message "Failed to retrieve inbox rule changes for $($UserName): $($CippRuleError.NormalizedError)" -tenant $TenantFilter -sev Warning -LogData $CippRuleError
129+
}
130+
103131
Write-Information 'Getting rules'
104132

105133
try {
106134
$RulesLog = New-ExoRequest -cmdlet 'Get-InboxRule' -tenantid $TenantFilter -cmdParams @{ Mailbox = $Username; IncludeHidden = $true } -Anchor $Username |
107135
Where-Object { $_.Name -ne 'Junk E-Mail Rule' -and $_.Name -notlike 'Microsoft.Exchange.OOF.*' }
108136
} catch {
109-
Write-Host 'Failed to get rules: ' + $_.Exception.Message
137+
$CippRulesError = Get-CippException -Exception $_
138+
Write-LogMessage -API 'BECRun' -message "Failed to retrieve inbox rules for $($UserName): $($CippRulesError.NormalizedError)" -tenant $TenantFilter -sev Warning -LogData $CippRulesError
110139
$RulesLog = @()
111140
}
112141

142+
# inbox rules carry no timestamps, so 'recent' = name-matches a 7-day audit event; Outlook-client changes (UpdateInboxRules) carry no rule name and stay unflagged
143+
$RecentRuleNames = @($RuleChangesLog | Where-Object { $_.Operation -in 'New-InboxRule', 'Set-InboxRule' } | ForEach-Object { ($_.RuleName -split '\\')[-1] })
144+
$RulesLog = @($RulesLog | Where-Object { $_ } | Select-Object *, @{ Name = 'RecentlyChanged'; Expression = { $_.Name -in $RecentRuleNames } })
145+
113146
Write-Information 'Getting sent message trace'
114147
try {
115148
$MessageTraceParams = @{
@@ -118,7 +151,7 @@ function Push-BECRun {
118151
EndDate = $endDate.ToString('s')
119152
}
120153
$SentMessages = @(New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MessageTraceV2' -cmdParams $MessageTraceParams -Anchor $UserName |
121-
Select-Object MessageTraceId, Status, Subject, RecipientAddress, @{ Name = 'Received'; Expression = { $_.Received.ToString('u') } }, FromIP)
154+
Select-Object MessageTraceId, Status, Subject, RecipientAddress, @{ Name = 'Received'; Expression = { $_.Received.ToString('u') } }, FromIP)
122155
} catch {
123156
$SentMessages = @()
124157
$CippTraceError = Get-CippException -Exception $_
@@ -177,6 +210,7 @@ function Push-BECRun {
177210
LastSuspectUserLogon = @($LastSignIn)
178211
SuspectUserDevices = @($Devices)
179212
NewRules = @($RulesLog)
213+
InboxRuleChanges = @($RuleChangesLog)
180214
SentMessages = @($SentMessages)
181215
MailboxPermissionChanges = @($PermissionsLog)
182216
NewUsers = @($NewUsers)
@@ -198,7 +232,7 @@ function Push-BECRun {
198232
} catch {
199233
$errMessage = Get-NormalizedError -message $_.Exception.Message
200234
$CippError = Get-CippException -Exception $_
201-
$results = [pscustomobject]@{'Results' = "$errMessage"; Exception = $CippError }
235+
$results = [pscustomobject]@{'Results' = "$errMessage"; Exception = $CippError; ExtractedAt = (Get-Date) }
202236
Write-LogMessage -API 'BECRun' -message "Error Running BEC for $($UserName): $errMessage" -tenant $TenantFilter -sev 'Error' -LogData $CIPPError
203237
$Entity = @{
204238
UserId = $SuspectUser

Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
function New-CIPPCAPolicy {
32
[CmdletBinding()]
43
param (
@@ -116,6 +115,14 @@ function New-CIPPCAPolicy {
116115
if ($JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) {
117116
$JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.PSObject.Properties.Remove('@odata.context')
118117
}
118+
if ($JSONobj.sessionControls) {
119+
if ($JSONobj.sessionControls.disableResilienceDefaults -ne $true) {
120+
$JSONobj.sessionControls.PSObject.Properties.Remove('disableResilienceDefaults')
121+
}
122+
if (@($JSONobj.sessionControls.PSObject.Properties).Count -eq 0) {
123+
$JSONobj.PSObject.Properties.Remove('sessionControls')
124+
}
125+
}
119126
if ($State -and $State -ne 'donotchange') {
120127
$JSONobj | Add-Member -NotePropertyName 'state' -NotePropertyValue $State -Force
121128
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
function New-CIPPTravelPolicy {
2+
<#
3+
.SYNOPSIS
4+
Creates a temporary travel conditional access policy and named location for vacation mode.
5+
6+
.DESCRIPTION
7+
Builds a template-style policy JSON and hands it to New-CIPPCAPolicy, which creates the country
8+
named location from LocationInfo, waits for it to propagate, replaces the display name reference
9+
with the location ID and retries policy creation on propagation errors. The resulting policy blocks
10+
sign-ins for the included users from all locations except the travel destination. Entra ID has no
11+
standalone 'allow' control, so restricting sign-ins to the travel destination is achieved by blocking
12+
every other location. The policy and named location share the same display name so that
13+
Remove-CIPPTravelPolicy can remove both when the vacation ends.
14+
15+
.PARAMETER Headers
16+
The headers to include in the request, typically containing authentication tokens. Supplied automatically by the API.
17+
18+
.PARAMETER TenantFilter
19+
The tenant identifier to filter the request.
20+
21+
.PARAMETER Users
22+
An array of user principal names or object IDs of the users travelling.
23+
24+
.PARAMETER Countries
25+
An array of ISO 3166-1 alpha-2 country codes for the travel destination(s).
26+
27+
.PARAMETER PolicyName
28+
The display name used for both the conditional access policy and the named location.
29+
30+
.PARAMETER APIName
31+
The name of the API operation being performed. Defaults to 'New-CIPPTravelPolicy'.
32+
#>
33+
[CmdletBinding()]
34+
param(
35+
$Headers,
36+
[Parameter(Mandatory = $true)][string]$TenantFilter,
37+
[Parameter(Mandatory = $true)][string[]]$Users,
38+
[Parameter(Mandatory = $true)][string[]]$Countries,
39+
[Parameter(Mandatory = $true)][string]$PolicyName,
40+
[string]$APIName = 'New-CIPPTravelPolicy'
41+
)
42+
try {
43+
$GuidRegex = '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$'
44+
$UserIds = foreach ($User in $Users) {
45+
if ($User -match $GuidRegex) {
46+
$User
47+
} else {
48+
(New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$([System.Web.HttpUtility]::UrlEncode($User))?`$select=id" -tenantid $TenantFilter -asApp $true).id
49+
}
50+
}
51+
52+
$RawJSON = ConvertTo-Json -Depth 10 -InputObject @{
53+
displayName = $PolicyName
54+
state = 'enabled'
55+
conditions = @{
56+
users = @{ includeUsers = @($UserIds) }
57+
applications = @{ includeApplications = @('All') }
58+
clientAppTypes = @('all')
59+
locations = @{
60+
includeLocations = @('All')
61+
excludeLocations = @($PolicyName)
62+
}
63+
}
64+
grantControls = @{
65+
operator = 'OR'
66+
builtInControls = @('block')
67+
}
68+
LocationInfo = @(
69+
@{
70+
'@odata.type' = '#microsoft.graph.countryNamedLocation'
71+
displayName = $PolicyName
72+
countriesAndRegions = @($Countries)
73+
includeUnknownCountriesAndRegions = $false
74+
}
75+
)
76+
}
77+
$null = New-CIPPCAPolicy -RawJSON $RawJSON -TenantFilter $TenantFilter -Overwrite $true -ReplacePattern 'none' -APIName $APIName -Headers $Headers
78+
$Result = "Created temporary travel policy '$PolicyName' allowing sign-ins from $($Countries -join ', ') only."
79+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info'
80+
return $Result
81+
} catch {
82+
$ErrorMessage = Get-CippException -Exception $_
83+
$Result = "Failed to create temporary travel policy '$PolicyName': $($ErrorMessage.NormalizedError)"
84+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' -LogData $ErrorMessage
85+
throw $Result
86+
}
87+
}

Modules/CIPPCore/Public/Remove-CIPPLicense.ps1

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,66 @@ function Remove-CIPPLicense {
3535
try {
3636
$ConvertTable = [System.IO.File]::ReadAllText((Join-Path $env:CIPPRootPath 'Config\ConversionTable.csv')) | ConvertFrom-Csv
3737
$User = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($userid)" -tenantid $tenantFilter
38-
$GroupMemberships = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($userid)/memberOf/microsoft.graph.group?`$select=id,displayName,assignedLicenses" -tenantid $tenantFilter
38+
$GroupMemberships = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($userid)/memberOf/microsoft.graph.group?`$select=id,displayName,assignedLicenses,mailEnabled,groupTypes" -tenantid $tenantFilter
3939
$LicenseGroups = $GroupMemberships | Where-Object { ($_.assignedLicenses | Measure-Object).Count -gt 0 }
4040

4141
if ($LicenseGroups) {
42-
# remove user from groups with licenses, these can only be graph groups
43-
$RemoveRequests = foreach ($LicenseGroup in $LicenseGroups) {
44-
@{
45-
id = $LicenseGroup.id
46-
method = 'DELETE'
47-
url = "groups/$($LicenseGroup.id)/members/$($User.id)/`$ref"
42+
# remove user from license groups. Mail-enabled security groups can't be modified through Graph, so those go through Exchange Online instead
43+
$GraphRemoveRequests = [System.Collections.Generic.List[object]]::new()
44+
$ExoRemoveRequests = [System.Collections.Generic.List[object]]::new()
45+
$ExoGroups = [System.Collections.Generic.List[object]]::new()
46+
47+
foreach ($LicenseGroup in $LicenseGroups) {
48+
$IsM365Group = $LicenseGroup.groupTypes -contains 'Unified'
49+
if ($LicenseGroup.mailEnabled -and -not $IsM365Group) {
50+
$ExoRemoveRequests.Add(@{
51+
CmdletInput = @{
52+
CmdletName = 'Remove-DistributionGroupMember'
53+
Parameters = @{
54+
Identity = $LicenseGroup.id
55+
Member = $User.id
56+
BypassSecurityGroupManagerCheck = $true
57+
}
58+
}
59+
})
60+
$ExoGroups.Add($LicenseGroup)
61+
} else {
62+
$GraphRemoveRequests.Add(@{
63+
id = $LicenseGroup.id
64+
method = 'DELETE'
65+
url = "groups/$($LicenseGroup.id)/members/$($User.id)/`$ref"
66+
})
4867
}
4968
}
5069

5170
Write-Information 'Removing user from groups with licenses'
52-
$RemoveResults = New-GraphBulkRequest -tenantid $tenantFilter -requests @($RemoveRequests)
53-
Write-Information ($RemoveResults | ConvertTo-Json -Depth 5)
54-
foreach ($Result in $RemoveResults) {
55-
$Group = $LicenseGroups | Where-Object { $_.id -eq $Result.id }
56-
$GroupName = $Group.displayName
5771

58-
if ($Result.status -eq 204) {
59-
Write-LogMessage -headers $Headers -API $APIName -message "Removed $($User.displayName) from license group $GroupName" -Sev 'Info' -tenant $TenantFilter
60-
"Removed $($User.displayName) from license group $GroupName"
61-
} else {
62-
Write-LogMessage -headers $Headers -API $APIName -message "Failed to remove $($User.displayName) from license group $GroupName. This is likely because its a Dynamic Group or synced with active directory." -Sev 'Error' -tenant $TenantFilter
63-
"Failed to remove $($User.displayName) from license group $GroupName. This is likely because its a Dynamic Group or synced with active directory."
72+
if ($GraphRemoveRequests.Count -gt 0) {
73+
$RemoveResults = New-GraphBulkRequest -tenantid $tenantFilter -requests @($GraphRemoveRequests)
74+
Write-Information ($RemoveResults | ConvertTo-Json -Depth 5)
75+
foreach ($Result in $RemoveResults) {
76+
$GroupName = ($LicenseGroups | Where-Object { $_.id -eq $Result.id }).displayName
77+
if ($Result.status -eq 204) {
78+
Write-LogMessage -headers $Headers -API $APIName -message "Removed $($User.displayName) from license group $GroupName" -Sev 'Info' -tenant $TenantFilter
79+
"Removed $($User.displayName) from license group $GroupName"
80+
} else {
81+
Write-LogMessage -headers $Headers -API $APIName -message "Failed to remove $($User.displayName) from license group $GroupName. This is likely because its a Dynamic Group or synced with active directory." -Sev 'Error' -tenant $TenantFilter
82+
"Failed to remove $($User.displayName) from license group $GroupName. This is likely because its a Dynamic Group or synced with active directory."
83+
}
84+
}
85+
}
86+
87+
if ($ExoRemoveRequests.Count -gt 0) {
88+
$RawExoRequest = New-ExoBulkRequest -tenantid $tenantFilter -cmdletArray @($ExoRemoveRequests)
89+
$LastError = $RawExoRequest | Select-Object -Last 1
90+
foreach ($ExoGroup in $ExoGroups) {
91+
if (!$LastError -or ($LastError.error -and $LastError.target -notcontains $User.id)) {
92+
Write-LogMessage -headers $Headers -API $APIName -message "Removed $($User.displayName) from mail-enabled license group $($ExoGroup.displayName)" -Sev 'Info' -tenant $TenantFilter
93+
"Removed $($User.displayName) from mail-enabled license group $($ExoGroup.displayName)"
94+
} else {
95+
Write-LogMessage -headers $Headers -API $APIName -message "Failed to remove $($User.displayName) from mail-enabled license group $($ExoGroup.displayName). This is likely because its a Dynamic Group or synced with active directory." -Sev 'Error' -tenant $TenantFilter
96+
"Failed to remove $($User.displayName) from mail-enabled license group $($ExoGroup.displayName)."
97+
}
6498
}
6599
}
66100
}

0 commit comments

Comments
 (0)