From 42bf551e2e2869f7706856dfe3c0d3f7f26b8469 Mon Sep 17 00:00:00 2001 From: JoeyInvictus <129975292+JoeyInvictus@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:54:29 +0100 Subject: [PATCH] Update 2.1.1 - Accepted pull reuqest from @Angrybender updating the date format in Get-UALGraph for improved readability and consistency. - Corrected a typo in the $filePath variable when using the -Download flag in Get-MessageIDs. - Implemented suggestions from @Calvindd2f to add additional parameters for connection scripts. Users can now connect using an access token. - Reworked the $areYouConnected functionality for the UAL scripts. - Introduced the -All parameter to Get-ADAuditLogsGraph. By default, filtering with the UserIds field retrieves only actions directly performed by the specified user. With the new -All flag, the command now includes all related events involving the user, such as events where an MFA device was added for them. - Fixed an issue where the merge output would throw "out of memory" errors. Now, while merging the output files, each file is written directly to the merged output file instead of reading everything into memory first and then saving it. As suggested by @evild3ad: - Updated the import command: Import-Module .\Microsoft-Extractor-Suite.psm1 -ArgumentList $true to suppress the logo output, optimizing it for automation scenarios. - Replaced remaining Write-Host commands in Get-Rules.ps1 with the custom Write-LogFile function for consistent logging. - Fixed an issue in Get-MailboxRules where using the -UserIDs flag with no rules found would incorrectly display the total inbox rules. - Added support for the -UserIds flag to Risky Users and Detections. - Added support for the -UserIds flag to the Get-MFA functionality. --- Microsoft-Extractor-Suite.psd1 | 2 +- Microsoft-Extractor-Suite.psm1 | 41 +++- Scripts/Connect.ps1 | 175 +++++++++++++- Scripts/Get-AzureADGraphLogs.ps1 | 52 ++-- Scripts/Get-MFAStatus.ps1 | 37 ++- Scripts/Get-MailItemsAccessed.ps1 | 2 +- Scripts/Get-RiskyEvents.ps1 | 224 +++++++++++++----- Scripts/Get-Rules.ps1 | 28 ++- Scripts/Get-UAL.ps1 | 8 +- docs/source/conf.py | 4 +- .../functionality/AzureAuditLogsGraph.rst | 8 + docs/source/functionality/GetUserInfo.rst | 8 + docs/source/index.rst | 13 +- 13 files changed, 475 insertions(+), 127 deletions(-) diff --git a/Microsoft-Extractor-Suite.psd1 b/Microsoft-Extractor-Suite.psd1 index b5fa7db..0da7b22 100644 --- a/Microsoft-Extractor-Suite.psd1 +++ b/Microsoft-Extractor-Suite.psd1 @@ -8,7 +8,7 @@ Author = 'Joey Rentenaar & Korstiaan Stam' CompanyName = 'Invictus-IR' # Version number of this module. -ModuleVersion = '2.1.0' +ModuleVersion = '2.1.1' # ID used to uniquely identify this module GUID = '4376306b-0078-4b4d-b565-e22804e3be01' diff --git a/Microsoft-Extractor-Suite.psm1 b/Microsoft-Extractor-Suite.psm1 index deecda9..17d408e 100644 --- a/Microsoft-Extractor-Suite.psm1 +++ b/Microsoft-Extractor-Suite.psm1 @@ -1,3 +1,7 @@ +param ( + [switch]$NoWelcome = $false +) + # Set supported TLS methods [Net.ServicePointManager]::SecurityProtocol = "Tls12, Tls13" @@ -5,7 +9,8 @@ $manifest = Import-PowerShellDataFile "$PSScriptRoot\Microsoft-Extractor-Suite.p $version = $manifest.ModuleVersion $host.ui.RawUI.WindowTitle = "Microsoft-Extractor-Suite $version" -$logo=@" +if (-not $NoWelcome) { + $logo=@" +-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+ |M|i|c|r|o|s|o|f|t| |E|x|t|r|a|c|t|o|r| |S|u|i|t|e| +-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+ @@ -13,7 +18,8 @@ Copyright 2024 Invictus Incident Response Created by Joey Rentenaar & Korstiaan Stam "@ -Write-Host $logo -ForegroundColor Yellow + Write-Host $logo -ForegroundColor Yellow +} $outputDir = "Output" if (!(test-path $outputDir)) { @@ -186,20 +192,33 @@ function Merge-OutputFiles { Write-LogFile -Message "[INFO] CSV files merged into $mergedPath" } 'JSON' { - $allJsonObjects = @() + "[" | Set-Content $mergedPath -Encoding UTF8 - Get-ChildItem $OutputDir -Filter *.json | ForEach-Object { - $jsonContent = Get-Content -Path $_.FullName -Raw | ConvertFrom-Json - if ($jsonContent -is [System.Collections.ArrayList] -or $jsonContent -is [System.Collections.Generic.List[object]]) { - $allJsonObjects += $jsonContent + $firstFile = $true + Get-ChildItem $OutputDir -Filter *.json | ForEach-Object { + $content = Get-Content -Path $_.FullName -Raw + + $content = $content.Trim() + if ($content.StartsWith('[')) { + $content = $content.Substring(1) + } + if ($content.EndsWith(']')) { + $content = $content.Substring(0, $content.Length - 1) } - else { - $allJsonObjects += @($jsonContent) + $content = $content.Trim() + + if (-not $firstFile -and $content) { + Add-Content -Path $mergedPath -Value "," -Encoding UTF8 + } + + if ($content) { + Add-Content -Path $mergedPath -Value $content -Encoding UTF8 + $firstFile = $false } } - $allJsonObjects | ConvertTo-Json -Depth 100 | Set-Content $mergedPath - Write-Host "[INFO] JSON files merged into $mergedPath" + "]" | Add-Content $mergedPath -Encoding UTF8 + Write-LogFile -Message "[INFO] JSON files merged into $mergedPath" } default { Write-LogFile -Message "[ERROR] Unsupported file type specified: $OutputType" -Color Red diff --git a/Scripts/Connect.ps1 b/Scripts/Connect.ps1 index 280cb4d..2df9a47 100644 --- a/Scripts/Connect.ps1 +++ b/Scripts/Connect.ps1 @@ -1,18 +1,177 @@ Function Connect-M365 { - versionCheck - Connect-ExchangeOnline > $null + PARAM( + [string] + $ConnectionUri, + [string] + $AzureADAuthorizationEndpointUri, + [ValidateSet('O365China', 'O365Default', 'O365GermanyCloud', 'O365USGovDoD', 'O365USGovGCCHigh')] + [string] + $ExchangeEnvironmentName, + [string[]] + $PSSessionOptions, + [string] + $DelegatedOrganization, + [string] + $Prefix, + [string[]] + $CommandName, + [string[]] + $FormatTypeName, + [string] + $AccessToken, + [string] + $AppId, + [switch] + $BypassMailboxAnchoring, + [X509Certificate] + $Certificate, + [string] + $CertificateFilePath, + [SecureString] + $CertificatePassword, + [string] + $CertificateThumbprint, + [PSCredential] + $Credential, + [switch] + $Device, + [switch] + $EnableErrorReporting, + [switch] + $InlineCredential, + [string] + $LogDirectoryPath, + [string] + $LogLevel, + [switch] + $ManagedIdentity, + [string] + $ManagedIdentityAccountId, + [string] + $Organization, + [int] + $PageSize, + [switch] + $ShowBanner, + [X509Certificate] + $SigningCertificate, + [switch] + $SkipLoadingCmdletHelp, + [switch] + $SkipLoadingFormatData, + [Boolean] + $TrackPerformance, + [Boolean] + $UseMultithreading, + [string] + $UserPrincipalName, + [Switch] + $UseRPSSession + ) + versionCheck + Connect-ExchangeOnline @PSBoundParameters > $null; } Function Connect-Azure { - versionCheck - Connect-AzureAD > $null + PARAM( + [ValidateSet('AzureChinaCloud', 'AzureCloud', 'AzureGermanyCloud', 'AzurePPE', 'AzureUSGovernment', 'AzureUSGovernment2', 'AzureUSGovernment3')] + [string] + $AzureEnvironmentName, + [string] + $TenantId, + [pscredential] + $Credential, + [string] + $CertificateThumbprint, + [string] + $ApplicationId, + [string] + $AadAccessToken, + [string] + $MsAccessToken, + [string] + $AccountId, + [ValidateSet('Error', 'Info', 'None', 'Warning')] + [string] + $LogLevel, + [string] + $LogFilePath, + [switch] + $WhatIf, + [switch] + $Confirm, + [Switch] + $Verbose, + [switch] + $Debug + ) + versionCheck + Connect-AzureAD @PSBoundParameters > $null; } Function Connect-AzureAZ { - versionCheck - Connect-AzAccount > $null -} - + PARAM( + [String] + $AccessToken , + [String] + $AccountId , + [String] + $ApplicationId , + [String] + $AuthScope , + [SecureString] + $CertificatePassword, + [String] + $CertificatePath , + [String] + $CertificateThumbprint , + [String] + $ContextName , + [PSCredential] + $Credential, + [string] + $DefaultProfile , + [String] + $Environment , + [String] + $FederatedToken , + [switch] + $Force , + [String] + $GraphAccessToken , + [switch] + $Identity, + [String] + $KeyVaultAccessToken , + [int] + $MaxContextPopulation, + [String] + $MicrosoftGraphAccessToken , + [ValidateSet('CurrentUser', 'Process')] + [string] + $Scope, + [switch] + $SendCertificateChain, + [switch] + $ServicePrincipal, + [switch] + $SkipContextPopulation , + [switch] + $SkipValidation , + [String] + $Subscription , + [String] + $Tenant , + [switch] + $UseDeviceAuthentication, + [switch] + $Confirm, + [switch] + $WhatIf + ) + versionCheck + Connect-AzAccount @PSBoundParameters > $null; +} \ No newline at end of file diff --git a/Scripts/Get-AzureADGraphLogs.ps1 b/Scripts/Get-AzureADGraphLogs.ps1 index 3523620..9e66261 100644 --- a/Scripts/Get-AzureADGraphLogs.ps1 +++ b/Scripts/Get-AzureADGraphLogs.ps1 @@ -81,7 +81,6 @@ function Get-ADSignInLogsGraph { $StartDate = $script:StartDate.ToString('yyyy-MM-ddTHH:mm:ssZ') $EndDate = $script:EndDate.ToString('yyyy-MM-ddTHH:mm:ssZ') - $TotalTicks = ($script:EndDate-$script:StartDate).Ticks $filterQuery = "createdDateTime ge $StartDate and createdDateTime le $EndDate" if ($UserIds) { @@ -100,16 +99,18 @@ function Get-ADSignInLogsGraph { $date = [datetime]::Now.ToString('yyyyMMddHHmmss') $filePath = Join-Path -Path $OutputDir -ChildPath "$($date)-SignInLogsGraph.json" - $responseJson.value | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding - $dates = $responseJson.value | ForEach-Object { [DateTime]::Parse($_.CreatedDateTime) } | Sort-Object + $responseJson.value | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding + #$dates = $responseJson.value | ForEach-Object { [DateTime]::Parse($_.CreatedDateTime) } | Sort-Object + + $dates = $responseJson.value | ForEach-Object { + [DateTime]::Parse($_.CreatedDateTime, [System.Globalization.CultureInfo]::InvariantCulture) + } | Sort-Object + $from = $dates | Select-Object -First 1 - $fromstr = $from.ToString('yyyy-MM-ddTHH:mmZ') - $to = ($dates | Select-Object -Last 1).ToString('yyyy-MM-ddTHH:mmZ') + # $fromstr = $from.ToString('yyyy-MM-ddTHH:mmZ') + $to = ($dates | Select-Object -Last 1) #.ToString('yyyy-MM-ddTHH:mmZ') $count = ($responseJson.value | measure).Count - Write-Host "[INFO] Sign-in logs written to $filePath ($count records between $fromStr and $to)" -ForegroundColor Green - - $progress = [Math]::Round(($script:EndDate-$from).Ticks / $TotalTicks * 100, 2) - Write-Progress -Activity "Collecting Sign-in logs" -Status "$progress% Complete" -PercentComplete $progress + Write-LogFile -Message "[INFO] Sign-in logs written to $filePath ($count records between $from and $to)" -Color Green } $apiUrl = $responseJson.'@odata.nextLink' } While ($apiUrl) @@ -147,6 +148,9 @@ function Get-ADAuditLogsGraph { .PARAMETER UserIds UserIds is the UserIds parameter filtering the log entries by the account of the user who performed the actions. + .PARAMETER All + When specified along with UserIds, this parameter filters the results to include events where the provided UserIds match any user principal name found in either the userPrincipalNames or targetResources fields. + .PARAMETER Encoding Encoding is the parameter specifying the encoding of the JSON output file. Default: UTF8 @@ -163,6 +167,10 @@ function Get-ADAuditLogsGraph { Get-ADAuditLogsGraph -Application Get directory audit logs via application authentication. + .EXAMPLE + Get-ADAuditLogsGraph -UserIds 'user@example.com' -All + Get sign-in logs for 'user@example.com', including both userPrincipalName and targetResources in the filter. + .EXAMPLE Get-ADAuditLogsGraph -Before 2023-04-12 Get directory audit logs before 2023-04-12. @@ -178,7 +186,8 @@ function Get-ADAuditLogsGraph { [string]$OutputDir, [string]$Encoding = "UTF8", [switch]$MergeOutput, - [string]$UserIds + [string]$UserIds, + [switch]$All ) $requiredScopes = @("AuditLog.Read.All", "Directory.Read.All") @@ -209,12 +218,20 @@ function Get-ADAuditLogsGraph { $StartDate = $script:StartDate.ToString('yyyy-MM-ddTHH:mm:ssZ') $EndDate = $script:EndDate.ToString('yyyy-MM-ddTHH:mm:ssZ') - $TotalTicks = ($script:EndDate-$script:StartDate).Ticks $filterQuery = "activityDateTime ge $StartDate and activityDateTime le $EndDate" if ($UserIds) { $filterQuery += " and startsWith(initiatedBy/user/userPrincipalName, '$UserIds')" + + if ($All.IsPresent) { + $filterQuery = "($filterQuery) or (targetResources/any(tr: tr/userPrincipalName eq '$UserIds'))" + } } + else { + if ($All.IsPresent) { + Write-LogFile -Message "[WARNING] '-All' switch has no effect without specifying UserIds" + } + } $encodedFilterQuery = [System.Web.HttpUtility]::UrlEncode($filterQuery) $apiUrl = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=$encodedFilterQuery" @@ -227,15 +244,18 @@ function Get-ADAuditLogsGraph { $date = [datetime]::Now.ToString('yyyyMMddHHmmss') $filePath = Join-Path -Path $OutputDir -ChildPath "$($date)-AuditLogs.json" $responseJson.value | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding - $dates = $responseJson.value | ForEach-Object { [DateTime]::Parse($_.activityDateTime) } | Sort-Object + + #$dates = $responseJson.value | ForEach-Object { [DateTime]::Parse($_.activityDateTime) } | Sort-Object + + $dates = $responseJson.value | ForEach-Object { + [DateTime]::Parse($_.activityDateTime, [System.Globalization.CultureInfo]::InvariantCulture) + } | Sort-Object + $from = $dates | Select-Object -First 1 $fromstr = $from.ToString('yyyy-MM-ddTHH:mmZ') $to = ($dates | Select-Object -Last 1).ToString('yyyy-MM-ddTHH:mmZ') $count = ($responseJson.value | measure).Count - Write-Host "[INFO] Audit logs written to $filePath ($count records between $fromstr and $to)" -ForegroundColor Green - - $progress = [Math]::Round(($script:EndDate-$from).Ticks / $TotalTicks * 100, 2) - Write-Progress -Activity "Collecting Audit logs" -Status "$progress% Complete" -PercentComplete $progress + Write-LogFile -Message "[INFO] Audit logs written to $filePath ($count records between $fromstr and $to)" -Color Green } $apiUrl = $responseJson.'@odata.nextLink' } While ($apiUrl) diff --git a/Scripts/Get-MFAStatus.ps1 b/Scripts/Get-MFAStatus.ps1 index 4a834eb..5b749c3 100644 --- a/Scripts/Get-MFAStatus.ps1 +++ b/Scripts/Get-MFAStatus.ps1 @@ -34,7 +34,8 @@ function Get-MFA { [CmdletBinding()] param( [string]$OutputDir = "Output\MFA", - [string]$Encoding = "UTF8" + [string]$Encoding = "UTF8", + [string[]]$UserIds ) $requiredScopes = @("UserAuthenticationMethod.Read.All","User.Read.All") @@ -59,13 +60,25 @@ function Get-MFA { $results = @() $allUsers = @() - $nextLink = "https://graph.microsoft.com/v1.0/users" - do { - $response = Invoke-MgGraphRequest -Uri $nextLink -Method Get -OutputType PSObject - $allUsers += $response.value - $nextLink = $response.'@odata.nextLink' - } while ($nextLink) + if ($UserIds) { + foreach ($userId in $UserIds) { + $userUri = "https://graph.microsoft.com/v1.0/users/$userId" + try { + $user = Invoke-MgGraphRequest -Uri $userUri -Method Get -OutputType PSObject + $allUsers += $user + } catch { + Write-LogFile -Message "[WARNING] User with ID $userId not found" -Color "Yellow" + } + } + } else { + $nextLink = "https://graph.microsoft.com/v1.0/users" + do { + $response = Invoke-MgGraphRequest -Uri $nextLink -Method Get -OutputType PSObject + $allUsers += $response.value + $nextLink = $response.'@odata.nextLink' + } while ($nextLink) + } $MFAEmail = 0 $MFAfido2 = 0 @@ -199,11 +212,13 @@ function Get-MFA { $nextLink = $response.'@odata.nextLink' ForEach ($detail in $userDetails) { - $myObject = [PSCustomObject]@{} - $detail.PSObject.Properties | ForEach-Object { - $myObject | Add-Member -Type NoteProperty -Name $_.Name -Value $_.Value + if (!$UserIds -or $UserIds -contains $detail.userPrincipalName) { + $myObject = [PSCustomObject]@{} + $detail.PSObject.Properties | ForEach-Object { + $myObject | Add-Member -Type NoteProperty -Name $_.Name -Value $_.Value + } + $results += $myObject } - $results += $myObject } } while ($nextLink) diff --git a/Scripts/Get-MailItemsAccessed.ps1 b/Scripts/Get-MailItemsAccessed.ps1 index 7be6d08..166bcff 100644 --- a/Scripts/Get-MailItemsAccessed.ps1 +++ b/Scripts/Get-MailItemsAccessed.ps1 @@ -680,7 +680,7 @@ function DownloadMails($iMessageID,$UserIds){ $subject = $getMessage.Subject $subject = $subject -replace '[\\/:*?"<>|]', '_' - $filePath = "$outputDir\$ReceivedDateTime-$subject.elm" + $filePath = "$outputDir\$ReceivedDateTime-$subject.eml" try { Get-MgUserMessageContent -MessageId $messageId -UserId $userId -OutFile $filePath diff --git a/Scripts/Get-RiskyEvents.ps1 b/Scripts/Get-RiskyEvents.ps1 index 896e092..dc374ba 100644 --- a/Scripts/Get-RiskyEvents.ps1 +++ b/Scripts/Get-RiskyEvents.ps1 @@ -13,6 +13,10 @@ function Get-RiskyUsers { .PARAMETER Encoding Encoding is the parameter specifying the encoding of the CSV output file. Default: UTF8 + + .PARAMETER UserIds + An array of User IDs to retrieve risky user information for. + If not specified, retrieves all risky users. .EXAMPLE Get-RiskyUsers @@ -24,12 +28,17 @@ function Get-RiskyUsers { .EXAMPLE Get-RiskyUsers -OutputDir C:\Windows\Temp - Retrieves all risky users and saves the output to the C:\Windows\Temp folder. + Retrieves all risky users and saves the output to the C:\Windows\Temp folder. + + .EXAMPLE + Get-RiskyUsers -UserIds "user-id-1","user-id-2" + Retrieves risky user information for the specified User IDs. #> [CmdletBinding()] param( [string]$OutputDir = "Output\RiskyEvents", - [string]$Encoding = "UTF8" + [string]$Encoding = "UTF8", + [string[]]$UserIds ) $requiredScopes = @("IdentityRiskEvent.Read.All","IdentityRiskyUser.Read.All") @@ -54,31 +63,68 @@ function Get-RiskyUsers { $count = 0 try { - $uri = "https://graph.microsoft.com/v1.0/identityProtection/riskyUsers" - do { - $response = Invoke-MgGraphRequest -Method GET -Uri $uri - - if ($response.value) { - foreach ($user in $response.value) { - $results += [PSCustomObject]@{ - Id = $user.Id - IsDeleted = $user.IsDeleted - IsProcessing = $user.IsProcessing - RiskDetail = $user.RiskDetail - RiskLastUpdatedDateTime = $user.RiskLastUpdatedDateTime - RiskLevel = $user.RiskLevel - RiskState = $user.RiskState - UserDisplayName = $user.UserDisplayName - UserPrincipalName = $user.UserPrincipalName - AdditionalProperties = $user.AdditionalProperties -join ", " - } + $baseUri = "https://graph.microsoft.com/v1.0/identityProtection/riskyUsers" + + if ($UserIds) { + foreach ($userId in $UserIds) { + $encodedUserId = [System.Web.HttpUtility]::UrlEncode($userId) + $uri = "$baseUri`?`$filter=userPrincipalName eq '$encodedUserId'" + Write-LogFile -Message "[INFO] Retrieving risky user for UPN: $userId" + + try { + $response = Invoke-MgGraphRequest -Method GET -Uri $uri - $count++ + if ($response.value -and $response.value.Count -gt 0) { + foreach ($user in $response.value) { + $results += [PSCustomObject]@{ + Id = $user.Id + IsDeleted = $user.IsDeleted + IsProcessing = $user.IsProcessing + RiskDetail = $user.RiskDetail + RiskLastUpdatedDateTime = $user.RiskLastUpdatedDateTime + RiskLevel = $user.RiskLevel + RiskState = $user.RiskState + UserDisplayName = $user.UserDisplayName + UserPrincipalName = $user.UserPrincipalName + AdditionalProperties = $user.AdditionalProperties -join ", " + } + $count++ + } + } else { + Write-LogFile -Message "[INFO] User ID $userId not found or not risky." + } + } catch { + Write-LogFile -Message "[ERROR] Failed to retrieve data for User ID $userId : $($_.Exception.Message)" -Color "Red" } } - - $uri = $response.'@odata.nextLink' - } while ($uri -ne $null) + } + else { + $uri = "https://graph.microsoft.com/v1.0/identityProtection/riskyUsers" + do { + $response = Invoke-MgGraphRequest -Method GET -Uri $uri + + if ($response.value) { + foreach ($user in $response.value) { + $results += [PSCustomObject]@{ + Id = $user.Id + IsDeleted = $user.IsDeleted + IsProcessing = $user.IsProcessing + RiskDetail = $user.RiskDetail + RiskLastUpdatedDateTime = $user.RiskLastUpdatedDateTime + RiskLevel = $user.RiskLevel + RiskState = $user.RiskState + UserDisplayName = $user.UserDisplayName + UserPrincipalName = $user.UserPrincipalName + AdditionalProperties = $user.AdditionalProperties -join ", " + } + + $count++ + } + } + + $uri = $response.'@odata.nextLink' + } while ($uri -ne $null) + } } catch { Write-LogFile -Message "[ERROR] An error occurred: $($_.Exception.Message)" -Color "Red" throw @@ -111,6 +157,10 @@ function Get-RiskyDetections { .PARAMETER Encoding Encoding is the parameter specifying the encoding of the CSV output file. Default: UTF8 + + .PARAMETER UserIds + An array of User IDs to retrieve risky detections information for. + If not specified, retrieves all risky detections. .EXAMPLE Get-RiskyDetections @@ -122,12 +172,17 @@ function Get-RiskyDetections { .EXAMPLE Get-RiskyDetections -OutputDir C:\Windows\Temp - Retrieves the risky detections and saves the output to the C:\Windows\Temp folder. + Retrieves the risky detections and saves the output to the C:\Windows\Temp folder. + + .EXAMPLE + Get-RiskyDetections -UserIds "user-id-1","user-id-2" + Retrieves risky detections for the specified User IDs. #> [CmdletBinding()] param( [string]$OutputDir= "Output\RiskyEvents", - [string]$Encoding = "UTF8" + [string]$Encoding = "UTF8", + [string[]]$UserIds ) $requiredScopes = @("IdentityRiskEvent.Read.All","IdentityRiskyUser.Read.All") @@ -154,46 +209,93 @@ function Get-RiskyDetections { $count = 0 try { - $uri = "https://graph.microsoft.com/v1.0/identityProtection/riskDetections" - do { - $response = Invoke-MgGraphRequest -Method GET -Uri $uri - - if ($response.value) { - foreach ($detection in $response.value) { - $results += [PSCustomObject]@{ - Activity = $detection.Activity - ActivityDateTime = $detection.ActivityDateTime - AdditionalInfo = $detection.AdditionalInfo - CorrelationId = $detection.CorrelationId - DetectedDateTime = $detection.DetectedDateTime - IPAddress = $detection.IPAddress - Id = $detection.Id - LastUpdatedDateTime = $detection.LastUpdatedDateTime - City = $detection.Location.City - CountryOrRegion = $detection.Location.CountryOrRegion - State = $detection.Location.State - RequestId = $detection.RequestId - RiskDetail = $detection.RiskDetail - RiskEventType = $detection.RiskEventType - RiskLevel = $detection.RiskLevel - RiskState = $detection.RiskState - DetectionTimingType = $detection.DetectionTimingType - Source = $detection.Source - TokenIssuerType = $detection.TokenIssuerType - UserDisplayName = $detection.UserDisplayName - UserId = $detection.UserId - UserPrincipalName = $detection.UserPrincipalName - AdditionalProperties = $detection.AdditionalProperties -join ", " + $baseUri = "https://graph.microsoft.com/v1.0/identityProtection/riskDetections" + + if ($UserIds) { + foreach ($userId in $UserIds) { + $encodedUserId = [System.Web.HttpUtility]::UrlEncode($userId) + $uri = "$baseUri`?`$filter=UserPrincipalName eq '$encodedUserId'" + Write-LogFile -Message "[INFO] Retrieving risky detections for User ID: $userId" + + do { + $response = Invoke-MgGraphRequest -Method GET -Uri $uri + + if ($response.value) { + foreach ($detection in $response.value) { + $results += [PSCustomObject]@{ + Activity = $detection.Activity + ActivityDateTime = $detection.ActivityDateTime + AdditionalInfo = $detection.AdditionalInfo + CorrelationId = $detection.CorrelationId + DetectedDateTime = $detection.DetectedDateTime + IPAddress = $detection.IPAddress + Id = $detection.Id + LastUpdatedDateTime = $detection.LastUpdatedDateTime + City = $detection.Location.City + CountryOrRegion = $detection.Location.CountryOrRegion + State = $detection.Location.State + RequestId = $detection.RequestId + RiskDetail = $detection.RiskDetail + RiskEventType = $detection.RiskEventType + RiskLevel = $detection.RiskLevel + RiskState = $detection.RiskState + DetectionTimingType = $detection.DetectionTimingType + Source = $detection.Source + TokenIssuerType = $detection.TokenIssuerType + UserDisplayName = $detection.UserDisplayName + UserId = $detection.UserId + UserPrincipalName = $detection.UserPrincipalName + AdditionalProperties = $detection.AdditionalProperties -join ", " + } + $count++ + } } - $count++ - } + + $uri = $response.'@odata.nextLink' + } while ($uri -ne $null) } + } + else { + do { + $response = Invoke-MgGraphRequest -Method GET -Uri $baseUri + + if ($response.value) { + foreach ($detection in $response.value) { + $results += [PSCustomObject]@{ + Activity = $detection.Activity + ActivityDateTime = $detection.ActivityDateTime + AdditionalInfo = $detection.AdditionalInfo + CorrelationId = $detection.CorrelationId + DetectedDateTime = $detection.DetectedDateTime + IPAddress = $detection.IPAddress + Id = $detection.Id + LastUpdatedDateTime = $detection.LastUpdatedDateTime + City = $detection.Location.City + CountryOrRegion = $detection.Location.CountryOrRegion + State = $detection.Location.State + RequestId = $detection.RequestId + RiskDetail = $detection.RiskDetail + RiskEventType = $detection.RiskEventType + RiskLevel = $detection.RiskLevel + RiskState = $detection.RiskState + DetectionTimingType = $detection.DetectionTimingType + Source = $detection.Source + TokenIssuerType = $detection.TokenIssuerType + UserDisplayName = $detection.UserDisplayName + UserId = $detection.UserId + UserPrincipalName = $detection.UserPrincipalName + AdditionalProperties = $detection.AdditionalProperties -join ", " + } + $count++ + } + } - $uri = $response.'@odata.nextLink' - } while ($uri -ne $null) + $baseUri = $response.'@odata.nextLink' + } while ($baseUri -ne $null) + } } catch { Write-LogFile -Message "[ERROR] An error occurred: $($_.Exception.Message)" -Color "Red" - Write-LogFile -Message "[ERROR (Continued)] Check the below, as the target tenant may not be licenced for this feature $($_.ErrorDetails.Message)" -Color "Red" + Write-LogFile -Message "[ERROR (Continued)] Check the below, as the target tenant may not be licenced for this feature $($_.ErrorDetails.Message)" -Color "Red" throw } diff --git a/Scripts/Get-Rules.ps1 b/Scripts/Get-Rules.ps1 index 20848aa..815e467 100644 --- a/Scripts/Get-Rules.ps1 +++ b/Scripts/Get-Rules.ps1 @@ -126,7 +126,7 @@ function Show-MailboxRules else { if ($UserIds -match ",") { - $UserIds.Split(",") | Foreach { + $UserIds.Split(",") | ForEach-Object { $user = $_ Write-Output ('[INFO] Checking {0}...' -f $user) @@ -167,8 +167,15 @@ function Show-MailboxRules } } } + + if ($amountofRules -gt 0) { + write-LogFile -Message "[INFO] A total of $amountofRules Inbox Rules found" -Color "Green" + } + else { + write-LogFile -Message "[INFO] No Inbox Rules found!" -Color "Yellow" + } - write-LogFile -Message "[INFO] A total of $amountofRules InboxRules found" -Color "Green" + } function Get-MailboxRules @@ -228,7 +235,7 @@ function Get-MailboxRules $totalRules = 0 Get-mailbox -resultsize unlimited | ForEach-Object { - Write-Output ('[INFO] Checking {0}...' -f $_.UserPrincipalName) + write-LogFile -Message ('[INFO] Checking {0}...' -f $_.UserPrincipalName) $inboxrule = Get-inboxrule -Mailbox $_.UserPrincipalName if ($inboxrule) { @@ -259,10 +266,10 @@ function Get-MailboxRules else { if ($UserIds -match ",") { - $UserIds.Split(",") | Foreach { + $UserIds.Split(",") | ForEach-Object { $User = $_ - Write-Output ('[INFO] Checking {0}...' -f $User) + write-LogFile -Message ('[INFO] Checking {0}...' -f $User) $inboxrule = get-inboxrule -Mailbox $User if ($inboxrule) { $amountofRules = 0 @@ -294,7 +301,7 @@ function Get-MailboxRules Write-Output ('[INFO] Checking {0}...' -f $UserIds) $inboxrule = get-inboxrule -Mailbox $UserIds if ($inboxrule) { - write-host ('[INFO] Found InboxRule(s) for: {0}...' -f $UserIds) -ForegroundColor Yellow + write-LogFile -Message ('[INFO] Found InboxRule(s) for: {0}...' -f $UserIds) -ForegroundColor Yellow foreach($rule in $inboxrule){ $amountofRules = $amountofRules + 1 $tempval = [pscustomobject]@{ @@ -318,6 +325,11 @@ function Get-MailboxRules } } - write-LogFile -Message "[INFO] A total of $totalRules InboxRules found!" -Color "Green" - write-LogFile -Message "[INFO] InboxRules rules are collected and writen to: $outputDirectory" -Color "Green" + if ($RuleCount -gt 0) { + write-LogFile -Message "[INFO] A total of $RuleCount Inbox Rules found!" -Color "Green" + write-LogFile -Message "[INFO] Inbox rules are collected and written to: $outputDirectory" -Color "Green" + } + else { + write-LogFile -Message "[INFO] No Inbox Rules found!" -Color "Yellow" + } } diff --git a/Scripts/Get-UAL.ps1 b/Scripts/Get-UAL.ps1 index 3631302..eee634a 100644 --- a/Scripts/Get-UAL.ps1 +++ b/Scripts/Get-UAL.ps1 @@ -88,7 +88,7 @@ function Get-UALAll ) try { - $areYouConnected = Get-AdminAuditLogConfig -ErrorAction stop + $areYouConnected = Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-1) -EndDate (Get-Date) -ResultSize 1 -ErrorAction Stop } catch { write-logFile -Message "[INFO] Ensure you are connected to M365 by running the Connect-M365 command before executing this script" -Color "Yellow" @@ -326,7 +326,7 @@ function Get-UALGroup ) try { - $areYouConnected = Get-AdminAuditLogConfig -ErrorAction stop + $areYouConnected = Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-1) -EndDate (Get-Date) -ResultSize 1 -ErrorAction Stop } catch { write-logFile -Message "[INFO] Ensure you are connected to M365 by running the Connect-M365 command before executing this script" -Color "Yellow" @@ -604,7 +604,7 @@ function Get-UALSpecific ) try { - $areYouConnected = Get-AdminAuditLogConfig -ErrorAction stop + $areYouConnected = Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-1) -EndDate (Get-Date) -ResultSize 1 -ErrorAction Stop } catch { write-logFile -Message "[INFO] Ensure you are connected to M365 by running the Connect-M365 command before executing this script" -Color "Yellow" @@ -854,7 +854,7 @@ function Get-UALSpecificActivity ) try { - $areYouConnected = Get-AdminAuditLogConfig -ErrorAction stop + $areYouConnected = Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-1) -EndDate (Get-Date) -ResultSize 1 -ErrorAction Stop } catch { write-logFile -Message "[INFO] Ensure you are connected to M365 by running the Connect-M365 command before executing this script" -Color "Yellow" diff --git a/docs/source/conf.py b/docs/source/conf.py index 5e9cf60..6720ea0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,8 +6,8 @@ copyright = 'Copyright 2024 Invictus Incident Response' author = 'Joey Rentenaar & Korstiaan Stam' -release = '2.1.0' -version = '2.1.0' +release = '2.1.1' +version = '2.1.1' # -- General configuration diff --git a/docs/source/functionality/AzureAuditLogsGraph.rst b/docs/source/functionality/AzureAuditLogsGraph.rst index 11ff786..cc645b2 100644 --- a/docs/source/functionality/AzureAuditLogsGraph.rst +++ b/docs/source/functionality/AzureAuditLogsGraph.rst @@ -19,6 +19,11 @@ Get the Azure Active Directory Audit Log after 2023-04-12: Get-ADAuditLogsGraph -endDate 2023-04-12 +Get sign-in logs for 'user@example.com', including both userPrincipalName and targetResources in the filter: +:: + + Get-ADAuditLogsGraph -UserIds 'user@example.com' -All + Parameters """""""""""""""""""""""""" -startDate (optional) @@ -41,6 +46,9 @@ Parameters -UserIds (optional) - UserIds is the UserIds parameter filtering the log entries by the account of the user who performed the actions. +-All (optional) + - When specified along with UserIds, this parameter filters the results to include events where the provided UserIds match any user principal name found in either the userPrincipalNames or targetResources fields. + Output """""""""""""""""""""""""" The output will be saved to the 'AzureAD' directory within the 'Output' directory, with the file name 'AuditlogsGraph.json'. Each time an acquisition is performed, the output JSON file will be overwritten. Therefore, if you perform multiple acquisitions, the JSON file will only contain the results from the latest acquisition. diff --git a/docs/source/functionality/GetUserInfo.rst b/docs/source/functionality/GetUserInfo.rst index f018bd4..07a688c 100644 --- a/docs/source/functionality/GetUserInfo.rst +++ b/docs/source/functionality/GetUserInfo.rst @@ -141,6 +141,10 @@ Parameters - Encoding is the parameter specifying the encoding of the CSV/JSON output file. - Default: UTF8 +-UserIds (optional) + - An array of User IDs to retrieve risky user information for. + - Default: If not specified, retrieves all risky users. + Output """""""""""""""""""""""""" The output will be saved to the 'RiskyEvents' directory within the 'Output' directory. @@ -172,6 +176,10 @@ Parameters - Encoding is the parameter specifying the encoding of the CSV/JSON output file. - Default: UTF8 +-UserIds (optional) + - An array of User IDs to retrieve risky detections information for. + - Default: If not specified, retrieves all risky detections. + Output """""""""""""""""""""""""" The output will be saved to the 'RiskyEvents' directory within the 'Output' directory. diff --git a/docs/source/index.rst b/docs/source/index.rst index 52a0bb0..69fef39 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,7 +8,7 @@ Microsoft-Extractor-Suite documentation! .. note:: - 🆘 Incident Response support reach out to cert@invictus-ir.com or go to https://www.invictus-ir.com/247 + 🆘 Incident Response support reach out to cert@invictus-ir.com or go to https://www.invictus-ir.com/24-7-emergency-response Supported sources ------- @@ -60,12 +60,17 @@ To import the Microsoft-Extractor-Suite: Import-Module .\Microsoft-Extractor-Suite.psd1 +To import the Microsoft-Extractor-Suite without the logo output: +:: + + Import-Module .\Microsoft-Extractor-Suite.psd1 -ArgumentList $true + Additionally, you must sign-in to Microsoft 365 or Azure depending on your usage before M365-Toolkit functions are made available. To sign in, use the cmdlets: :: - Connect-M365 - Connect-Azure - Connect-AzureAZ + Connect-M365 or Connect-ExchangeOnline + Connect-Azure or Connect-AzureAD + Connect-AzureAZ or Connect-AzAccount Getting Help ------------