diff --git a/DFIR-O365RC/DFIR-O365RC.psd1 b/DFIR-O365RC/DFIR-O365RC.psd1 index 8bc3b72..11e75e2 100755 --- a/DFIR-O365RC/DFIR-O365RC.psd1 +++ b/DFIR-O365RC/DFIR-O365RC.psd1 @@ -3,89 +3,107 @@ # @{ - -# Script module or binary module file associated with this manifest. -RootModule = '.\DFIR-O365RC.psm1' - -# Version number of this module. -ModuleVersion = '1.2.0' - -# Supported PSEditions -CompatiblePSEditions = 'Core', 'Desktop' - -# ID used to uniquely identify this module -GUID = '84b1ed98-447f-4d4e-aa52-fd9339cf7cca' - -# Author of this module -Author = 'leonard.savina@ssi.gouv.fr' - -# Company or vendor of this module -CompanyName = 'CERT-FR' - -# Description of the functionality provided by this module -Description = 'The DFIR-O365RC module will extract logs from O365 Unified audit logs, Azure AD signin logs, Azure AD audit logs, Azure RM and DevOps activity logs' - - -# Minimum version of the Windows PowerShell engine required by this module -PowerShellVersion = '5.0' - - - -# Modules that must be imported into the global environment prior to importing this module -RequiredModules = @( - @{ModuleName = 'PoshRSJob'; ModuleVersion = '1.7.4.4'; }, - @{ModuleName = 'MSAL.PS'; ModuleVersion = '4.37.0.0'; } - @{ModuleName = 'ExchangeOnlineManagement'; ModuleVersion = '3.1.0'; } + # Script module or binary module file associated with this manifest. + RootModule = '.\DFIR-O365RC.psm1' + + # Version number of this module. + ModuleVersion = '2.0.0' + + # Supported PSEditions + CompatiblePSEditions = 'Core', 'Desktop' + + # ID used to uniquely identify this module + GUID = '84b1ed98-447f-4d4e-aa52-fd9339cf7cca' + + # Author of this module + Author = 'leonard.savina@ssi.gouv.fr' + + # Company or vendor of this module + CompanyName = 'CERT-FR' + + # Description of the functionality provided by this module + Description = 'The DFIR-O365RC module will extract logs from the unified audit log (using Exchange Online and Purview), Entra ID Sign In logs, Entra ID Audit Logs, Azure Monitor and Azure DevOps activity logs' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @( + @{ModuleName = 'Az.Accounts'; ModuleVersion = '3.0.2'; } + @{ModuleName = 'Az.Monitor'; ModuleVersion = '5.2.1'; } + @{ModuleName = 'Az.Resources'; ModuleVersion = '7.2.0'; } + @{ModuleName = 'ExchangeOnlineManagement'; ModuleVersion = '3.5.1'; } + @{ModuleName = 'Microsoft.Graph.Authentication'; ModuleVersion = '2.20.0'; } + @{ModuleName = 'Microsoft.Graph.Applications'; ModuleVersion = '2.20.0'; } + @{ModuleName = 'Microsoft.Graph.Beta.Reports'; ModuleVersion = '2.20.0'; } + @{ModuleName = 'Microsoft.Graph.Beta.Security'; ModuleVersion = '2.20.0'; } + @{ModuleName = 'Microsoft.Graph.Identity.DirectoryManagement'; ModuleVersion = '2.20.0'; } + @{ModuleName = 'PoshRSJob'; ModuleVersion = '1.7.4.4'; } ) + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + NestedModules = @( + 'Get-AADApps.ps1', + 'Get-AADDevices.ps1', + 'Get-AADLogs.ps1', + 'Get-AzDevOpsActivityLogs.ps1', + 'Get-AzRMActivityLogs.ps1', + 'Get-O365.ps1', + 'Manage-Applications.ps1' + ) + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Connect-AzApplication', + 'Connect-AzUser', + 'Connect-ExchangeOnlineApplication', + 'Connect-ExchangeOnlineUser', + 'Connect-MicrosoftGraphApplication', + 'Connect-MicrosoftGraphUser', + 'Get-AADApps', + 'Get-AADDevices', + 'Get-AADLogs', + 'Get-AzDevOpsActivityLogs', + 'Get-AzDevOpsAuditLogs', + 'Get-AzDevOpsRestAPIResponseUser', + 'Get-AzRMActivityLogs', + 'Get-AzureRMActivityLog', + 'Get-LargeUnifiedAuditLog', + 'Get-MailboxAuditLog', + 'Get-MicrosoftGraphLogs', + 'Get-O365Defender', + 'Get-O365Full', + 'Get-O365Light', + 'Get-UnifiedAuditLogPurview', + 'New-Application', + 'Remove-Application', + 'Import-Certificate', + 'Search-O365', + 'Update-Application', + 'Write-Log' + ) -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess - -NestedModules = @( - 'Get-O365Full.ps1', - 'Get-O365Light.ps1', - 'Get-AADApps.ps1', - 'Get-DefenderforO365.ps1', - 'Search-O365.ps1', - 'Get-AADDevices.ps1', - 'Get-AzRMActivityLogs.ps1', - 'Get-AzDevOpsActivityLogs.ps1', - 'Get-AADLogs.ps1' -) - -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Get-OAuthToken', 'Get-RestAPIResponse', 'Connect-EXOPsearchUnified', 'Get-LargeUnifiedAuditLog', 'Get-MailboxAuditLog', 'Get-AADApps', 'Get-AADLogs', 'Get-O365Full', 'Get-O365Light', 'Get-DefenderforO365', 'Search-O365', 'Get-AADDevices', 'Get-AzRMActivityLogs', 'Write-Log', 'Get-AzDevOpsActivityLogs' - - -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = @() - -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = @() - - -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @("O365","Security","Forensics","DFIR","Exchange","Defender","AzureAD","MSGraph","Azure", "DevOps") - - - # ReleaseNotes of this module - ReleaseNotes =' - 1.0.0 - Initial release - 1.1.0 - Added Get-AADDevices and Get-AzRMActivityLogs functions - 1.2.0 - Added Get-AzDevOpsActivityLogs function and added mailobx audit logs retrieval to the Search-o365 function - ' - + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() - } # End of PSData hashtable + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() -} # End of PrivateData hashtable + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @("O365", "Security", "Forensics", "DFIR", "Exchange", "Defender", "AzureAD", "MSGraph", "Azure", "DevOps", "Purview", "Entra ID", "Logs") + # ReleaseNotes of this module + ReleaseNotes = ' + 1.0.0 - Initial release + 1.1.0 - Added Get-AADDevices and Get-AzRMActivityLogs functions + 1.2.0 - Added Get-AzDevOpsActivityLogs function and added mailobx audit logs retrieval to the Search-O365 function + 2.0.0 - Rework of the project: use of an application to do the log collection, instead of an authenticated user. Add Purview + ' + } # End of PSData hashtable + } # End of PrivateData hashtable } diff --git a/DFIR-O365RC/DFIR-O365RC.psm1 b/DFIR-O365RC/DFIR-O365RC.psm1 index c83bd4e..605027f 100755 --- a/DFIR-O365RC/DFIR-O365RC.psm1 +++ b/DFIR-O365RC/DFIR-O365RC.psm1 @@ -1,484 +1,1010 @@ -Function Write-Log { - - Param - ( - [Parameter(Mandatory=$true, - ValueFromPipeline = $true)] - [ValidateNotNullOrEmpty()] - [string]$Message, - - [Parameter(Mandatory=$true)] - [Alias('LogPath')] - [string]$Path, - - [Parameter(Mandatory=$false)] - [ValidateSet("Error","Warning","Info")] - [Alias('LogLevel')] - [string]$Level="Info" - - ) - - $logtime = "{0:yyyy-MM-dd} {0:HH:mm:ss}" -f (get-date) + "," - - switch ($Level) { - 'Error' { - $LevelText = 'ERROR,' - } - 'Warning' { - $LevelText = 'WARNING,' - } - 'Info' { - $LevelText = 'INFO,' - } - } - - "$logtime $LevelText $Message" | Out-File -FilePath $Path -Append -Encoding UTF8 -} +function Write-Log { + param + ( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [ValidateNotNullOrEmpty()] + [String]$message, + [Parameter(Mandatory=$true)] + [Alias("LogPath")] + [String]$path, + [Parameter(Mandatory=$false)] + [ValidateSet("Error","Warning","Info")] + [Alias("LogLevel")] + [String]$level="Info" + ) -Function Get-OAuthToken { + $logTime = "{0:yyyy-MM-dd} {0:HH:mm:ss}" -f (Get-Date) + "," - <# - .SYNOPSIS - The Get-OAuthToken function returns a MSAL token for a given Microsoft Cloud Service using the MSAL.PS module. + switch ($level){ + "Error" { + $levelText = "ERROR," + } + "Warning" { + $levelText = "WARNING," + } + "Info" { + $levelText = "INFO," + } + } + + "$logTime $levelText $message" | Out-File -FilePath $path -Append -Encoding UTF8 +} - .EXAMPLE - --- Prompts connexion for Microsoft Graph Service and returns token --- - Get-OAuthToken -Service MSGraph -silent $false +function Import-Certificate { + param ( + [Parameter(Mandatory = $true)] + [String]$logFile, + [Parameter(Mandatory = $true)] + [String]$certificatePath + ) + if (-not (Test-Path -Path $certificatePath)){ + Write-Error "The provided path for certificate: $certificatePath does not exist. Exiting" + "The provided path for certificate: $certificatePath does not exist. Exiting" | Write-Log -LogPath $logFile -LogLevel "ERROR" + exit + } - #> + "Loading certificate $certificatePath" | Write-Log -LogPath $logFile + try { + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certificatePath) + Write-Host "Loaded certificate $certificatePath with no password" + "Loaded certificate $certificatePath with no password" | Write-Log -LogPath $logFile + $emptySecurePassword = New-Object System.Security.SecureString + return $cert, $false, $emptySecurePassword + } + catch { + $errorMessage = $_.Exception.ToString() + if ($errorMessage.Contains("password")){ + try { + $certificatePassword = Read-Host -MaskInput "Please enter the password for the certificate $certificatePath" + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certificatePath,$certificatePassword) + $certificateSecurePassword = ConvertTo-SecureString -String $certificatePassword -AsPlainText -Force + Write-Host "Loaded certificate $certificatePath with a password" + "Loaded certificate $certificatePath with a password" | Write-Log -LogPath $logFile + return $cert, $true, $certificateSecurePassword + } + catch { + $errorMessage = $_.Exception.ToString() + if ($errorMessage.Contains("password")){ + Write-Error "Wrong password was provided for certificate $certificatePath. Exiting" + "Wrong password was provided for certificate $certificatePath. Exiting" | Write-Log -LogPath $logFile -LogLevel "ERROR" + exit + } + else { + Write-Error "Error while loading the certificate: $errorMessage. Exiting" + "Error while loading the certificate: $errorMessage. Exiting" | Write-Log -LogPath $logFile -LogLevel "ERROR" + exit + } + } + } + else { + Write-Error "Error while loading the certificate: $errorMessage. Exiting" + "Error while loading the certificate: $errorMessage. Exiting" | Write-Log -LogPath $logFile -LogLevel "ERROR" + exit + } + } +} +function Connect-AzUser { param ( + [Parameter(Mandatory = $true)] + [String]$logFile + ) + + $stopLoop = $false + [Int]$retryCount = "0" + + try { + $null = Disconnect-AzAccount -ErrorAction Stop + } + catch { + } + do { + try { + "Connecting to Azure" | Write-Log -LogPath $logFile + Write-Warning "Please log in to Azure using a privileged account" + $null = Connect-AzAccount -DeviceAuth -Confirm:$false -ErrorAction Stop + "Successfully logged in to Azure" | Write-Log -LogPath $logFile + $stopLoop = $true + } + catch { + if ($retryCount -ge 3){ + Write-Error "Failed to log in to Azure $($retryCount + 1) times - aborting" + "Failed to log in to Azure $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $stopLoop = $true + } + else { + $errorMessage = $_.Exception.Message + Write-Warning "Failed to log in to Azure $($retryCount + 1) times - sleeping and retrying - $errorMessage" + "Failed to log in to Azure $($retryCount + 1) times - sleeping and retrying - $errorMessage" | Write-Log -LogPath $logFile -LogLevel "Warning" + Start-Sleep -Seconds (60 * ($retryCount + 1)) + $retryCount = $retryCount + 1 + } + } + } while ($stopLoop -eq $false) +} + +function Connect-AzApplication { + param ( [Parameter(Mandatory = $true)] - [ValidateSet("EXO","MSGraph","AzRM","AzDevOps")] - [string]$Service, - [Parameter(Mandatory = $false, ParameterSetName="silent")] - [boolean]$silent=$false, - [Parameter(Mandatory = $false, ParameterSetName="DeviceCode")] - [boolean]$DeviceCode=$false, - [Parameter(Mandatory = $false)] - [string]$LoginHint, - [Parameter(Mandatory = $false)] - [string]$Logfile + [String]$logFile, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [SecureString]$certificateSecurePassword, + [Parameter(Mandatory = $true)] + [Bool]$needPassword, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $true)] + [String]$appId ) - switch ($Service) { - exo { - # EXO Powershell Client ID - $clientId = "a0c73c16-a7e3-4564-9a95-2bdf47383716" - $scope = "https://outlook.office365.com/.default" - $redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient" - } - MSGraph { - # Azure AD PowerShell Client ID - $clientId = "1b730954-1685-4b74-9bfd-dac224a7b894" - $scope = "https://graph.microsoft.com/.default" - $redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient" - - } - AzRM - { - # AZ PowerShell Client ID - $clientid = "1950a258-227b-4e31-a9cf-717495945fc2" - $scope = "https://management.azure.com/.default" - $redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient" - } - AzDevOps - { - # AZ PowerShell Client ID - $clientid = "1950a258-227b-4e31-a9cf-717495945fc2" - $scope = "499b84ac-1321-427f-aa17-267ca6975798/user_impersonation" - $redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient" - } - Default { Write-Error "Service Not Implemented" -ErrorAction Stop } + $stopLoop = $false + [Int]$retryCount = "0" + do { + try { + try { + $null = Disconnect-AzAccount -ErrorAction Stop + } + catch { + + } + if ($needPassword){ + $null = Connect-AzAccount -CertificatePath $certificatePath -CertificatePassword $certificateSecurePassword -ServicePrincipal -Tenant $tenant -ApplicationId $appId -ErrorAction Stop + } + else { + $null = Connect-AzAccount -CertificatePath $certificatePath -ServicePrincipal -Tenant $tenant -ApplicationId $appId -ErrorAction Stop + } + "Successfully logged in to Az using application $appId" | Write-Log -LogPath $logFile + $stopLoop = $true + } + catch { + if ($retryCount -ge 3){ + "Failed to log in to Az using application $appId $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $stopLoop = $true + } + else { + $errorMessage = $_.Exception.Message + "Failed to log in to Az using application $appId $($retryCount + 1) times - sleeping and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning" + Start-Sleep -Seconds (60 * ($retryCount + 1)) + $retryCount = $retryCount + 1 + } + } + } while ($stopLoop -eq $false) +} + +function Connect-ExchangeOnlineUser { + param ( + [Parameter(Mandatory = $true)] + [String]$logFile + ) + + $stopLoop = $false + [Int]$retryCount = "0" + + try { + $null = Disconnect-ExchangeOnline -Confirm:$false -ErrorAction Stop } + catch { + + } + do { + try { + "Connecting to Exchange Online" | Write-Log -LogPath $logFile + Write-Warning "Please log in to Exchange Online using a privileged account" + Connect-ExchangeOnline -ErrorAction Stop -Device -ShowBanner:$false + "Successfully logged in to Exchange Online" | Write-Log -LogPath $logFile + $stopLoop = $true + } + catch { + if ($retryCount -ge 3){ + Write-Error "Failed to log in to Exchange Online $($retryCount + 1) times - aborting" + "Failed to log in to Exchange Online $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $stopLoop = $true + } + else { + $errorMessage = $_.Exception.Message + Write-Warning "Failed to log in to Exchange Online $($retryCount + 1) times - sleeping and retrying - $errorMessage" + "Failed to log in to Exchange Online $($retryCount + 1) times - sleeping and retrying - $errorMessage" | Write-Log -LogPath $logFile -LogLevel "Warning" + Start-Sleep -Seconds (60 * ($retryCount + 1)) + $retryCount = $retryCount + 1 + } + } + } while ($stopLoop -eq $false) +} +function Connect-ExchangeOnlineApplication { - $Stoploop = $false - [int]$Retrycount = "0" + param ( + [Parameter(Mandatory = $true)] + [String]$logFile, + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$organization, + [Parameter(Mandatory = $false)] + [Array]$commandNames = @("Search-UnifiedAuditLog","Search-MailboxAuditLog") + ) + + $stopLoop = $false + [Int]$retryCount = "0" do { try { - if($silent) - {$app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq $clientId} - if($app) - { - if($logfile){"Asking Oauth silent token renewal for $($Service)" | Write-Log -LogPath $logfile} - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes $scope -ErrorAction Stop - } - else{ - Write-Error "Silent token renewal asked but no token cache available for the given application ID" - if($logfile){"Silent token renewal asked for $($Service) but no token cache available for the given application ID" | Write-Log -LogPath $logfile -LogLevel "Error"} - } + try { + Disconnect-ExchangeOnline -Confirm:$false -ErrorAction Stop } - else { - if($logfile){"Asking Oauth token for $($Service)" | Write-Log -LogPath $logfile} - if($DeviceCode -eq $true) - { - $token = Get-MsalToken -ClientId $clientId -Interactive -Scope $scope -DeviceCode - } - else - { - if($PSVersionTable.PSEdition -eq "Desktop") - { - $token = Get-MsalToken -ClientId $clientId -Interactive -Scope $scope -RedirectUri $redirectUri - } - elseif($PSVersionTable.PSEdition -eq "Core") - { - $token = Get-MsalToken -ClientId $clientId -Interactive -Scope $scope -DeviceCode - } - } - } - $Stoploop = $true + catch { + } + Connect-ExchangeOnline -Certificate $certificate -AppID $appId -Organization $organization -CommandName $commandNames -ErrorAction Stop -ShowBanner:$false + "Successfully logged in to Exchange Online using application $appId" | Write-Log -LogPath $logFile + $stopLoop = $true + } catch { - if ($Retrycount -gt 3){ - $Stoploop = $true - $ErrorMessage = $_.Exception.Message - $FailedItem = $_.Exception.ItemName - Write-Error "Failed to get Oauth token after 4 retries" - if($logfile){"Failed to get Oauth token for $($Service) service after 4 retries: Item $($FailedItem) Error message $($ErrorMessage)" | Write-Log -LogPath $logfile -LogLevel "Error"} - } + if ($retryCount -ge 3){ + "Failed to log in to Exchange Online using application $appId $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $stopLoop = $true + } else { - Start-Sleep -Seconds 2 - $Retrycount = $Retrycount + 1 - $ErrorMessage = $_.Exception.Message - $FailedItem = $_.Exception.ItemName - Write-Warning -Message "Failed to get Oauth Token, retrying..." - if($logfile){"Failed to get Oauth token for $($Service) service: Item $($FailedItem) Error message $($ErrorMessage)" | Write-Log -LogPath $logfile -LogLevel "Warning"} + $errorMessage = $_.Exception.Message + "Failed to log in to Exchange Online using application $appId $($retryCount + 1) times - sleeping and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning" + if ($errorMessage -like "*No cmdlet assigned to the user have this feature enabled.*" -or $errorMessage -eq "UnAuthorized"){ + $retryCount = 3 + } + else { + Start-Sleep -Seconds (60 * ($retryCount + 1)) } + $retryCount = $retryCount + 1 } } - While ($Stoploop -eq $false) - + } while ($stopLoop -eq $false) +} - $toklifetime = (New-TimeSpan -Start (get-date) -End (get-date $token.ExpiresOn.LocalDateTime)).Minutes - if($toklifetime -ge 59) - { - - if($logfile){"New Oauth token for $($Service) service acquired" | Write-Log -LogPath $logfile} - } - return $token +function Connect-MicrosoftGraphUser { + param ( + [Parameter(Mandatory = $true)] + [String]$logFile + ) + + $stopLoop = $false + [Int]$retryCount = "0" + try { + $null = Disconnect-MgGraph -ErrorAction Stop + } + catch { + + } + do { + try { + "Connecting to Entra ID" | Write-Log -LogPath $logFile + Write-Warning "Please log in to Entra ID using a privileged account" + Connect-MgGraph -NoWelcome -Scopes "Application.ReadWrite.All, Directory.Read.All, GroupMember.ReadWrite.All, RoleManagement.ReadWrite.Directory" -UseDeviceCode -ErrorAction Stop + "Successfully logged in to Entra ID" | Write-Log -LogPath $logFile + $stopLoop = $true + } + catch { + if ($retryCount -ge 3){ + Write-Error "Failed to log in to Entra ID $($retryCount + 1) times - aborting" + "Failed to log in to Entra ID $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $stopLoop = $true + } + else { + $errorMessage = $_.Exception.Message + Write-Warning "Failed to log in to Entra ID $($retryCount + 1) times - sleeping and retrying - $errorMessage" + "Failed to log in to Entra ID $($retryCount + 1) times - sleeping and retrying - $errorMessage" | Write-Log -LogPath $logFile -LogLevel "Warning" + Start-Sleep -Seconds (60 * ($retryCount + 1)) + $retryCount = $retryCount + 1 + } + } + } while ($stopLoop -eq $false) } -Function Get-RestAPIResponse { - param - ( +function Connect-MicrosoftGraphApplication { + + param ( [Parameter(Mandatory = $true)] - [string]$uri, - [Parameter(Mandatory = $true)] - [System.Object]$app, + [String]$logFile, [Parameter(Mandatory = $true)] - [string]$user, + [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, [Parameter(Mandatory = $true)] - [ValidateSet("MSGraph","AzRM","AzDevOps")] - [string]$RESTAPIService, + [String]$appId, [Parameter(Mandatory = $true)] - [string]$Logfile + [String]$tenant ) - if($RESTAPIService -eq "MSGraph") - {$token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://graph.microsoft.com/.default"} - elseif($RESTAPIService -eq "AzRM") - {$token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://management.azure.com/.default"} - else - {$token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "499b84ac-1321-427f-aa17-267ca6975798/user_impersonation"} - $APIresults = @() - $Stoploop = $false - [int]$Retrycount = "0" + $stopLoop = $false + [Int]$retryCount = "0" do { try { + try { + $null = Disconnect-MgGraph -ErrorAction Stop + } + catch { - $Data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Uri $Uri -Method Get -ContentType "application/json" -ErrorAction Stop - $Stoploop = $true } + Connect-MgGraph -Certificate $certificate -ClientId $appId -TenantId $tenant -NoWelcome -ErrorAction Stop + "Successfully logged in to Microsoft Graph using application $appId" | Write-Log -LogPath $logFile + $stopLoop = $true + } catch { - if ($Retrycount -gt 9){ - "Failed to dump from $($RESTAPIService) uri $($uri) records $($Retrycount) times - aborting" | Write-Log -LogPath $logfile -LogLevel "Error" - $Data = @() - $Stoploop = $true - } + if ($retryCount -ge 3){ + "Failed to log in to Microsoft Graph using application $appId $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $stopLoop = $true + } else { - $errorcode = $_.Exception.Response.StatusCode.value__ - $errormessage = $_.ErrorDetails.Message - "Failed to dump from $($RESTAPIService) uri $($uri) - sleeping and retrying - $($errorcode) : $($errormessage)" | Write-Log -LogPath $logfile -LogLevel "Warning" - - If ($errorcode -eq "429") { - Start-Sleep -Seconds (5 * ($Retrycount + 1)) + $errorMessage = $_.Exception.Message + "Failed to log in to Microsoft Graph using application $appId $($retryCount + 1) times - sleeping and retrying - $errorMessage" | Write-Log -LogPath $logFile -LogLevel "Warning" + Start-Sleep -Seconds (60 * ($retryCount + 1)) + $retryCount = $retryCount + 1 + } + } + } while ($stopLoop -eq $false) +} + +function Get-AzDevOpsRestAPIResponseUser { + param + ( + [Parameter(Mandatory = $true)] + [String]$uri, + [Parameter(Mandatory = $true)] + [String]$logFile + ) + try { + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + } + catch { + Connect-AzUser -logFile $logFile + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + } + + $APIresults = @() + + $stopLoop = $false + [Int]$retryCount = "0" + while ($stopLoop -eq $false){ + try { + $data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Uri $($uri) -Method GET -ContentType "application/json" -ResponseHeadersVariable responseHeaders -ErrorAction Stop + $stopLoop = $true + } + catch { + if ($retryCount -ge 10){ + Write-Error "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" + "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $data = @() + $stopLoop = $true + } + else { + $errorCode = $_.Exception.Response.StatusCode.value__ + $errorMessage = $_.ErrorDetails.Message + Write-Error "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" + "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" | Write-Log -LogPath $logFile -LogLevel "Warning" + if ($errorCode -eq "429"){ + Start-Sleep -Seconds (15 * ($retryCount + 1)) } - Elseif ($errorcode -eq "403") { - $Retrycount = 9 + elseif ($errorCode -eq "401" -or $errorCode -eq "403"){ + $retryCount = 10 } - Else { + else { Start-Sleep -Seconds 1 } - $Retrycount = $Retrycount + 1 - } + $retryCount = $retryCount + 1 } } - While ($Stoploop -eq $false) + } + if ($data){ + $APIresults += $data.value - if($Data) - { if($RESTAPIService -eq "AzDevOps"){$APIresults+=$Data.decoratedAuditLogEntries} - else{$APIresults+=$Data.Value} - while(($null -ne $Data."@odata.nextLink") -or ($null -ne $Data.nextLink) -or ($Data.hasMore -eq $true)) { - $Stoploop = $false - [int]$Retrycount = "0" - do { - try { - if($RESTAPIService -eq "MSGraph") - { - $Data = Invoke-RestMethod -Uri $Data."@odata.nextLink" -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Method Get -ContentType "application/json" -ErrorAction Stop - } - elseif($RESTAPIService -eq "AzRM") { - $Data = Invoke-RestMethod -Uri $Data.nextLink -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Method Get -ContentType "application/json" -ErrorAction Stop - } - else{ - $urisuite = (($uri -split "startTime")[0]) + "continuationToken=$($Data.continuationToken)&api-version=6.0-preview.1" - $Data = Invoke-RestMethod -Uri $urisuite -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Method Get -ContentType "application/json" -ErrorAction Stop - } - if($RESTAPIService -eq "AzDevOps"){$APIresults+=$Data.decoratedAuditLogEntries} - else{$APIresults+=$Data.Value} - $Stoploop = $true - } + if ($null -ne $($responseHeaders."X-MS-ContinuationToken")){ + $stopLoop = $false + [Int]$retryCount = "0" + while ($stopLoop -eq $false -and $null -ne $($responseHeaders."X-MS-ContinuationToken")){ + try { + $continuationToken = $responseHeaders."X-MS-ContinuationToken" + if ($uri.contains("continuationToken=")){ + $uri = ($uri -Split "continuationToken=")[0] + $uri = $uri.Substring(0, $uri.Length - 1) + } + if ($uri.contains("?")){ + $uri += "&continuationToken=$continuationToken" + } + else { + $uri += "?continuationToken=$continuationToken" + } + $data = Invoke-RestMethod -Uri $uri -Headers @{Authorization = "Bearer $($token.Token)"} -Method GET -ContentType "application/json" -ResponseHeadersVariable responseHeaders -ErrorAction Stop + $APIresults += $data.value + } catch { - if ($Retrycount -gt 9){ - "Failed to dump from $($RESTAPIService) uri $($uri) records 9 times - aborting" | Write-Log -LogPath $logfile -LogLevel "Error" - $Data = @() - $Stoploop = $true + if ($retryCount -ge 10){ + Write-Error "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" + "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $data = @() + $stopLoop = $true } - else { - $errormessage = $_.Exception.Message - "Failed to dump from $($RESTAPIService) uri $($uri) - sleeping and retrying - $($errormessage)" | Write-Log -LogPath $logfile -LogLevel "Warning" - Start-Sleep -Seconds (5 * ($Retrycount + 1)) - if($token.ExpiresOn -le (get-date)) - { - "Token has expired renewing $($RESTAPIService) token" | Write-Log -LogPath $logfile -LogLevel "Warning" - if($RESTAPIService -eq "MSGraph") - {$token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://graph.microsoft.com/.default"} - elseif($RESTAPIService -eq "AzRM") - {$token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://management.azure.com/.default"} - else - {$token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "499b84ac-1321-427f-aa17-267ca6975798/user_impersonation"} + else { + $errorCode = $_.Exception.Response.StatusCode.value__ + $errorMessage = $_.ErrorDetails.Message + Write-Warning "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" + "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" | Write-Log -LogPath $logFile -LogLevel "Warning" + if ($token.ExpiresOn -le (Get-Date)){ + Write-Warning "Token has expired, renewing" + "Token has expired, renewing" | Write-Log -LogPath $logFile -LogLevel "Warning" + try { + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + } + catch { + Connect-AzUser -logFile $logFile + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop } - $Retrycount = $Retrycount + 1 } - } - } While ($Stoploop -eq $false) + else { + Start-Sleep -Seconds (5 * ($retryCount + 1)) + } + $retryCount = $retryCount + 1 + } + } } - } - else - {"No event to process for uri $($uri)" | Write-Log -LogPath $logfile } + } + } + else { + Write-Host "No events to dump from Azure DevOps URI $($uri)" + "No events to dump from Azure DevOps URI $($uri)" | Write-Log -LogPath $logFile + } return $APIresults } -function Connect-EXOPsearchUnified -{ - -param ( - - [Parameter(Mandatory = $true)] - [System.Object]$token, - [Parameter(Mandatory = $true)] - [string]$sessionName, +function Get-AzDevOpsAuditLogs { + param + ( [Parameter(Mandatory = $true)] - [string]$Logfile, - [Parameter(Mandatory = $false)] - [array]$commandNames = "Search-UnifiedAuditLog" + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [SecureString]$certificateSecurePassword, + [Parameter(Mandatory = $true)] + [Bool]$needPassword, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$uri, + [Parameter(Mandatory = $true)] + [String]$logFile ) + try { + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + } + catch { + Connect-AzApplication -logFile $logFile -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + } - - $UserId = ($token.Account.Username).tostring() - $Stoploop = $false - [int]$Retrycount = "0" - do { + $APIresults = @() + + $stopLoop = $false + [Int]$retryCount = "0" + while ($stopLoop -eq $false){ try { - Connect-ExchangeOnline -AccessToken $token.AccessToken -UserPrincipalName $UserId -CommandName $commandNames -ErrorAction Stop - "EXO session $($sessionName) successfully created" | Write-Log -LogPath $logfile - $Stoploop = $true - } + $data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Uri $($uri + "&batchSize=1") -Method GET -ContentType "application/json" -ErrorAction Stop + $stopLoop = $true + } catch { - if ($Retrycount -gt 3){ - "Failed to create EXO session $($sessionName) $($Retrycount) times - aborting" | Write-Log -LogPath $logfile -LogLevel "Error" - $Stoploop = $true + if ($retryCount -ge 10){ + "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $data = @() + $stopLoop = $true + } + else { + $errorCode = $_.Exception.Response.StatusCode.value__ + $errorMessage = $_.ErrorDetails.Message + "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" | Write-Log -LogPath $logFile -LogLevel "Warning" + if ($errorCode -eq "429"){ + Start-Sleep -Seconds (15 * ($retryCount + 1)) } - else - { - $errormessage = $_.Exception.Message - "Failed to create EXO session $($sessionName) - sleeping and retrying - $($errormessage)" | Write-Log -LogPath $logfile -LogLevel "Warning" - if ($errormessage -like "*No cmdlet assigned to the user have this feature enabled.*") { - $Retrycount = 3 + elseif ($errorCode -eq "401" -or $errorCode -eq "403"){ + $retryCount = 10 } - else { - Start-Sleep -Seconds (60 * ($Retrycount + 1)) + else { + Start-Sleep -Seconds 1 } - $Retrycount = $Retrycount + 1 + $retryCount = $retryCount + 1 + } + } + } + if ($data){ + $APIresults += $data.decoratedAuditLogEntries + + if ($data.hasMore -eq $true){ + $stopLoop = $false + [Int]$retryCount = "0" + while ($stopLoop -eq $false -and $data.hasMore -eq $true){ + try { + $continuationToken = $data.continuationToken + if ($uri.contains("&continuationToken=")){ + $uri = ($uri -Split "&continuationToken=")[0] + } + $uri += "&continuationToken=$continuationToken" + $data = Invoke-RestMethod -Uri $uri -Headers @{Authorization = "Bearer $($token.Token)"} -Method GET -ContentType "application/json" -ErrorAction Stop + $APIresults += $data.decoratedAuditLogEntries + } + catch { + if ($retryCount -ge 10){ + "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $data = @() + $stopLoop = $true + } + else { + $errorCode = $_.Exception.Response.StatusCode.value__ + $errorMessage = $_.ErrorDetails.Message + "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" | Write-Log -LogPath $logFile -LogLevel "Warning" + if ($token.ExpiresOn -le (Get-Date)){ + "Token has expired, renewing" | Write-Log -LogPath $logFile -LogLevel "Warning" + try { + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + } + catch { + Connect-AzApplication -logFile $logFile -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + } + } + else { + Start-Sleep -Seconds (5 * ($retryCount + 1)) + } + $retryCount = $retryCount + 1 + } } } } - While ($Stoploop -eq $false) + } + else { + "No events to dump from Azure DevOps URI $($uri)" | Write-Log -LogPath $logFile + } + return $APIresults } - -Function Get-LargeUnifiedAuditLog { - param +function Get-LargeUnifiedAuditLog { + param ( [Parameter(Mandatory = $true)] - [ValidateSet("Operations","Records","freetext","IPAddresses","UserIds")] - [string]$requesttype, - [Parameter(Mandatory = $true)] - [string]$sessionName, - [Parameter(Mandatory = $true)] - [string]$outputfile, + [ValidateSet("Unfiltered","Operations","RecordTypes","FreeText","IPAddresses","UserIds")] + [String]$requestType, + [Parameter(Mandatory = $false)] + [String]$freeText, [Parameter(Mandatory = $false)] - [string]$recordtype, + [string[]]$IPAddresses, [Parameter(Mandatory = $false)] - [string]$searchstring, + [string[]]$userIds, [Parameter(Mandatory = $false)] - [System.Array]$searchtable, + [string[]]$operations, [Parameter(Mandatory = $false)] - [array]$operations, + [String]$recordTypes, + [Parameter(Mandatory = $true)] + [String]$sessionName, + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, [Parameter(Mandatory = $true)] - [datetime]$StartDate, + [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, [Parameter(Mandatory = $true)] - [datetime]$EndDate, + [String]$appId, [Parameter(Mandatory = $true)] - [string]$Logfile + [String]$tenant, + [Parameter(Mandatory = $true)] + [String]$logFile, + [Parameter(Mandatory = $true)] + [String]$outputFile ) - $j = 0 - Do { - $Stoploop = $false - [int]$Retrycount = "0" + "Collecting $requestType events for $startDate - $endDate" | Write-Log -LogPath $logFile -LogLevel "Info" + [Int]$lastUnifiedAuditLogEntriesResultIndex = "0" + do { + $stopLoop = $false + [Int]$retryCount = "0" do { try { - # Using ReturnLargeSet to get back up to 50k records, 5k at a time - if($requesttype -eq "Records") - { - $o = Search-UnifiedAuditLog -StartDate $startdate -EndDate $enddate -RecordType $recordtype -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop - $n = ($o | measure-object).count - $f = (($o | Select-Object -Property ResultCount -Unique).ResultCount -eq 0) -or (($o | Select-Object -Property ResultIndex -Unique).ResultIndex -eq -1) - } - elseif($requesttype -eq "Operations") - { - $o = Search-UnifiedAuditLog -StartDate $startdate -EndDate $enddate -Operations $operations -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop - $n = ($o | measure-object).count - $f = (($o | Select-Object -Property ResultCount -Unique).ResultCount -eq 0) -or (($o | Select-Object -Property ResultIndex -Unique).ResultIndex -eq -1) + # Using ReturnLargeSet to get back up to 50k events, 5k at a time + if ($requestType -eq "Unfiltered"){ + $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop + } + if ($requestType -eq "RecordTypes"){ + $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -RecordType $recordTypes -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop } - elseif($requesttype -eq "freetext") - { - $o = Search-UnifiedAuditLog -StartDate $startdate -EndDate $enddate -FreeText $searchstring -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop - $n = ($o | measure-object).count - $f = (($o | Select-Object -Property ResultCount -Unique).ResultCount -eq 0) -or (($o | Select-Object -Property ResultIndex -Unique).ResultIndex -eq -1) + elseif ($requestType -eq "Operations"){ + $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -Operations $operations -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop } - elseif($requesttype -eq "IPAddresses") - { - $o = Search-UnifiedAuditLog -StartDate $startdate -EndDate $enddate -IPAddresses $searchstring -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop - $n = ($o | measure-object).count - $f = (($o | Select-Object -Property ResultCount -Unique).ResultCount -eq 0) -or (($o | Select-Object -Property ResultIndex -Unique).ResultIndex -eq -1) + elseif ($requestType -eq "FreeText"){ + $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -FreeText $freeText -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop } - elseif($requesttype -eq "UserIds") - { - $o = Search-UnifiedAuditLog -StartDate $startdate -EndDate $enddate -UserIds $searchtable -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop - $n = ($o | measure-object).count - $f = (($o | Select-Object -Property ResultCount -Unique).ResultCount -eq 0) -or (($o | Select-Object -Property ResultIndex -Unique).ResultIndex -eq -1) + elseif ($requestType -eq "IPAddresses"){ + $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -IPAddresses $IPAddresses -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop } - "Got $($n) records" | Write-Log -LogPath $logfile -LogLevel "Info" - if ($f){ - Start-Sleep -Seconds 300 - throw "Error. Internal timeout" + elseif ($requestType -eq "UserIds"){ + $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -UserIds $userIds -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop } - $Stoploop = $true + + if ($null -eq $unifiedAuditLogEntries){ + if ($lastUnifiedAuditLogEntriesResultIndex -ne 0){ + throw "We were supposed to have some events, but we got an empty result instead. This might be because of a server timeout" + } + else { + "0 $($requestType) events between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} were found" -f ($startDate, $endDate) | Write-Log -LogPath $logFile -LogLevel "Warning" + $countUnifiedAuditLogEntries = 0 + break + } + } + + $countUnifiedAuditLogEntries = ($unifiedAuditLogEntries | Measure-Object).Count + $unifiedAuditLogEntriesResultCount = ($unifiedAuditLogEntries | Select-Object -Property ResultCount -Unique).ResultCount + $unifiedAuditLogEntriesResultIndex = $unifiedAuditLogEntries[-1].ResultIndex + + if (($unifiedAuditLogEntriesResultCount -eq 0) -or ($unifiedAuditLogEntriesResultIndex -eq -1)){ + throw "We were supposed to have some events, but we got a boggus result instead (ResultCount = 0 or ResultIndex = -1). This might be because of a server timeout" + } + elseif (($lastUnifiedAuditLogEntriesResultIndex + $countUnifiedAuditLogEntries) -ne $unifiedAuditLogEntriesResultIndex){ + throw "We did not get the expected record index (lastIndex + actualCount != actualIndex). This might be because of a server timeout" + } + + if ($lastUnifiedAuditLogEntriesResultIndex -eq 0 -and $unifiedAuditLogEntriesResultCount -gt 50000){ + "More than 50000 $($requestType) events between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} - some events will be missing" -f ($startDate, $endDate) | Write-Log -LogPath $logFile -LogLevel "Warning" + } + + "Collected $($unifiedAuditLogEntriesResultIndex) events out of $($unifiedAuditLogEntriesResultCount) (+$($countUnifiedAuditLogEntries))" | Write-Log -LogPath $logFile -LogLevel "Info" + $stopLoop = $true } catch { - if ($Retrycount -gt 3){ - "Failed to dump $($recordtype) records 4 times - aborting" | Write-Log -LogPath $logfile -LogLevel "Error" - $o = @() - $n = 0 - $Stoploop = $true - } + $lastUnifiedAuditLogEntriesResultIndex = 0 + $countUnifiedAuditLogEntries = 0 + $unifiedAuditLogEntries = @() + if ($retryCount -ge 10){ + "Failed to dump $($requestType) events $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $stopLoop = $true + } else { - $errormessage = $_.Exception.Message - "Failed to dump $($recordtype) records - sleeping and retrying - $($errormessage)" | Write-Log -LogPath $logfile -LogLevel "Warning" - Start-Sleep -Seconds (60 * ($Retrycount + 1)) - $Retrycount = $Retrycount + 1 + $errorMessage = $_.Exception.Message + "Failed to dump $($requestType) events $($retryCount + 1) times - deleting, reconnecting, sleeping and retrying for the time period $startDate - $endDate - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning" + $sessionName = $(New-Guid).Guid + if ((Test-Path $outputFile) -eq $true){ + $null = Remove-Item $outputFile -Force -Confirm:$false } + Start-Sleep -Seconds (60 * ($retryCount + 1)) + Connect-ExchangeOnlineApplication -logFile $logFile -certificate $cert -appId $appId -organization $tenant + $retryCount = $retryCount + 1 } } - While ($Stoploop -eq $false) - # If count is 0, no records to process - if ($n -gt 0) - { + } while ($stopLoop -eq $false) + + # If count is 0, no events to process + if ($countUnifiedAuditLogEntries -gt 0){ # Dump data to json file - $o | Select-Object -ExpandProperty AuditData | out-file $outputfile -encoding UTF8 -append - if ($n -lt 5000) { - $o = @() - $n = 0 - } - else { - $j++ + $unifiedAuditLogEntries | Select-Object -ExpandProperty AuditData | Out-File $outputFile -Encoding UTF8 -Append + $lastUnifiedAuditLogEntriesResultIndex += $countUnifiedAuditLogEntries + if ($unifiedAuditLogEntriesResultIndex -ne 0 -and (($unifiedAuditLogEntriesResultIndex -eq $unifiedAuditLogEntriesResultCount) -or ($unifiedAuditLogEntriesResultIndex -eq 50000))){ + "Done collecting events for ${startDate} - ${endDate}: ${unifiedAuditLogEntriesResultIndex} events were collected out of $($unifiedAuditLogEntriesResultCount)" | Write-Log -LogPath $logFile -LogLevel "Info" + $countUnifiedAuditLogEntries = 0 + } + } + } until ($countUnifiedAuditLogEntries -eq 0) +} + +function Get-MicrosoftGraphLogs { + param + ( + [Parameter(Mandatory = $true)] + [String]$type, + [Parameter(Mandatory = $false)] + [String]$tenantSize, + [Parameter(Mandatory = $true)] + [String]$dateStart, + [Parameter(Mandatory = $true)] + [String]$dateEnd, + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $true)] + [String]$logFile + ) + $stopLoop = $false + [Int]$retryCount = "0" + do { + try { + if ($type -eq "SignIns"){ + if ($tenantSize -eq "normal"){ + $AzureADEvents = Get-MgBetaAuditLogSignIn -All -Filter "createdDateTime ge $($dateStart) and createdDateTime lt $($dateEnd)" -ErrorAction Stop + } + else { + $AzureADEvents = Get-MgBetaAuditLogSignIn -All -Filter "createdDateTime ge $($dateStart) and createdDateTime lt $($dateEnd) and status/errorCode eq 0 and (appId eq '00000002-0000-0ff1-ce00-000000000000' or appId eq '1b730954-1685-4b74-9bfd-dac224a7b894' or appId eq 'a0c73c16-a7e3-4564-9a95-2bdf47383716' or appId eq '00000003-0000-0ff1-ce00-000000000000' or appId eq '6eb59a73-39b2-4c23-a70f-e2e3ce8965b1' or appId eq 'cb1056e2-e479-49de-ae31-7812af012ed8' or appId eq '1950a258-227b-4e31-a9cf-717495945fc2' or appId eq 'fb78d390-0c51-40cd-8e17-fdbfab77341b' or appId eq '04b07795-8ddb-461a-bbee-02f9e1bf7b46')" -ErrorAction Stop + } + } + elseif ($type -eq "AuditLogs"){ + $AzureADEvents = Get-MgBetaAuditLogDirectoryAudit -All -Filter "activityDateTime ge $($dateStart) and activityDateTime lt $($dateEnd)" -ErrorAction Stop + } + $stopLoop = $true + } + catch { + if ($retryCount -ge 10){ + "Failed to get $($type) logs $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $AzureADEvents = $null + $stopLoop = $true + } + else { + $errorMessage = $_.Exception.Message + if ($errorMessage -ne "Too many retries performed"){ + Start-Sleep -Seconds (60 * ($retryCount + 1) + $(Get-Random -Minimum 1 -Maximum 60)) + } + "Failed to get $($type) logs $($retryCount + 1) times - reconnecting and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning" + Connect-MicrosoftGraphApplication -certificate $certificate -appId $appId -tenant $tenant -logFile $logFile + $retryCount = $retryCount + 1 + } + } + } while ($stopLoop -eq $false) + return $AzureADEvents +} + +function Get-AzureRMActivityLog { + param + ( + [Parameter(Mandatory = $true)] + [String]$dateStart, + [Parameter(Mandatory = $true)] + [String]$dateEnd, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [SecureString]$certificateSecurePassword, + [Parameter(Mandatory = $true)] + [Bool]$needPassword, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $true)] + [String]$logFile + ) + $stopLoop = $false + [Int]$retryCount = "0" + do { + try { + $azureRMActivityEvents = Get-AzActivityLog -StartTime $dateStart -EndTime $dateEnd -DetailedOutput -ErrorAction Stop + $stopLoop = $true + } + catch { + if ($retryCount -ge 10){ + "Failed to get Azure Resource Manager activity logs $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $azureRMActivityEvents = $null + $stopLoop = $true + } + else { + $errorMessage = $_.Exception.Message + Start-Sleep -Seconds (60 * ($retryCount + 1) + $(Get-Random -Minimum 1 -Maximum 60)) + "Failed to get Azure Resource Manager activity logs $($retryCount + 1) times - reconnecting and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning" + Connect-AzApplication -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId -logFile $logFile + $retryCount = $retryCount + 1 + } + } + } while ($stopLoop -eq $false) + return $azureRMActivityEvents +} + +function Get-UnifiedAuditLogPurview { + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet("Unfiltered","Operations","RecordTypes","FreeText","IPAddresses","UserIds")] + [String]$requestType, + [Parameter(Mandatory = $false)] + [String]$freeText, + [Parameter(Mandatory = $false)] + [string[]]$IPAddresses, + [Parameter(Mandatory = $false)] + [string[]]$userIds, + [Parameter(Mandatory = $false)] + [string[]]$operations, + [Parameter(Mandatory = $false)] + [string[]]$recordTypes, + [Parameter(Mandatory = $true)] + [String]$sessionName, + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $true)] + [String]$logFile, + [Parameter(Mandatory = $true)] + [String]$outputFile + ) + $stopLoop = $false + [Int]$retryCount = "0" + "Collecting $requestType events for $startDate - $endDate" | Write-Log -LogPath $logFile -LogLevel "Info" + do { + try { + if ($requestType -eq "Unfiltered"){ + $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -ErrorAction Stop + } + if ($requestType -eq "RecordTypes"){ + $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -RecordTypeFilters $recordTypes -ErrorAction Stop + } + elseif ($requestType -eq "Operations"){ + $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -OperationFilters $operations -ErrorAction Stop + } + elseif ($requestType -eq "FreeText"){ + $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -KeywordFilter $freeText -ErrorAction Stop + } + elseif ($requestType -eq "IPAddresses"){ + $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -IPAddressFilters $IPAddresses -ErrorAction Stop + } + elseif ($requestType -eq "UserIds"){ + $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -UserPrincipalNameFilters $userIds -ErrorAction Stop + } + $stopLoop = $true + } + catch { + if ($retryCount -ge 10){ + "Failed to create a Purview query for $($requestType) events for $startDate - $endDate $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $auditLogQuery = $null + $stopLoop = $true + } + else { + $errorMessage = $_.Exception.Message + Start-Sleep -Seconds (60 * ($retryCount + 1) + $(Get-Random -Minimum 1 -Maximum 60)) + "Failed to create a Purview query for $($requestType) events for $startDate - $endDate $($retryCount + 1) times - reconnecting and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning" + Connect-MicrosoftGraphApplication -certificate $certificate -appId $appId -tenant $tenant -logFile $logFile + $retryCount = $retryCount + 1 } } - } Until ($n -eq 0) + } while ($stopLoop -eq $false) + + if ($null -ne $auditLogQuery){ + $auditLogQueryId = $auditLogQuery.Id + "Purview query for $($requestType) events for $startDate - $endDate was created with the Id $($auditLogQueryId)" | Write-Log -LogPath $logFile -LogLevel "Info" + $stopLoop = $false + while (-not $stopLoop){ + try { + $status = (Get-MgBetaSecurityAuditLogQuery -AuditLogQueryId $auditLogQueryId -ErrorAction Stop).Status + } + catch { + "Failed to get status for query $auditLogQueryId. Retrying" | Write-Log -LogPath $logFile -LogLevel "Warning" + continue + } + "Audit Log Query $($auditLogQueryId) is in status `"$status`"" | Write-Log -LogPath $logFile + if ($status -eq "succeeded"){ + $stopLoop = $true + } + if ($status -eq "failed" -or $status -eq "cancelled"){ + "Audit Log Query $auditLogQueryId has failed: `"$status`"" | Write-Log -LogPath $logFile -LogLevel "Error" + $stopLoop = $true + } + Start-Sleep -Seconds 10 + } + if ($status -eq "succeeded"){ + $stopLoop = $false + [Int]$retryCount = "0" + do { + try { + $records = Get-MgBetaSecurityAuditLogQueryRecord -AuditLogQueryId $auditLogQueryId -All -ErrorAction Stop + $stopLoop = $true + } + catch { + if ($retryCount -ge 10){ + "Failed to get events for Purview request $auditLogQueryId $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $records = $null + $stopLoop = $true + } + else { + $errorMessage = $_.Exception.Message + "Failed to get events for Purview request $auditLogQueryId $($retryCount + 1) times - reconnecting and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning" + Connect-MicrosoftGraphApplication -certificate $certificate -appId $appId -tenant $tenant -logFile $logFile + $retryCount = $retryCount + 1 + } + } + } while ($stopLoop -eq $false) + + if ($null -ne $records){ + $recordsCount = $records.Count + "Got $recordsCount events for Purview request $auditLogQueryId" | Write-Log -LogPath $logFile -LogLevel "Info" + "[" | Out-File $outputFile -Encoding UTF8 -Append + $i = 0 + $records | ForEach-Object { + if ($i % 10000 -eq 0){ + "Wrote $i events out of $recordsCount for query $auditLogQueryId" | Write-Log -LogPath $logFile -LogLevel "Info" + } + $_ | Select-Object -ExpandProperty AuditData | Select-Object -ExpandProperty AdditionalProperties | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 -Append + if ($i -ne ($recordsCount -1)){ + "," | Out-File $outputFile -Encoding UTF8 -Append + } + $i += 1 + } + "]" | Out-File $outputFile -Encoding UTF8 -Append + "Wrote $i events out of $recordsCount for query $auditLogQueryId" | Write-Log -LogPath $logFile -LogLevel "Info" + } + else { + "Got 0 events for query $auditLogQueryId" | Write-Log -LogPath $logFile -LogLevel "Warning" + } + } + } } -Function Get-MailboxAuditLog { - param +function Get-MailboxAuditLog { + param ( [Parameter(Mandatory = $true)] - [string]$outputfile, + [String]$outputFileWithoutJson, + [Parameter(Mandatory = $true)] + [System.Array]$userIds, + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, [Parameter(Mandatory = $true)] - [System.Array]$UserIds, + [String]$logFile, [Parameter(Mandatory = $true)] - [datetime]$StartDate, + [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, [Parameter(Mandatory = $true)] - [datetime]$EndDate, + [String]$appId, [Parameter(Mandatory = $true)] - [string]$logfile + [String]$tenant ) - foreach ($userId in $UserIds) - { - $Stoploop = $false - [int]$Retrycount = "0" + foreach ($userId in $userIds){ + $mailboxAuditLogEntries = @() + $countMailboxAuditLogEntries = 0 + + "Collecting MailboxAuditLog events for $startDate - $endDate for user $userId" | Write-Log -LogPath $logFile -LogLevel "Info" + $outputFile = "$($outputFileWithoutJson)_$($userId).json" + $stopLoop = $false + [Int]$retryCount = "0" do { try { - $o = Search-MailboxAuditLog -StartDate $startdate -EndDate $enddate -Identity $userId -LogonTypes Admin,Delegate,Owner -IncludeInactiveMailbox -ShowDetails -ResultSize 250000 -ErrorAction Stop - $n = ($o | measure-object).count - "Got $($n) records" | Write-Log -LogPath $logfile -LogLevel "Info" - $Stoploop = $true + $mailboxAuditLogEntries = Search-MailboxAuditLog -StartDate $startDate -EndDate $endDate -Identity $userId -LogonTypes Admin,Delegate,Owner -IncludeInactiveMailbox -ShowDetails -ResultSize 250000 -ErrorAction Stop + $countMailboxAuditLogEntries = ($mailboxAuditLogEntries | Measure-Object).Count + $stopLoop = $true } catch { - if ($_.CategoryInfo.Reason -eq "ManagementObjectNotFoundException") { - "$($userId) does not have a mailbox" | Write-Log -LogPath $logfile -LogLevel "Warning" - $o = @() - $n = 0 - $Stoploop = $true + if ($_.ToString().contains("ManagementObjectNotFoundException")){ + "$($userId) does not have a mailbox" | Write-Log -LogPath $logFile -LogLevel "Warning" + $countMailboxAuditLogEntries = 0 + $stopLoop = $true } - else - { - if ($Retrycount -gt 3){ - "Failed to dump MailboxAuditLog for $($userId) 4 times - aborting" | Write-Log -LogPath $logfile -LogLevel "Error" - $o = @() - $n = 0 - $Stoploop = $true + else { + if ($retryCount -ge 10){ + "Failed to dump MailboxAuditLog for $($userId) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error" + $countMailboxAuditLogEntries = 0 + $stopLoop = $true } else { - $errormessage = $_.Exception.Message - "Failed to dump MailboxAuditLog for $($userId) - sleeping and retrying - $($errormessage)" | Write-Log -LogPath $logfile -LogLevel "Warning" - Start-Sleep -Seconds 1 - $Retrycount = $Retrycount + 1 + $errorMessage = $_.Exception.Message + "Failed to dump MailboxAuditLog for $($userId) $($retryCount + 1) times - reconnecting, sleeping and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning" + Connect-ExchangeOnlineApplication -logFile $logFile -certificate $cert -appId $appId -organization $tenant + Start-Sleep -Seconds (60 * ($retryCount + 1)) + $retryCount = $retryCount + 1 } } } - } While ($Stoploop -eq $false) - if ($n -eq 250000){ - "More than 250000 events in one day, consider logs incomplete between $($startdate) and $($enddate) for $($userId)" | Write-Log -LogPath $logfile -LogLevel "Warning" + } while ($stopLoop -eq $false) + + if ($countMailboxAuditLogEntries -gt 250000){ + "More than 250000 events in one day, consider those events incomplete between $($startDate) and $($endDate) for $($userId)" | Write-Log -LogPath $logFile -LogLevel "Warning" } - if ($n -gt 0){ - $o | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 -append - $o = @() - $n = 0 + if ($countMailboxAuditLogEntries -gt 0){ + "Collected $countMailboxAuditLogEntries MailboxAuditLog events for $startDate - $endDate for user $userId" | Write-Log -LogPath $logFile -LogLevel "Info" + $mailboxAuditLogEntries | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 -Append + $mailboxAuditLogEntries = @() + $countMailboxAuditLogEntries = 0 + } + else { + "0 MailboxAuditLog events between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} were found for user $userId" -f ($startDate, $endDate) | Write-Log -LogPath $logFile -LogLevel "Warning" } } } \ No newline at end of file diff --git a/DFIR-O365RC/Get-AADApps.ps1 b/DFIR-O365RC/Get-AADApps.ps1 index c42e26b..ab0ba7a 100755 --- a/DFIR-O365RC/Get-AADApps.ps1 +++ b/DFIR-O365RC/Get-AADApps.ps1 @@ -1,239 +1,187 @@ -Function Get-AADApps { +function Get-AADApps { <# .SYNOPSIS - The Get-AADApps function dumps in JSON files Azure AD applications and Service Principals related events for a specific time range and enriches the object with the application or service principal configuration. + The Get-AADApps function dumps in JSON files Entra ID applications and Service Principals related events for a specific time range and tries to enrich the objects with the application or service principal configurations. .EXAMPLE - - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-30) - PS C:\>Get-AADApps -startdate $startdate -enddate $enddate + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-30) - Dump all Azure AD applications and Service Principals related events. + PS C:\>Get-AADApps -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath + Dump all Entra ID applications and Service Principals related events for the last 30 days. #> - + param ( [Parameter(Mandatory = $true)] - [DateTime]$Enddate, + [DateTime]$startDate, [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, [Parameter(Mandatory = $false)] - [String]$logfile = "Get-AADApps.log" + [String]$logFile = "Get-AADApps.log" ) - $currentpath = (get-location).path - - $logfile = $currentpath + "\" + $logfile - "Getting MSGraph Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service MSGraph -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName - $tenant = ($user).split("@")[1] - $foldertoprocess = $currentpath + '\azure_ad_apps' - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory | Out-Null} - $outputfile = $foldertoprocess + "\AADApps_" + $tenant + ".json" - - - # Get Service Principal related events - "Getting all Service principal related events via auditlog" | Write-Log -LogPath $logfile - $Auditstart = "{0:s}" -f $StartDate + "Z" - $Auditend = "{0:s}" -f $Enddate + "Z" - $uri = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=(activityDisplayName eq 'Consent to application' or activityDisplayName eq 'Add app role assignment to service principal' or activityDisplayName eq 'Add delegated permission grant' or activityDisplayName eq 'Add service principal credentials' or activityDisplayName eq 'Add service principal' or activityDisplayName eq 'Add OAuth2PermissionGrant') and activityDateTime gt $($Auditstart) and activityDateTime lt $($Auditend)" - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1b730954-1685-4b74-9bfd-dac224a7b894"} - $SPEvents = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uri -logfile $logfile -app $app -user $user - - #Get all Service Principals - "Getting all service principals" | Write-Log -LogPath $logfile - $uriSP = "https://graph.microsoft.com/v1.0/servicePrincipals/" - $ALLServicePrincipals = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uriSP -logfile $logfile -app $app -user $user - $sp_outputfile = $foldertoprocess + "\AADApps_" + $tenant + "_service_principals_raw.json" - $ALLServicePrincipals | ConvertTo-Json -Depth 99 | out-file $sp_outputfile -encoding UTF8 - - $EnrichedSPEvents = @() - $UniqServicePrincipals = $SPEvents | Select-Object -ExpandProperty targetResources | Group-Object -Property id - #Loop through Service Principals in activity logs - foreach($ServicePrincipal in $UniqServicePrincipals) - { - - #Get Service Principal object - $SPObject = $ALLServicePrincipals | where-object {$_.Id -eq $ServicePrincipal.Name} - $EventsperSP = $SPEvents | where-object { $_.targetResources.id -eq $ServicePrincipal.Name} - - if($SPObject) - { - "Getting Oauth PermissionGrants for $($ServicePrincipal.Name) Service principal" | Write-Log -LogPath $logfile - $uriOauth = "https://graph.microsoft.com/v1.0/servicePrincipals/$($ServicePrincipal.Name)/oauth2PermissionGrants/" - $SPOAuth = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uriOauth -logfile $logfile -app $app -user $user - $delegatedconsentautorisations = (($SPOAuth | Group-Object -Property scope).Name) -join "," - $delegatedconsentTypes = (($SPOAuth | Group-Object -Property consentType).Name) -join "," - - "Getting appRoleAssignments for $($ServicePrincipal.Name) Service principal" | Write-Log -LogPath $logfile - $uriOauth = "https://graph.microsoft.com/v1.0/servicePrincipals/$($ServicePrincipal.Name)/appRoleAssignments/" - $SPOAuth = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uriOauth -logfile $logfile -app $app -user $user - $appRoleIdAssignements = (($SPOAuth | Group-Object -Property appRoleId).Name) -join "," - - $EventsperSP | add-member -MemberType NoteProperty -Name appRoleIdAssignements -Value $appRoleIdAssignements -force - $EventsperSP | add-member -MemberType NoteProperty -Name delegatedconsentautorisations -Value $delegatedconsentautorisations -force - $EventsperSP | add-member -MemberType NoteProperty -Name delegatedconsentTypes -Value $delegatedconsentTypes -force - $EventsperSP | add-member -MemberType NoteProperty -Name ServicePrincipalId -Value $SPObject.id -force - $EventsperSP | add-member -MemberType NoteProperty -Name appDescription -Value $SPObject.appDescription -force - $EventsperSP | add-member -MemberType NoteProperty -Name verifiedPublisher -Value $SPObject.verifiedPublisher -force - $EventsperSP | add-member -MemberType NoteProperty -Name accountEnabled -Value $SPObject.accountEnabled -force - $EventsperSP | add-member -MemberType NoteProperty -Name appDisplayName -Value $SPObject.appDisplayName -force - $EventsperSP | add-member -MemberType NoteProperty -Name appId -Value $SPObject.appId -force - $EventsperSP | add-member -MemberType NoteProperty -Name appOwnerOrganizationId -Value $SPObject.appOwnerOrganizationId -force - $EventsperSP | add-member -MemberType NoteProperty -Name createdDateTime -Value $SPObject.createdDateTime -force - $EventsperSP | add-member -MemberType NoteProperty -Name homepage -Value $SPObject.homepage -force - $EventsperSP | add-member -MemberType NoteProperty -Name loginUrl -Value $SPObject.loginUrl -force - $EventsperSP | add-member -MemberType NoteProperty -Name replyUrls -Value $SPObject.replyUrls -force - $EventsperSP | add-member -MemberType NoteProperty -Name addIns -Value $SPObject.addIns -force - $EventsperSP | add-member -MemberType NoteProperty -Name appRoles -Value $SPObject.appRoles -force - $EventsperSP | add-member -MemberType NoteProperty -Name info -Value $SPObject.info -force - $EventsperSP | add-member -MemberType NoteProperty -Name oauth2PermissionScopes -Value $SPObject.oauth2PermissionScopes -force - - $EnrichedSPEvents += $EventsperSP + + $currentPath = (Get-Location).path + + $logFile = $currentPath + "\" + $logFile + + $folderToProcess = $currentPath + '\azure_ad_apps' + if ((Test-Path $folderToProcess) -eq $false){ + New-Item $folderToProcess -Type Directory | Out-Null + } + $outputFile = $folderToProcess + "\AADApps_" + $tenant + ".json" + + $maxStartDate = (Get-Date).AddDays(-30) + if ($startDate -lt $maxStartDate){ + Write-Warning "You can only get events in the last 30 days with the Audit Log. Setting startDate to $maxStartDate" + "You can only get events in the last 30 days with the Audit Log. Setting startDate to $maxStartDate" | Write-Log -LogPath $logFile -LogLevel "Warning" + if ($endDate -lt $maxStartDate){ + Write-Host "Incompatible endDate: $endDate. Exiting" + "Incompatible endDate: $endDate. Exiting" | Write-Log -LogPath $logFile + exit + } + $startDate = $maxStartDate + } + + $cert, $null, $null = Import-Certificate -certificatePath $certificatePath -logFile $logFile + Connect-MicrosoftGraphApplication -certificate $cert -appId $appId -tenant $tenant -logFile $logFile + + # Check if Microsoft Entra ID P1 is enabled + "Checking Microsoft Entra ID P1"| Write-Log -LogPath $logFile + try { + $null = Get-MgBetaAuditLogSignIn -Top 1 -All -ErrorAction Stop + } + catch { + if ($_.ErrorDetails.Message -like "*RequestFromNonPremiumTenant*"){ + Write-Warning "Entra ID P1 is not enabled tenant-wide. You should buy at least one Entra ID P1 licence to be able to retrieve the full audit log" + "Entra ID P1 is not enabled tenant-wide. You should buy at least one Entra ID P1 licence to be able to retrieve the full audit log" | Write-Log -LogPath $logFile -LogLevel "Warning" + $maxStartDate = (Get-Date).AddDays(-7) + if ($startDate -lt $maxStartDate){ + Write-Warning "Entra ID P1 not enabled tenant-wide, you can only get 7 days of Audit Log. Setting startDate to $maxStartDate" + "Entra ID P1 not enabled tenant-wide, you can only get 7 days of Audit Log. Setting startDate to $maxStartDate" | Write-Log -LogPath $logFile -LogLevel "Warning" + $startDate = [DateTime]$maxStartDate.ToString("yyyy-MM-dd") + if ($endDate -lt $startDate){ + Write-Error "Incompatible endDate: $endDate. Exiting" + "Incompatible endDate: $endDate. Exiting" | Write-Log -LogPath $logFile -LogLevel "Error" + exit + } + } + } + } + + # Get service principal related events + $auditStart = "{0:s}" -f $startDate + "Z" + $auditEnd = "{0:s}" -f $endDate + "Z" + "Getting all service principal related events via Audit Log" | Write-Log -LogPath $logFile + $servicePrincipalEvents = Get-MgBetaAuditLogDirectoryAudit -All -Filter "activityDateTime ge $($auditStart) and activityDateTime lt $($auditEnd) and (activityDisplayName eq 'Consent to application' or activityDisplayName eq 'Add app role assignment to service principal' or activityDisplayName eq 'Add delegated permission grant' or activityDisplayName eq 'Add service principal credentials' or activityDisplayName eq 'Add service principal' or activityDisplayName eq 'Add OAuth2PermissionGrant')" -ErrorAction Stop + + # Get all service principals + "Getting all service principals" | Write-Log -LogPath $logFile + $allServicePrincipals = Get-MgServicePrincipal -All -ErrorAction Stop + $servicePrincipalsOutputFile = $folderToProcess + "\AADApps_" + $tenant + "_service_principals_raw.json" + $allServicePrincipals | ConvertTo-Json -Depth 99 | Out-File $servicePrincipalsOutputFile -Encoding UTF8 + + $enrichedServicePrincipalEvents = @() + $uniqueServicePrincipals = $servicePrincipalEvents | Select-Object -ExpandProperty targetResources | Group-Object -Property Id + + # Loop through Service Principals seen in Audit Log + foreach ($uniqueServicePrincipal in $uniqueServicePrincipals){ + # Get Service Principal object + $servicePrincipalObject = $allServicePrincipals | Where-Object {$_.Id -eq $uniqueServicePrincipal.Name} + $eventsPerServicePrincipal = $servicePrincipalEvents | Where-Object { $_.targetResources.Id -eq $uniqueServicePrincipal.Name} + + if ($servicePrincipalObject){ + "Getting OAuth2PermissionGrants for $($uniqueServicePrincipal.Name) Service Principal" | Write-Log -LogPath $logFile + $servicePrincipalOAuth = Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $($uniqueServicePrincipal.Name) -All -ErrorAction Stop + $delegatedConsentAutorisations = (($servicePrincipalOAuth | Group-Object -Property Scope).Name) -join "," + $delegatedConsentTypes = (($servicePrincipalOAuth | Group-Object -Property ConsentType).Name) -join "," + + "Getting appRoleAssignments for $($uniqueServicePrincipal.Name) Service Principal" | Write-Log -LogPath $logFile + $servicePrincipalAppRoleAssignement = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $($uniqueServicePrincipal.Name) -All -ErrorAction Stop + $appRoleIdAssignements = (($servicePrincipalAppRoleAssignement | Group-Object -Property AppRoleId).Name) -join "," + + if (-not ($null -eq $appRoleIdAssignements)){$eventsPerServicePrincipal | Add-Member -MemberType NoteProperty -Name "servicePrincipal_appRoleIdAssignements" -Value $appRoleIdAssignements -Force} + if (-not ($null -eq $delegatedConsentAutorisations)){$eventsPerServicePrincipal | Add-Member -MemberType NoteProperty -Name "servicePrincipal_delegatedConsentAutorisations" -Value $delegatedConsentAutorisations -Force} + if (-not ($null -eq $delegatedConsentTypes)){$eventsPerServicePrincipal | Add-Member -MemberType NoteProperty -Name "servicePrincipal_delegatedConsentTypes" -Value $delegatedConsentTypes -Force} + + $servicePrincipalObject.PSObject.Properties | ForEach-Object { + $newPropertyName = "servicePrincipal_$($_.Name)" + if (-not ($eventsPerServicePrincipal.PSObject.Properties.Name -contains $newPropertyName)){ + $eventsPerServicePrincipal | Add-Member -MemberType NoteProperty -Name $newPropertyName -Value $_.Value -Force + } + } + } + + # If the consent is not successful or the ServicePrincipal was deleted. + else { + "The service principal $($uniqueServicePrincipal.Name) does not exist in the tenant, creation operation failed or the service principal was deleted" | Write-Log -LogPath $logFile } - #If operation such as consent fails ServicePrincipal is not created. Or Service Principal can be deleted after the operation... - else{ - "The Service principal $($ServicePrincipal.Name) does not exist in the tenant, operation failed or Service Principal was deleted" | Write-Log -LogPath $logfile - $EventsperSP | add-member -MemberType NoteProperty -Name appRoleIdAssignements -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name delegatedconsentautorisations -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name delegatedconsentTypes -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name ServicePrincipalId -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name appDescription -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name verifiedPublisher -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name accountEnabled -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name appDisplayName -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name appId -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name appOwnerOrganizationId -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name createdDateTime -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name homepage -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name loginUrl -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name replyUrls -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name addIns -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name appRoles -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name info -Value "NotAvailable" -force - $EventsperSP | add-member -MemberType NoteProperty -Name oauth2PermissionScopes -Value "NotAvailable" -force - - $EnrichedSPEvents += $EventsperSP - - - } - + $enrichedServicePrincipalEvents += $eventsPerServicePrincipal } - - - # Get Application related events - "Getting all Application events via auditlog" | Write-Log -LogPath $logfile - $Auditstart = "{0:s}" -f $StartDate + "Z" - $Auditend = "{0:s}" -f $Enddate + "Z" - $uri = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=(activityDisplayName eq 'Add application' or startswith(activityDisplayName,'Update application')) and activityDateTime gt $($Auditstart) and activityDateTime lt $($Auditend)" - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1b730954-1685-4b74-9bfd-dac224a7b894"} - $AppEvents = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uri -logfile $logfile -app $app -user $user - - #Get all Apps - "Getting all Apps" | Write-Log -LogPath $logfile - $uriAPP = "https://graph.microsoft.com/v1.0/applications/" - $ALLApps = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uriAPP -logfile $logfile -app $app -user $user - #Get all deleted Apps - "Getting all deleted Apps" | Write-Log -LogPath $logfile - $uriDelAPPs = "https://graph.microsoft.com/v1.0/directory/deleteditems/microsoft.graph.application" - $DelApps = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uriDelAPPs -logfile $logfile -app $app -user $user - #merge existing and deleted Apps - $ALLApps += $DelApps - - $apps_outputfile = $foldertoprocess + "\AADApps_" + $tenant + "_applications_raw.json" - $ALLApps | ConvertTo-Json -Depth 99 | out-file $apps_outputfile -encoding UTF8 - - $EnrichedAppEvents = @() - $UniqApps = $AppEvents| Select-Object -ExpandProperty targetResources | Group-Object -Property id - #Loop through Apps present in activity logs - - foreach($UniqApp in $UniqApps) - { - - #Get Service Principal object - $AppObject = $ALLApps | where-object {$_.Id -eq $UniqApp.Name} - $EventsperApp = $AppEvents | where-object { $_.targetResources.id -eq $UniqApp.Name} - - if($AppObject) - { - - $EventsperApp | add-member -MemberType NoteProperty -Name appId -Value $AppObject.appId -force - $EventsperApp | add-member -MemberType NoteProperty -Name deletedDateTime -Value $AppObject.deletedDateTime -force - $EventsperApp | add-member -MemberType NoteProperty -Name applicationTemplateId -Value $AppObject.applicationTemplateId -force - $EventsperApp | add-member -MemberType NoteProperty -Name createdDateTime -Value $AppObject.createdDateTime -force - $EventsperApp | add-member -MemberType NoteProperty -Name appDisplayName -Value $AppObject.displayName -force - $EventsperApp | add-member -MemberType NoteProperty -Name appDescription -Value $AppObject.description -force - $EventsperApp | add-member -MemberType NoteProperty -Name groupMembershipClaims -Value $AppObject.groupMembershipClaims -force - $EventsperApp | add-member -MemberType NoteProperty -Name identifierUris -Value $AppObject.identifierUris -force - $EventsperApp | add-member -MemberType NoteProperty -Name isDeviceOnlyAuthSupported -Value $AppObject.isDeviceOnlyAuthSupported -force - $EventsperApp | add-member -MemberType NoteProperty -Name isFallbackPublicClient -Value $AppObject.isFallbackPublicClient -force - $EventsperApp | add-member -MemberType NoteProperty -Name publisherDomain -Value $AppObject.publisherDomain -force - $EventsperApp | add-member -MemberType NoteProperty -Name signInAudience -Value $AppObject.signInAudience -force - $EventsperApp | add-member -MemberType NoteProperty -Name verifiedPublisher -Value $AppObject.verifiedPublisher -force - $EventsperApp | add-member -MemberType NoteProperty -Name defaultRedirectUri -Value $AppObject.defaultRedirectUri -force - $EventsperApp | add-member -MemberType NoteProperty -Name addIns -Value $AppObject.addIns -force - $EventsperApp | add-member -MemberType NoteProperty -Name appRoles -Value $AppObject.appRoles -force - $EventsperApp | add-member -MemberType NoteProperty -Name info -Value $AppObject.info -force - $EventsperApp | add-member -MemberType NoteProperty -Name keyCredentials -Value $AppObject.keyCredentials -force - $EventsperApp | add-member -MemberType NoteProperty -Name optionalClaims -Value $AppObject.optionalClaims -force - $EventsperApp | add-member -MemberType NoteProperty -Name passwordCredentials -Value $AppObject.passwordCredentials -force - $EventsperApp | add-member -MemberType NoteProperty -Name publicClient -Value $AppObject.publicClient -force - $EventsperApp | add-member -MemberType NoteProperty -Name requiredResourceAccess -Value $AppObject.requiredResourceAccess -force - $EventsperApp | add-member -MemberType NoteProperty -Name web -Value $AppObject.web -force - - $EnrichedAppEvents += $EventsperApp + # Get application related events + "Getting all application related events via Audit Log" | Write-Log -LogPath $logFile + $auditStart = "{0:s}" -f $startDate + "Z" + $auditEnd = "{0:s}" -f $endDate + "Z" + + $appEvents = Get-MgBetaAuditLogDirectoryAudit -All -Filter "activityDateTime ge $($auditStart) and activityDateTime lt $($auditEnd) and (activityDisplayName eq 'Add application' or startswith(activityDisplayName, 'Update application'))" -ErrorAction Stop + + # Get all applications + "Getting all existing applications" | Write-Log -LogPath $logFile + $existingApplications = Get-MgApplication -All -ErrorAction Stop + $existingApplicationsOutputFile = $folderToProcess + "\AADApps_" + $tenant + "_existing_applications_raw.json" + $existingApplications | ConvertTo-Json -Depth 99 | Out-File $existingApplicationsOutputFile -Encoding UTF8 + + # Get all deleted applications + "Getting all deleted applications" | Write-Log -LogPath $logFile + $deletedApplications = Get-MgDirectoryDeletedItemAsApplication -All -ErrorAction Stop + $deletedApplicationsOutputFile = $folderToProcess + "\AADApps_" + $tenant + "_deleted_applications_raw.json" + $deletedApplications | ConvertTo-Json -Depth 99 | Out-File $deletedApplicationsOutputFile -Encoding UTF8 + + $enrichedAppEvents = @() + $uniqueApplications = $appEvents | Select-Object -ExpandProperty targetResources | Group-Object -Property Id + + # Loop through applications present in audit log + foreach ($uniqueApplication in $uniqueApplications){ + # Get Application object + $applicationObject = $existingApplications | Where-Object {$_.Id -eq $uniqueApplication.Name} + if ($null -eq $applicationObject){ + $applicationObject = $deletedApplications | Where-Object {$_.Id -eq $uniqueApplication.Name} + } + $eventsPerApplication = $appEvents | Where-Object { $_.targetResources.id -eq $uniqueApplication.Name} + if ($applicationObject){ + $applicationObject.PSObject.Properties | ForEach-Object { + $newPropertyName = "application_$($_.Name)" + if (-not ($eventsPerApplication.PSObject.Properties.Name -contains $newPropertyName)){ + $eventsPerApplication | Add-Member -MemberType NoteProperty -Name $newPropertyName -Value $_.Value -Force + } + } + } + else { + "The application $($uniqueApplication.Name) does not exist in the tenant" | Write-Log -LogPath $logFile } - - else{ - "The App $($UniqApp.Name) does not exist in the tenant" | Write-Log -LogPath $logfile - - - $EventsperApp | add-member -MemberType NoteProperty -Name appId -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name deletedDateTime -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name applicationTemplateId -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name createdDateTime -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name appDisplayName -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name appDescription -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name groupMembershipClaims -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name identifierUris -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name isDeviceOnlyAuthSupported -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name isFallbackPublicClient -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name publisherDomain -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name signInAudience -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name verifiedPublisher -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name defaultRedirectUri -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name addIns -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name appRoles -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name info -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name keyCredentials -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name optionalClaims -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name passwordCredentials -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name publicClient -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name requiredResourceAccess -Value "NotAvailable" -force - $EventsperApp | add-member -MemberType NoteProperty -Name web -Value "NotAvailable" -force - - - $EnrichedAppEvents += $EventsperApp - } - + $enrichedAppEvents += $eventsPerApplication } - - $nbEnrichedAppEvents = ($EnrichedAppEvents | Measure-Object).count - "Collected $($nbEnrichedAppEvents) enriched Applications related events" | Write-Log -LogPath $logfile - - - $nbEnrichedSPEvents = ($EnrichedSPEvents | Measure-Object).count - "Collected $($nbEnrichedSPEvents) enriched Service principal related events" | Write-Log -LogPath $logfile - $TotalEnrichedEvents = $nbEnrichedSPEvents + $nbEnrichedAppEvents - "Dumping $($TotalEnrichedEvents) enriched events to $($outputfile)" | Write-Log -LogPath $logfile - - @($EnrichedAppEvents; $EnrichedSPEvents) | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 + $nbEnrichedApplicationEvents = ($enrichedAppEvents | Measure-Object).Count + "Collected $($nbEnrichedApplicationEvents) enriched application related events" | Write-Log -LogPath $logFile + + $nbEnrichedServicePrincipalEvents = ($enrichedServicePrincipalEvents | Measure-Object).Count + "Collected $($nbEnrichedServicePrincipalEvents) enriched service principal related events" | Write-Log -LogPath $logFile + + $totalEnrichedEvents = $nbEnrichedServicePrincipalEvents + $nbEnrichedApplicationEvents + "Dumping $($totalEnrichedEvents) enriched events to $($outputFile)" | Write-Log -LogPath $logFile + + @($enrichedAppEvents; $enrichedServicePrincipalEvents) | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 } diff --git a/DFIR-O365RC/Get-AADDevices.ps1 b/DFIR-O365RC/Get-AADDevices.ps1 index 9e77eda..978c97e 100755 --- a/DFIR-O365RC/Get-AADDevices.ps1 +++ b/DFIR-O365RC/Get-AADDevices.ps1 @@ -1,170 +1,152 @@ -Function Get-AADDevices { +function Get-AADDevices { <# .SYNOPSIS - The Get-AADApps function dumps in JSON files Azure AD devices related events for a specific time range and enriches the object with the device configuration. + The Get-AADDevices function dumps in JSON files Entra ID devices related events for a specific time range and tries to enrich the objects with the device configuration. If you want to limit the number of events considered by removing the "Update device" event, you can use the -filterUpdateDevice switch .EXAMPLE - - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-30) - PS C:\>Get-AADDevices -startdate $startdate -enddate $enddate + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-30) + + PS C:\>Get-AADDevices -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath + + Dump all Entra ID devices related events for the last 30 days. - Dump all Azure AD devices related events. + PS C:\>Get-AADDevices -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -filterUpdateDevice + Dump all Entra ID devices related events, excluding "Update device" events, for the last 30 days. #> - + param ( [Parameter(Mandatory = $true)] - [DateTime]$Enddate, + [DateTime]$startDate, [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false)] - [boolean]$Allevents=$true, + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, + [Switch]$filterUpdateDevice=$false, [Parameter(Mandatory = $false)] - [String]$logfile = "Get-AADDevices.log" + [String]$logFile = "Get-AADDevices.log" ) - $currentpath = (get-location).path - - $logfile = $currentpath + "\" + $logfile - "Getting MSGraph Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service MSGraph -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName - $tenant = ($user).split("@")[1] - $foldertoprocess = $currentpath + '\azure_ad_devices' - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory | Out-Null} - $outputfile = $foldertoprocess + "\AADDevices_" + $tenant + ".json" + + $currentPath = (Get-Location).path + + $logFile = $currentPath + "\" + $logFile + $cert, $null, $null = Import-Certificate -certificatePath $certificatePath -logFile $logFile - # Get devices related events + $folderToProcess = $currentPath + '\azure_ad_devices' + if ((Test-Path $folderToProcess) -eq $false){ + New-Item $folderToProcess -Type Directory | Out-Null + } + $outputFile = $folderToProcess + "\AADDevices_" + $tenant + ".json" - $Auditstart = "{0:s}" -f $StartDate + "Z" - $Auditend = "{0:s}" -f $Enddate + "Z" - if($Allevents -eq $false) - { - "Getting all important device related events via auditlog" | Write-Log -LogPath $logfile - $uri = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=(activityDisplayName eq 'Add device' or activityDisplayName eq 'Device no longer compliant' or activityDisplayName eq 'Add registered users to device' or activityDisplayName eq 'Add registered owner to device' or activityDisplayName eq 'Delete device' or activityDisplayName eq 'Device no longer managed' or activityDisplayName eq 'Remove registered users from device' or activityDisplayName eq 'Remove registered owner from device') and activityDateTime gt $($Auditstart) and activityDateTime lt $($Auditend)" + $maxStartDate = (Get-Date).AddDays(-30) + if ($startDate -lt $maxStartDate){ + Write-Warning "You can only get events in the last 30 days with the Audit Log. Setting startDate to $maxStartDate" + "You can only get events in the last 30 days with the Audit Log. Setting startDate to $maxStartDate" | Write-Log -LogPath $logFile -LogLevel "Warning" + if ($endDate -lt $maxStartDate){ + Write-Host "Incompatible endDate: $endDate. Exiting" + "Incompatible endDate: $endDate. Exiting" | Write-Log -LogPath $logFile + exit + } + $startDate = $maxStartDate + } + + Connect-MicrosoftGraphApplication -certificate $cert -appId $appId -tenant $tenant -logFile $logFile + + # Check if Microsoft Entra ID P1 is enabled + "Checking Microsoft Entra ID P1"| Write-Log -LogPath $logFile + try { + $null = Get-MgBetaAuditLogSignIn -Top 1 -All -ErrorAction Stop + } + catch { + if ($_.ErrorDetails.Message -like "*RequestFromNonPremiumTenant*"){ + Write-Warning "Entra ID P1 is not enabled tenant-wide. You should buy at least one Entra ID P1 licence to be able to retrieve the full audit log" + "Entra ID P1 is not enabled tenant-wide. You should buy at least one Entra ID P1 licence to be able to retrieve the full audit log" | Write-Log -LogPath $logFile -LogLevel "Warning" + $maxStartDate = (Get-Date).AddDays(-7) + if ($startDate -lt $maxStartDate){ + Write-Warning "Entra ID P1 not enabled tenant-wide, you can only get 7 days of Audit Log. Setting startDate to $maxStartDate" + "Entra ID P1 not enabled tenant-wide, you can only get 7 days of Audit Log. Setting startDate to $maxStartDate" | Write-Log -LogPath $logFile -LogLevel "Warning" + $startDate = [DateTime]$maxStartDate.ToString("yyyy-MM-dd") + if ($endDate -lt $startDate){ + Write-Error "Incompatible endDate: $endDate. Exiting" + "Incompatible endDate: $endDate. Exiting" | Write-Log -LogPath $logFile -LogLevel "Error" + exit + } + } } + } + + # Get device related events + $auditStart = "{0:s}" -f $startDate + "Z" + $auditEnd = "{0:s}" -f $endDate + "Z" + if ($filterUpdateDevice -eq $true){ + "Getting all device related events via Audit Log (except 'Update device')" | Write-Log -LogPath $logFile + $deviceEvents = Get-MgBetaAuditLogDirectoryAudit -All -Filter "activityDateTime ge $($auditStart) and activityDateTime lt $($auditEnd) and (activityDisplayName eq 'Add device' or activityDisplayName eq 'Device no longer compliant' or activityDisplayName eq 'Add registered users to device' or activityDisplayName eq 'Add registered owner to device' or activityDisplayName eq 'Delete device' or activityDisplayName eq 'Device no longer managed' or activityDisplayName eq 'Remove registered users from device' or activityDisplayName eq 'Remove registered owner from device')" -ErrorAction Stop + } else { - "Getting all device related events via auditlog" | Write-Log -LogPath $logfile - $uri = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=(activityDisplayName eq 'Add device' or activityDisplayName eq 'Device no longer compliant' or activityDisplayName eq 'Add registered users to device' or activityDisplayName eq 'Add registered owner to device' or activityDisplayName eq 'Delete device' or activityDisplayName eq 'Device no longer managed' or activityDisplayName eq 'Remove registered users from device' or activityDisplayName eq 'Remove registered owner from device' or activityDisplayName eq 'Update device') and activityDateTime gt $($Auditstart) and activityDateTime lt $($Auditend)" - + "Getting all device related events via Audit Log" | Write-Log -LogPath $logFile + $deviceEvents = Get-MgBetaAuditLogDirectoryAudit -All -Filter "activityDateTime ge $($auditStart) and activityDateTime lt $($auditEnd) and (activityDisplayName eq 'Add device' or activityDisplayName eq 'Device no longer compliant' or activityDisplayName eq 'Add registered users to device' or activityDisplayName eq 'Add registered owner to device' or activityDisplayName eq 'Delete device' or activityDisplayName eq 'Device no longer managed' or activityDisplayName eq 'Remove registered users from device' or activityDisplayName eq 'Remove registered owner from device' or activityDisplayName eq 'Update device')" -ErrorAction Stop } - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1b730954-1685-4b74-9bfd-dac224a7b894"} - $DeviceEvents = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uri -logfile $logfile -app $app -user $user - - #Get all Service Principals - "Getting all devices" | Write-Log -LogPath $logfile - $uriSP = "https://graph.microsoft.com/v1.0/devices" - $ALLDevices = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uriSP -logfile $logfile -app $app -user $user - $nbALLDevices = ($ALLDevices | Measure-Object).count - "Total number of devices in tenant is $($nbALLDevices)" | Write-Log -LogPath $logfile - - $EnrichedDeviceEvents = @() - $UniqDevices = $DeviceEvents | Select-Object -ExpandProperty targetResources | Group-Object -Property id - #Loop through devices in activity logs - foreach($UniqDevice in $UniqDevices) - { - - #Get Service Principal object - $DeviceObject = $ALLDevices | where-object {$_.id -eq $UniqDevice.Name} - $EventsperDevice = $DeviceEvents | where-object { $_.targetResources.id -eq $UniqDevice.Name} - - if($DeviceObject) - { - "Get owners and users for $($UniqDevice.Name) device" | Write-Log -LogPath $logfile - $uriOwners = "https://graph.microsoft.com/v1.0/devices/$($UniqDevice.Name)/registeredOwners/" - $DeviceOwners = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uriOwners -logfile $logfile -app $app -user $user - $owners = (($DeviceOwners | Group-Object -Property userPrincipalName).Name) -join "," - $olanguage = (($DeviceOwners | Group-Object -Property preferredLanguage).Name) -join "," - - $uriUsers = "https://graph.microsoft.com/v1.0/devices/$($UniqDevice.Name)/registeredUsers/" - $DeviceUsers = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uriUsers -logfile $logfile -app $app -user $user - $users = (($DeviceUsers | Group-Object -Property userPrincipalName).Name) -join "," - $ulanguage = (($DeviceUsers | Group-Object -Property preferredLanguage).Name) -join "," - - $EventsperDevice | add-member -MemberType NoteProperty -Name deviceowners -Value $owners -force - $EventsperDevice | add-member -MemberType NoteProperty -Name owners_language -Value $olanguage -force - $EventsperDevice | add-member -MemberType NoteProperty -Name deviceusers -Value $users -force - $EventsperDevice | add-member -MemberType NoteProperty -Name users_language -Value $ulanguage -force - - $EventsperDevice | add-member -MemberType NoteProperty -Name deletedDateTime -Value $DeviceObject.deletedDateTime -force - $EventsperDevice | add-member -MemberType NoteProperty -Name accountEnabled -Value $DeviceObject.accountEnabled -force - $EventsperDevice | add-member -MemberType NoteProperty -Name approximateLastSignInDateTime -Value $DeviceObject.approximateLastSignInDateTime -force - $EventsperDevice | add-member -MemberType NoteProperty -Name complianceExpirationDateTime -Value $DeviceObject.complianceExpirationDateTime -force - $EventsperDevice | add-member -MemberType NoteProperty -Name createdDateTime -Value $DeviceObject.createdDateTime -force - $EventsperDevice | add-member -MemberType NoteProperty -Name deviceMetadata -Value $DeviceObject.deviceMetadata -force - $EventsperDevice | add-member -MemberType NoteProperty -Name deviceVersion -Value $DeviceObject.deviceVersion -force - $EventsperDevice | add-member -MemberType NoteProperty -Name displayName -Value $DeviceObject.displayName -force - $EventsperDevice | add-member -MemberType NoteProperty -Name extensionAttributes -Value $DeviceObject.extensionAttributes -force - $EventsperDevice | add-member -MemberType NoteProperty -Name externalSourceName -Value $DeviceObject.externalSourceName -force - $EventsperDevice | add-member -MemberType NoteProperty -Name isCompliant -Value $DeviceObject.isCompliant -force - $EventsperDevice | add-member -MemberType NoteProperty -Name isManaged -Value $DeviceObject.isManaged -force - $EventsperDevice | add-member -MemberType NoteProperty -Name manufacturer -Value $DeviceObject.manufacturer -force - $EventsperDevice | add-member -MemberType NoteProperty -Name mdmAppId -Value $DeviceObject.mdmAppId -force - $EventsperDevice | add-member -MemberType NoteProperty -Name model -Value $DeviceObject.model -force - $EventsperDevice | add-member -MemberType NoteProperty -Name onPremisesLastSyncDateTime -Value $DeviceObject.onPremisesLastSyncDateTime -force - $EventsperDevice | add-member -MemberType NoteProperty -Name onPremisesSyncEnabled -Value $DeviceObject.onPremisesSyncEnabled -force - $EventsperDevice | add-member -MemberType NoteProperty -Name operatingSystem -Value $DeviceObject.operatingSystem -force - $EventsperDevice | add-member -MemberType NoteProperty -Name physicalIds -Value $DeviceObject.physicalIds -force - $EventsperDevice | add-member -MemberType NoteProperty -Name profileType -Value $DeviceObject.profileType -force - $EventsperDevice | add-member -MemberType NoteProperty -Name sourceType -Value $DeviceObject.sourceType -force - $EventsperDevice | add-member -MemberType NoteProperty -Name systemLabels -Value $DeviceObject.systemLabels -force - $EventsperDevice | add-member -MemberType NoteProperty -Name trustType -Value $DeviceObject.trustType -force - $EventsperDevice | add-member -MemberType NoteProperty -Name alternativeSecurityIds -Value $DeviceObject.alternativeSecurityIds -force - $EnrichedDeviceEvents += $EventsperDevice + + # Get all devices + "Getting all devices" | Write-Log -LogPath $logFile + $allDevices = Get-MgDevice -All -ErrorAction Stop + $devicesOutputFile = $folderToProcess + "\AADDevices_" + $tenant + "_devices_raw.json" + $allDevices | ConvertTo-Json -Depth 99 | Out-File $devicesOutputFile -Encoding UTF8 + $countDevices = ($allDevices | Measure-Object).Count + "Total number of devices in the tenant is $($countDevices)" | Write-Log -LogPath $logFile + + $enrichedDeviceEvents = @() + $uniqueDevices = $deviceEvents | Select-Object -ExpandProperty targetResources | Group-Object -Property Id + + # Loop through Devices seen in Audit Log + foreach ($uniqueDevice in $uniqueDevices){ + # Get Device object + $deviceObject = $allDevices | Where-Object {$_.Id -eq $uniqueDevice.Name} + $eventsPerDevice = $deviceEvents | Where-Object { $_.targetResources.Id -eq $uniqueDevice.Name} + + if ($deviceObject){ + "Get owners and users for $($uniqueDevice.Name) Device" | Write-Log -LogPath $logFile + $deviceOwners = Get-MgDeviceRegisteredOwner -DeviceId $uniqueDevice.Name -All -ErrorAction Stop + $owners = (($deviceOwners | Group-Object -Property UserPrincipalName).Name) -join "," + $ownersLanguage = (($deviceOwners | Group-Object -Property PreferredLanguage).Name) -join "," + + $deviceUsers = Get-MgDeviceRegisteredUser -DeviceId $uniqueDevice.Name -All -ErrorAction Stop + $users = (($deviceUsers | Group-Object -Property UserPrincipalName).Name) -join "," + $usersLanguage = (($deviceUsers | Group-Object -Property PreferredLanguage).Name) -join "," + + if (-not ($null -eq $owners)){$eventsPerDevice | Add-Member -MemberType NoteProperty -Name "deviceOwners" -Value $owners -Force} + if (-not ($null -eq $ownersLanguage)){$eventsPerDevice | Add-Member -MemberType NoteProperty -Name "deviceOwnersLanguage" -Value $ownersLanguage -Force} + if (-not ($null -eq $users)){$eventsPerDevice | Add-Member -MemberType NoteProperty -Name "deviceUsers" -Value $users -Force} + if (-not ($null -eq $usersLanguage)){$eventsPerDevice | Add-Member -MemberType NoteProperty -Name "deviceUsersLanguage" -Value $usersLanguage -Force} + + $deviceObject.PSObject.Properties | ForEach-Object { + $newPropertyName = "device_$($_.Name)" + if (-not ($eventsPerDevice.PSObject.Properties.Name -contains $newPropertyName)){ + $eventsPerDevice | Add-Member -MemberType NoteProperty -Name $newPropertyName -Value $_.Value -Force + } + } } - - else{ - "The device $($UniqDevice.Name) does not exist in the tenant" | Write-Log -LogPath $logfile - - - $EventsperDevice | add-member -MemberType NoteProperty -Name appId -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name deviceowners -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name owners_language -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name deviceusers -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name users_language -Value "NotAvailable" -force - - $EventsperDevice | add-member -MemberType NoteProperty -Name deletedDateTime -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name accountEnabled -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name approximateLastSignInDateTime -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name complianceExpirationDateTime -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name createdDateTime -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name deviceMetadata -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name deviceVersion -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name displayName -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name extensionAttributes -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name externalSourceName -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name isCompliant -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name isManaged -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name manufacturer -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name mdmAppId -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name model -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name onPremisesLastSyncDateTime -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name onPremisesSyncEnabled -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name operatingSystem -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name physicalIds -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name profileType -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name sourceType -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name systemLabels -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name trustType -Value "NotAvailable" -force - $EventsperDevice | add-member -MemberType NoteProperty -Name alternativeSecurityIds -Value "NotAvailable" -force - - - $EnrichedDeviceEvents += $EventsperDevice - } - + else { + "The device $($uniqueDevice.Name) does not exist in the tenant" | Write-Log -LogPath $logFile + } + $enrichedDeviceEvents += $eventsPerDevice } - - - $nbEnrichedDeviceEvents = ($EnrichedDeviceEvents | Measure-Object).count - - "Dumping $($nbEnrichedDeviceEvents) enriched events to $($outputfile)" | Write-Log -LogPath $logfile - - $EnrichedDeviceEvents | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 + $nbEnrichedDeviceEvents = ($enrichedDeviceEvents | Measure-Object).Count + + "Dumping $($nbEnrichedDeviceEvents) enriched events to $($outputFile)" | Write-Log -LogPath $logFile + + $enrichedDeviceEvents | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 } diff --git a/DFIR-O365RC/Get-AADLogs.ps1 b/DFIR-O365RC/Get-AADLogs.ps1 index 4e8cdb0..2006ba2 100755 --- a/DFIR-O365RC/Get-AADLogs.ps1 +++ b/DFIR-O365RC/Get-AADLogs.ps1 @@ -1,340 +1,315 @@  -Function Get-AADLogs { +function Get-AADLogs { <# .SYNOPSIS - The Get-AADLogs function dumps in JSON files Azure AD signins logs and Azure AD audit logs for a specific time range. + The Get-AADLogs function dumps in JSON files Entra ID devices related events for a specific time range. Please note that a Microsoft Entra ID P1 tenant is required to get sign in logs and more than a week of audit logs. .EXAMPLE - - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-30) - PS C:\>Get-AADLogs -startdate $startdate -enddate $enddate + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-30) - Dump all Azure AD logs available + PS C:\>Get-AADLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath + + Dump all Entra ID logs for the last 30 days. + + PS C:\>Get-AADLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -dumpLogs "all" + + Dump all Entra ID logs for the last 30 days. + + PS C:\>Get-AADLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -dumpLogs "auditOnly" + + Dump all Entra ID audit logs for the last 30 days. + + PS C:\>Get-AADLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -dumpLogs "signInsOnly" + + Dump all Entra ID sign ins for the last 30 days. #> - + param ( [Parameter(Mandatory = $true)] - [DateTime]$Enddate, + [DateTime]$endDate, [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false)] - [ValidateSet("All","AuditOnly","SigninOnly")] - [String]$Dumplogs = "All", + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, + [ValidateSet("all","auditOnly","signInsOnly")] + [String]$dumpLogs = "all", [Parameter(Mandatory = $false)] - [String]$logfile = "Get-AADLogs.log" + [String]$logFile = "Get-AADLogs.log" ) - $currentpath = (get-location).path - $logfile = $currentpath + "\" + $logfile - "Getting MSGraph Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service MSGraph -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName - - if($Dumplogs -eq "All") - {"Processing signins and audit logs" | Write-Log -LogPath $logfile} - elseif($Dumplogs -eq "AuditOnly") - {"Processing audit logs only" | Write-Log -LogPath $logfile} - else - {"Processing signins logs only" | Write-Log -LogPath $logfile} - - Get-RSJob | Remove-RSJob -Force -#Test the directory size -$tenant = ($user).split("@")[1] -$aadtenantfolder = $currentpath + "\azure_ad_tenant" -if ((Test-Path $aadtenantfolder) -eq $false){New-Item $aadtenantfolder -Type Directory | Out-Null} -$outputfile = $aadtenantfolder + "\AADTenant_" + $tenant + ".json" -$tenantsize = "normal" -$uri = "https://graph.microsoft.com/v1.0/organization" -$tenantinfo = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Uri $Uri -Method Get -ContentType "application/json" -ErrorAction Stop -if($tenantinfo.value.directorySizeQuota.used -ge 100000) - { - if($Dumplogs -eq "All" -or $Dumplogs -eq "SigninOnly") - { - Write-Host "Directory size is huge, processing might be long, as a consequence signins logs will be filtered" -ForegroundColor "Yellow" -BackgroundColor "Black" - "Directory size is huge, processing might be long, as a consequence signins logs will be filtered" | Write-Log -LogPath $logfile -LogLevel "Warning" - } - else { - Write-Host "Directory size is huge, processing might be long" -ForegroundColor "Yellow" -BackgroundColor "Black" - "Directory size is huge, processing might be long" | Write-Log -LogPath $logfile -LogLevel "Warning" - } - if($Dumplogs -eq "All") - { - Write-Host "You might also want to dump Signins and audit logs separately by using the Dumplogs switch" -ForegroundColor "Yellow" -BackgroundColor "Black" - "Processing signins and audit logs depsite the tenant size" | Write-Log -LogPath $logfile -LogLevel "Warning" - } - - $tenantsize = "huge" - } -else { - "Normal size tenant, dumping all logs" | Write-Log -LogPath $logfile -} -"Dumping tenant information in azure_ad_tenant folder" | Write-Log -LogPath $logfile -$tenantinfo.value | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 - -#Refresh token -$token = Get-OAuthToken -Service MSGraph -silent $true -LoginHint $user -Logfile $logfile -$app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1b730954-1685-4b74-9bfd-dac224a7b894"} -if($null -eq $app) -{ - "No token cache available for MSGraph service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service MSGraph -silent $true -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1b730954-1685-4b74-9bfd-dac224a7b894"} -} -# Check if Azure P1 is enabled -"Checking permissions for $($user)"| Write-Log -LogPath $logfile -$token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://graph.microsoft.com/.default" -$uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$top=1" -$P1Enabled = $true -try { - $void = Invoke-WebRequest -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Uri $Uri -Method Get -ContentType "application/json" -ErrorAction Stop + $currentPath = (Get-Location).path + $logFile = $currentPath + "\" + $logFile + + if ($dumpLogs -eq "all"){ + "Processing sign in and audit logs" | Write-Log -LogPath $logFile } -catch { - if ($psversiontable.psversion.major -lt 6) { - $result = $_.Exception.Response.GetResponseStream() - $reader = New-Object System.IO.StreamReader($result) - $reader.BaseStream.Position = 0 - $reader.DiscardBufferedData() - $errormessage = $reader.ReadToEnd(); + elseif ($dumpLogs -eq "auditOnly"){ + "Processing audit logs only" | Write-Log -LogPath $logFile } - else { - $errormessage = $_.ErrorDetails.Message + "Processing sign in logs only" | Write-Log -LogPath $logFile } - if($errormessage -like "*RequestFromNonPremiumTenant*") - { - $P1Enabled = $false - "Azure AD P1 not enabled on tenant, enable it if you wish to retrieve signin logs via MSGraph but be aware of additional costs" | Write-Error - "Azure AD P1 not enabled on tenant, enable it if you wish to retrieve signin logs via MSGraph but be aware of additional costs" | Write-Log -LogPath $logfile -LogLevel "Error" - if ($StartDate -lt (get-date).adddays(-7)) { - "Azure AD P1 not enabled on tenant, reducing to logs from the last 7 days" | Write-Log -LogPath $logfile -LogLevel "Error" - $StartDate = [DateTime](get-date).adddays(-7).ToString("yyyy-MM-dd") - if ($EndDate -lt $StartDate) { - "Azure AD P1 not enabled on tenant, can't dump logs for that period" | Write-Log -LogPath $logfile -LogLevel "Error" - exit - } - } - } - elseif($errormessage -like "*RequestFromUnsupportedUserRole*") - { - "$user does not have the required permissions to get Azure AD Audit Logs : not in the 'Global Reader' group on https://portal.azure.com. Cannot continue" | Write-Error - "$user does not have the required permissions to get Azure AD Audit Logs : not in the 'Global Reader' group on https://portal.azure.com. Cannot continue" | Write-Log -LogPath $logfile -LogLevel "Error" + $maxStartDate = (Get-Date).AddDays(-30) + if ($startDate -lt $maxStartDate){ + Write-Warning "You can only get 30 days with Audit Log. Setting startDate to $maxStartDate" + "You can only get 30 days with Audit Log. Setting startDate to $maxStartDate" | Write-Log -LogPath $logFile -LogLevel "Warning" + $startDate = $maxStartDate + if ($endDate -lt $startDate){ + Write-Host "Incompatible endDate: $endDate. Exiting" + "Incompatible endDate: $endDate. Exiting" | Write-Log -LogPath $logFile exit } -} - -$totaltimespan = (New-TimeSpan -Start $StartDate -End $Enddate) -if(($totaltimespan.hours -eq 0) -and ($totaltimespan.minutes -eq 0) -and ($totaltimespan.seconds -eq 0)) - {$totaldays = $totaltimespan.days - $totalloops = $totaldays - } -else - {$totaldays = $totaltimespan.days + 1 - $totalloops = $totaltimespan.days } - $Launchsearch = + $launchSearch = { - Param($app, $user, $newstartdate, $newenddate ,$currentpath,$tenantsize,$Dumplogs,$P1Enabled) - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $logfile = $currentpath + "\AAD" + $datetoprocess + ".log" - $aadauditfolder = $currentpath + "\azure_ad_audit" - if ((Test-Path $aadauditfolder) -eq $false){New-Item $aadauditfolder -Type Directory} - "Processing AAD logs for day $($datetoprocess)"| Write-Log -LogPath $logfile - - #Get AAD Audit logs - if(($Dumplogs -eq "All") -or ($Dumplogs -eq "AuditOnly")) - { - $outputdate = "{0:yyyy-MM-dd}" -f ($newstartdate) - $tenant = ($user).split("@")[1] - $outputfile = $aadauditfolder + "\AADAuditLog_" + $tenant + "_" + $outputdate + ".json" - $Auditstart = "{0:s}" -f $newstartdate + "Z" - $Auditend = "{0:s}" -f $newenddate + "Z" - $uri = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=activityDateTime gt $($Auditstart) and activityDateTime lt $($Auditend)" - $AADAuditEvents = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uri -logfile $logfile -app $app -user $user - if($AADAuditEvents) - { - $AADAuditEvents | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 - $nbAADAuditEvents = ($AADAuditEvents | Measure-Object).count - "Dumping $($nbAADAuditEvents) AAD audit events to $($outputfile)" | Write-Log -LogPath $logfile + param($newStartDate, $newEndDate, $currentPath, $tenantSize, $dumpLogs, $P1Enabled, $cert, $appId, $tenant) + + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $logFile = $currentPath + "\AAD" + $dateToProcess + ".log" + + Connect-MicrosoftGraphApplication -certificate $cert -appId $appId -tenant $tenant -logFile $logFile + + # Get Entra ID audit logs + if (($dumpLogs -eq "all") -or ($dumpLogs -eq "auditOnly")){ + $AzureADAuditFolder = $currentPath + "\azure_ad_audit" + if ((Test-Path $AzureADAuditFolder) -eq $false){ + New-Item $AzureADAuditFolder -Type Directory } - else { - "No AAD audit event to dump to $($outputfile)" | Write-Log -LogPath $logfile -LogLevel "Warning" + + "Processing Entra ID audit logs for day $($dateToProcess)" | Write-Log -LogPath $logFile + $outputdate = "{0:yyyy-MM-dd}" -f ($newStartDate) + $outputFile = $AzureADAuditFolder + "\AADAuditLog_" + $tenant + "_" + $outputdate + ".json" + $auditStart = "{0:s}" -f $newStartDate + "Z" + $auditEnd = "{0:s}" -f $newEndDate + "Z" + $AzureADAuditEvents = Get-MicrosoftGraphLogs -type "AuditLogs" -dateStart $auditStart -dateEnd $auditEnd -certificate $cert -appId $appId -tenant $tenant -logFile $logFile + if ($AzureADAuditEvents){ + $nbAzureADAuditEvents = ($AzureADAuditEvents | Measure-Object).Count + "Dumping $($nbAzureADAuditEvents) Entra ID audit events to $($outputFile)" | Write-Log -LogPath $logFile + $AzureADAuditEvents | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 + } + else { + "No Entra ID audit event to dump to $($outputFile)" | Write-Log -LogPath $logFile -LogLevel "Warning" } } - #Get AAD Signin logs - if(($Dumplogs -eq "All") -or ($Dumplogs -eq "SigninOnly")) - { - if($P1Enabled -eq $true) - { - $totalhours = [Math]::Floor((New-TimeSpan -Start $newstartdate -End $newenddate).Totalhours) - if($totalhours -eq 24){$totalhours--} - For ($h=0; $h -le $totalhours ; $h++) - { - if($h -eq 0) - { - $newstarthour = $newstartdate - $newendhour = $newstartdate.AddMinutes(59 - $newstartdate.Minute).AddSeconds(60 - $newstartdate.Second) + + # Get Entra ID sign in logs + if (($dumpLogs -eq "all") -or ($dumpLogs -eq "signInsOnly")) + { + if ($P1Enabled -eq $true){ + $totalHours = [Math]::Floor((New-TimeSpan -Start $newStartDate -End $newEndDate).TotalHours) + if ($totalHours -eq 24){ + $totalHours-- + } + for ($h=0; $h -le $totalHours; $h++){ + if ($h -eq 0){ + $newStartHour = $newStartDate + $newEndHour = $newStartDate.AddMinutes(59 - $newStartDate.Minute).AddSeconds(60 - $newStartDate.Second) } - elseif($h -eq $totalhours) - { - $newstarthour = $newendhour - $newendhour = $newenddate + elseif ($h -eq $totalHours){ + $newStartHour = $newEndHour + $newEndHour = $newEndDate } - else { - $newstarthour = $newendhour - $newendhour = $newstarthour.addHours(1) + else { + $newStartHour = $newEndHour + $newEndHour = $newStartHour.addHours(1) } - "Processing signIns logs between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile - $outputdate = "{0:yyyy-MM-dd}_{0:HH-00-00}" -f ($newstarthour) - - - $aadsigninfolder = $currentpath + "\azure_ad_signin" - if ((Test-Path $aadsigninfolder) -eq $false){New-Item $aadsigninfolder -Type Directory} - - $Signinstart = "{0:s}" -f $newstarthour + "Z" - $Signinend = "{0:s}" -f $newendhour + "Z" - if($tenantsize -eq "normal") - { - $uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=createdDateTime gt $($Signinstart) and createdDateTime lt $($Signinend)" - } - else { - - $uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=createdDateTime gt $($Signinstart) and createdDateTime lt $($Signinend) and status/errorCode eq 0 and (appId eq '00000002-0000-0ff1-ce00-000000000000' or appId eq '1b730954-1685-4b74-9bfd-dac224a7b894' or appId eq 'a0c73c16-a7e3-4564-9a95-2bdf47383716' or appId eq '00000003-0000-0ff1-ce00-000000000000' or appId eq '6eb59a73-39b2-4c23-a70f-e2e3ce8965b1' or appId eq 'cb1056e2-e479-49de-ae31-7812af012ed8' or appId eq '1950a258-227b-4e31-a9cf-717495945fc2' or appId eq 'fb78d390-0c51-40cd-8e17-fdbfab77341b' or appId eq '04b07795-8ddb-461a-bbee-02f9e1bf7b46')" - } - $AADSigninEvents = Get-RestAPIResponse -RESTAPIService "MSGraph" -uri $uri -logfile $logfile -app $app -user $user - $foldertoprocess = $aadsigninfolder + "\" + $datetoprocess - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory} - $outputfile = $foldertoprocess + "\AADSigninLog_" + $tenant + "_" + $outputdate + ".json" - if($AADSigninEvents) - { - $nbADSigninEvents = ($AADSigninEvents | Measure-Object).count - "Dumping $($nbADSigninEvents) AAD signIns events to $($outputfile)" | Write-Log -LogPath $logfile - $AADSigninEvents | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 + "Processing sign in logs between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartHour, $newEndHour) | Write-Log -LogPath $logFile + $outputDate = "{0:yyyy-MM-dd}_{0:HH-00-00}" -f ($newStartHour) + + $AzureADSignInsFolder = $currentPath + "\azure_ad_signin" + if ((Test-Path $AzureADSignInsFolder) -eq $false){ + New-Item $AzureADSignInsFolder -Type Directory + } + + $signInsStart = "{0:s}" -f $newStartHour + "Z" + $signInsEnd = "{0:s}" -f $newEndHour + "Z" + $AzureADSignInEvents = Get-MicrosoftGraphLogs -type "SignIns" -tenantSize $tenantSize -dateStart $signInsStart -dateEnd $signInsEnd -certificate $cert -appId $appId -tenant $tenant -logFile $logFile + $folderToProcess = $AzureADSignInsFolder + "\" + $dateToProcess + if ((Test-Path $folderToProcess) -eq $false){ + New-Item $folderToProcess -Type Directory + } + $outputFile = $folderToProcess + "\AADSigninLog_" + $tenant + "_" + $outputdate + ".json" + if ($AzureADSignInEvents){ + $nbADSigninEvents = ($AzureADSignInEvents | Measure-Object).Count + "Dumping $($nbADSigninEvents) Entra ID sign in events to $($outputFile)" | Write-Log -LogPath $logFile + $AzureADSignInEvents | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 + } + else { + "No Entra ID sign in events to dump to $($outputFile)" | Write-Log -LogPath $logFile -LogLevel "Warning" } - else{"No AAD signIns events to dump to $($outputfile)" | Write-Log -LogPath $logfile -LogLevel "Warning"} } } + else { + "No Entra ID P1 licence: can't dump sign in logs using API between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartDate, $newEndDate) | Write-Log -LogPath $logFile -LogLevel "Warning" + } } } - For ($d=0; $d -le $totalloops ; $d++) - { - if($d -eq 0) - { - $newstartdate = $StartDate - $newenddate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newstartdate.AddDays(1))) - } - elseif($d -eq $totaldays) - { - $newenddate = $Enddate - $newstartdate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newenddate)) - } - else { - $newstartdate = $newenddate - $newenddate = $newenddate.AddDays(+1) - } - #Refresh token - $token = Get-OAuthToken -Service MSGraph -silent $true -LoginHint $user -Logfile $logfile - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1b730954-1685-4b74-9bfd-dac224a7b894"} - if($null -eq $app) - { - "No token cache available for MSGraph service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service MSGraph -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1b730954-1685-4b74-9bfd-dac224a7b894"} - } - "Lauching job number $($d) with startdate {0:yyyy-MM-dd} {0:HH:mm:ss} and enddate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $jobname = "AAD" + $datetoprocess - Start-RSJob -Name $jobname -ScriptBlock $Launchsearch -FunctionsToImport write-log, Get-RestAPIResponse -ArgumentList $app, $user, $newstartdate, $newenddate, $currentpath, $tenantsize, $Dumplogs, $P1Enabled - - $maxjobrunning = 3 - if($tenantsize -eq "huge"){ - $maxjobrunning = 1 + $cert, $null, $null = Import-Certificate -certificatePath $certificatePath -logFile $logFile + + Get-RSJob | Remove-RSJob -Force + + Connect-MicrosoftGraphApplication -certificate $cert -appId $appId -tenant $tenant -logFile $logFile + + $AzureADTenantFolder = $currentPath + "\azure_ad_tenant" + if ((Test-Path $AzureADTenantFolder) -eq $false){ + New-Item $AzureADTenantFolder -Type Directory | Out-Null } + $outputFile = $AzureADTenantFolder + "\AADTenant_" + $tenant + ".json" - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge $maxjobrunning) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } - $jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} - if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } + # Test the directory size + $tenantSize = "normal" + $tenantInformation = Get-MgOrganization -ErrorAction Stop + if ($tenantInformation.AdditionalProperties.directorySizeQuota.used -ge 100000){ + $tenantSize = "huge" + if ($dumpLogs -eq "all" -or $dumpLogs -eq "signInsOnly"){ + Write-Warning "Directory size is huge, processing might be long. As a consequence, sign in logs will be filtered on some specific applications" + "Directory size is huge, processing might be long. As a consequence, sign in logs will be filtered on some specific applications" | Write-Log -LogPath $logFile -LogLevel "Warning" } - } - #Waiting for final jobs to complete - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 1) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile + else { + Write-Warning "Directory size is huge, processing of audit logs might be long" + "Directory size is huge, processing of audit logs might be long" | Write-Log -LogPath $logFile -LogLevel "Warning" } + Write-Host "You might also want to dump sign in logs and audit logs separately by using the dumpLogs switch" + "You might also want to dump sign in logs and audit logs separately by using the dumpLogs switch" | Write-Log -LogPath $logFile } -$jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} -if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } + else { + "Tenant of a normal size, dumping all logs" | Write-Log -LogPath $logFile } -} - + "Dumping tenant information in azure_ad_tenant folder" | Write-Log -LogPath $logFile + $tenantInformation | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 + # Check if Microsoft Entra ID P1 is enabled + "Checking Microsoft Entra ID P1"| Write-Log -LogPath $logFile + $P1Enabled = $true + try { + $null = Get-MgBetaAuditLogSignIn -Top 1 -All -ErrorAction Stop + } + catch { + if ($_.ErrorDetails.Message -like "*RequestFromNonPremiumTenant*"){ + $P1Enabled = $false + Write-Warning "Entra ID P1 is not enabled tenant-wide. You should buy at least one Entra ID P1 licence to be able to retrieve the full audit log" + "Entra ID P1 is not enabled tenant-wide. You should buy at least one Entra ID P1 licence to be able to retrieve the full audit log" | Write-Log -LogPath $logFile -LogLevel "Warning" + $maxStartDate = (Get-Date).AddDays(-7) + if ($startDate -lt $maxStartDate){ + Write-Warning "Entra ID P1 not enabled tenant-wide, you can only get 7 days of Audit Log. Setting startDate to $maxStartDate" + "Entra ID P1 not enabled tenant-wide, you can only get 7 days of Audit Log. Setting startDate to $maxStartDate" | Write-Log -LogPath $logFile -LogLevel "Warning" + $startDate = [DateTime]$maxStartDate.ToString("yyyy-MM-dd") + if ($endDate -lt $startDate){ + Write-Error "Incompatible endDate: $endDate. Exiting" + "Incompatible endDate: $endDate. Exiting" | Write-Log -LogPath $logFile -LogLevel "Error" + exit + } + } + } + } + $totalTimeSpan = (New-TimeSpan -Start $startDate -End $endDate) + if (($totalTimeSpan.Hours -eq 0) -and ($totalTimeSpan.Minutes -eq 0) -and ($totalTimeSpan.Seconds -eq 0)){ + $totalDays = $totalTimeSpan.days + $totalLoops = $totalDays + } + else { + $totalDays = $totalTimeSpan.days + 1 + $totalLoops = $totalTimeSpan.days + } + for ($d=0; $d -le $totalLoops; $d++){ + if ($d -eq 0){ + $newStartDate = $startDate + $newEndDate = Get-Date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newStartDate.AddDays(1))) + } + elseif ($d -eq $totalDays){ + $newEndDate = $endDate + $newStartDate = Get-Date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newEndDate)) + } + else { + $newStartDate = $newEndDate + $newEndDate = $newEndDate.AddDays(1) + } + "Lauching job number $($d) with startDate {0:yyyy-MM-dd} {0:HH:mm:ss} and endDate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartDate, $newEndDate) | Write-Log -LogPath $logFile + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $jobName = "AAD" + $dateToProcess + Start-RSJob -Name $jobName -ScriptBlock $launchSearch -FunctionsToImport Write-Log, Connect-MicrosoftGraphApplication, Get-MicrosoftGraphLogs -ArgumentList $newStartDate, $newEndDate, $currentPath, $tenantSize, $dumpLogs, $P1Enabled, $cert, $appId, $tenant + $maxJobRunning = 3 + if ($tenantSize -eq "huge"){ + $maxJobRunning = 1 + } + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge $maxJobRunning){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } + } + # Waiting for final jobs to complete + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge 1){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } +} diff --git a/DFIR-O365RC/Get-AzDevOpsActivityLogs.ps1 b/DFIR-O365RC/Get-AzDevOpsActivityLogs.ps1 index b64d704..a83b2ff 100755 --- a/DFIR-O365RC/Get-AzDevOpsActivityLogs.ps1 +++ b/DFIR-O365RC/Get-AzDevOpsActivityLogs.ps1 @@ -1,271 +1,265 @@ -Function Get-AzDevOpsActivityLogs { +function Get-AzDevOpsActivityLogs { <# .SYNOPSIS The Get-AzDevOpsActivityLogs function dumps in JSON files Azure DevOps activity logs for a specific time range. .EXAMPLE - - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-30) - PS C:\>Get-AzDevOpsActivityLogs -startdate $startdate -enddate $enddate + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-90) - Dump all Azure DevOps activity logs available the user has access to - .EXAMPLE - - Get-AzDevOpsActivityLogs -startdate $startdate -enddate $enddate -SelectOrg:$true - Dump Azure DevOps activity logs for a given organization + PS C:\>Get-AzDevOpsActivityLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath + + Dump all Azure DevOps activity logs for the last 90 days. #> - + param ( [Parameter(Mandatory = $true)] - [DateTime]$Enddate, + [DateTime]$startDate, [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false)] - [boolean]$SelectOrg=$false, - [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, [Parameter(Mandatory = $false)] - [String]$logfile = "Get-AzDevOpsActivityLogs.log" + [String]$logFile = "Get-AzDevOpsActivityLogs.log" ) - $currentpath = (get-location).path - $logfile = $currentpath + "\" + $logfile - "Getting AzDevOps Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service AzDevOps -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName - - -$totaltimespan = (New-TimeSpan -Start $StartDate -End $Enddate) + $currentPath = (Get-Location).path -if(($totaltimespan.hours -eq 0) -and ($totaltimespan.minutes -eq 0) -and ($totaltimespan.seconds -eq 0)) - {$totaldays = $totaltimespan.days - $totalloops = $totaldays - } -else - {$totaldays = $totaltimespan.days + 1 - $totalloops = $totaltimespan.days - } - -Get-RSJob | Remove-RSJob -Force - -$tenant = ($user).split("@")[1] -$azdevorgfolder = $currentpath + "\azure_DevOps_orgs" -if ((Test-Path $azdevorgfolder) -eq $false){New-Item $azdevorgfolder -Type Directory | Out-Null} -$outputfile = $azdevorgfolder + "\AzdevopsOrgs_" + $tenant + ".json" + $cert, $needPassword, $certificateSecurePassword = Import-Certificate -certificatePath $certificatePath -logFile $logFile -$urime = "https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=6.0-preview.1" -$me = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Uri $urime -Method Get -ContentType "application/json" -ErrorAction Stop -$uriorgs = "https://app.vssps.visualstudio.com/_apis/accounts?memberId=$($me.id)&api-version=6.0-preview.1" -$azdevopsorgs = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Uri $uriorgs -Method Get -ContentType "application/json" -ErrorAction Stop + Connect-AzApplication -logFile $logFile -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + $tenantId = (Get-AzTenant).Id -$nbazdevopsorgs = ($azdevopsorgs.value | Measure-Object).count - -if($nbazdevopsorgs -eq 0) -{ - Write-Host "No Azure DevOps organization to process, exiting" - "The user has $($nbazdevopsorgs) Azure DevOps organization attached, exiting" | Write-Log -LogPath $logfile -Level "ERROR" - exit -} -else - { - Write-Host "The user has $($nbazdevopsorgs) Azure DevOps organization attached:" - "The user has $($nbazdevopsorgs) Azure DevOps organization attached" | Write-Log -LogPath $logfile - $azdevopsorgs.value | ForEach-Object{write-host "$($_.accountName) | $($_.accountId)"} + $azureDevOpsOrganizationsFolder = $currentPath + "\azure_DevOps_orgs" + if ((Test-Path $azureDevOpsOrganizationsFolder) -eq $false){ + New-Item $azureDevOpsOrganizationsFolder -Type Directory | Out-Null } -if($SelectOrg -eq $false) - { - Write-Host "Processing activity logs for all Azure DevOps organizations." - "Processing activity logs for all Azure DevOps organizations, dumping all organizations information to $($outputfile) " | Write-Log -LogPath $logfile - $orgidtoprocess = $azdevopsorgs.value + + $azureDevOpsOrganizationsRaw = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Method Get -ContentType "application/json" -ErrorAction Stop -Uri "https://aexprodweu1.vsaex.visualstudio.com/_apis/EnterpriseCatalog/Organizations?tenantId=$tenantId" + if ($azureDevOpsOrganizationsRaw.Contains("Azure DevOps Services | Sign In")){ + Write-Warning "Could not enumerate the organizations the application has access to (this is a known bug from Microsoft). Please enter the name of the subscriptions manually" + "Could not enumerate the organizations the application has access to (this is a known bug from Microsoft). Please enter the name of the subscriptions manually" | Write-Log -LogPath $logFile + [System.Collections.ArrayList]$wantedOrganizationsNameAndId = @{} + $read = $True + Write-Host "Leave Blank and press 'Enter' to Stop" + while ($read){ + $inputOrganizationName = Read-Host "Please enter the organization names, one by one, and press 'Enter'" + if ($inputOrganizationName){ + $selectedInput = @{ + "Organization Name" = $inputOrganizationName ; + "Organization Id" = "000000000000000000" + } + $wantedOrganizationsNameAndId.Add($selectedInput) | Out-Null + Write-Host "Added $inputOrganizationName" + } + else { + $read = $False + } + } } -else - { - Write-Host "Please enter a Azure DevOps organization ID:" - $orgid = read-host - $orgidtoprocess = $azdevopsorgs.value | Where-Object{$_.accountId -eq $orgid} - if($orgidtoprocess) - { - Write-Host "Processing activity logs only for $($orgidtoprocess.accountName) Azure DevOps organization." - "Processing activity logs only for $($orgidtoprocess.accountName) Azure DevOps organization, dumping all organizations information to $($outputfile) " | Write-Log -LogPath $logfile + else { + $outputFile = $azureDevOpsOrganizationsFolder + "\AzdevopsOrgs_" + $tenant + ".json" + $azureDevOpsOrganizationsRaw | ConvertFrom-CSV | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 + $azureDevOpsOrganizationsNameAndId = $azureDevOpsOrganizationsRaw | ConvertFrom-CSV | ForEach-Object {$_ | Select-Object "Organization Name", "Organization Id"} + Write-Host "The following organizations are accessible in your Entra ID tenant:" + "The following organizations are accessible in your Entra ID tenant:" | Write-Log -LogPath $logFile + $azureDevOpsOrganizationsNameAndId | Out-Host + $azureDevOpsOrganizationsNameAndId | Write-Log -LogPath $logFile + $choice = Read-Host "Do you want to collect Azure DevOps activity logs for all [a], specific [s] or no [N] organizations ? [a/s/N]" + if ($choice.ToUpper() -eq "S"){ + [System.Collections.ArrayList]$wantedOrganizationsNameAndId = @{} + $read = $True + Write-Host "Leave Blank and press 'Enter' to Stop" + while ($read){ + $potentialOrganizationId = Read-Host "Please enter the organization IDs, one by one, and press 'Enter'" + if ($potentialOrganizationId){ + $selectedInput = $azureDevOpsOrganizationsNameAndId | Where-Object {$_."Organization Id" -eq $potentialOrganizationId} + if ($null -ne $selectedInput){ + $wantedOrganizationsNameAndId.Add($selectedInput) | Out-Null + Write-Host "Added $potentialOrganizationId" + } + else { + Write-Warning "Invalid organization ID, please try again" + } + } + else { + $read = $False + } } - else{ - Write-Host "Azure DevOps organization ID is incorrect, exiting" - "Azure DevOps organization ID is incorrect, exiting" | Write-Log -LogPath $logfile -Level "ERROR" + } + elseif ($choice.ToUpper() -eq "A"){ + $wantedOrganizationsNameAndId = $azureDevOpsOrganizationsNameAndId + } + else { + Write-Error "No organization was selected. Exiting" + "No organization was selected. Exiting" | Write-Log -LogPath $logFile -LogLevel Error exit } } -$azdevopsorgs.value | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 + $launchSearch = + { + param($newStartDate, $newEndDate, $currentPath, $organizationName, $appId, $tenant, $certificatePath, [SecureString]$certificateSecurePassword, [Bool]$needPassword) + $dateToProcess = ($newStartdate.ToString("yyyy-MM-dd")) + $logFile = $currentPath + "\AzDevOps_" + $organizationName + "_" + $dateToProcess + ".log" - $Launchsearch = - { - Param($app, $user, $newstartdate, $newenddate ,$currentpath,$orgname) - - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $logfile = $currentpath + "\AzDevOps_" + $orgname + "_" + $datetoprocess + ".log" - $tenant = ($user).split("@")[1] + $azureDevOpsActivityFolder = $currentPath + "\azure_DevOps_activity" + if ((Test-Path $azureDevOpsActivityFolder) -eq $false){ + New-Item $azureDevOpsActivityFolder -Type Directory + } - $azDevOpsActivityfolder = $currentpath + "\azure_DevOps_activity" - if ((Test-Path $azDevOpsActivityfolder) -eq $false){New-Item $azDevOpsActivityfolder -Type Directory} - - $totalhours = [Math]::Floor((New-TimeSpan -Start $newstartdate -End $newenddate).Totalhours) - if($totalhours -eq 24){$totalhours--} - - For ($h=0; $h -le $totalhours ; $h++) - { - if($h -eq 0) - { - $newstarthour = $newstartdate - $newendhour = $newstartdate.AddMinutes(59 - $newstartdate.Minute).AddSeconds(60 - $newstartdate.Second) + $totalHours = [Math]::Floor((New-TimeSpan -Start $newStartDate -End $newEndDate).TotalHours) + if ($totalHours -eq 24){ + $totalHours-- + } + for ($h=0; $h -le $totalHours; $h++){ + if ($h -eq 0){ + $newStartHour = $newStartDate + $newEndHour = $newStartDate.AddMinutes(59 - $newStartDate.Minute).AddSeconds(60 - $newStartDate.Second) } - elseif($h -eq $totalhours) - { - $newstarthour = $newendhour - $newendhour = $newenddate + elseif ($h -eq $totalHours){ + $newStartHour = $newEndHour + $newEndHour = $newEndDate } - else { - $newstarthour = $newendhour - $newendhour = $newstarthour.addHours(1) + else { + $newStartHour = $newEndHour + $newEndHour = $newStartHour.addHours(1) } - "Processing Azure DevOps activity logs between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile + "Processing Azure DevOps activity logs between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartHour, $newEndHour) | Write-Log -LogPath $logFile - $outputdate = "{0:yyyy-MM-dd}_{0:HH-00-00}" -f ($newstarthour) - $Auditstart = "{0:s}" -f $newstarthour + "Z" - $Auditend = "{0:s}" -f $newendhour + "Z" + $outputDate = "{0:yyyy-MM-dd}_{0:HH-00-00}" -f ($newStartHour) - $uri = "https://auditservice.dev.azure.com/$($orgname)/_apis/audit/auditlog?startTime=$($Auditstart)&endTime=$($Auditend)&api-version=6.0-preview.1" - $AzDevOpsactivityEvents = Get-RestAPIResponse -RESTAPIService "AzDevOps" -uri $uri -logfile $logfile -app $app -user $user - $foldertoprocess = $azDevOpsActivityfolder + "\" + $datetoprocess - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory} - $outputfile = $foldertoprocess + "\AzDevOps_" + $tenant + "_" + $orgname + "_" + $outputdate + ".json" - if($AzDevOpsactivityEvents) - { - $nbAzDevOpsactivityEvents = ($AzDevOpsactivityEvents | Measure-Object).count - "Dumping $($nbAzDevOpsactivityEvents) Azure DevOps activity logs events to $($outputfile)" | Write-Log -LogPath $logfile - $AzDevOpsactivityEvents | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 - } - else { - "No Azure DevOps activity logs event to dump to $($outputfile)" | Write-Log -LogPath $logfile -LogLevel "Warning" - } - } - } + $auditStart = "{0:s}" -f $newStartHour + "Z" + $auditEnd = "{0:s}" -f $newEndhour + "Z" + $uri = "https://auditservice.dev.azure.com/$($organizationName)/_apis/audit/auditlog?startTime=$($auditStart)&endTime=$($auditEnd)&api-version=7.1-preview.1" -foreach($org in $orgidtoprocess) -{ - Write-Host "Starting processing activity logs for $($org.accountName) Azure DevOps organization." - "Starting processing activity logs for $($org.accountName) Azure DevOps organization." | Write-Log -LogPath $logfile + $azureDevOpsActivityEvents = Get-AzDevOpsAuditLogs -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId -uri $uri -logFile $logFile - For ($d=0; $d -le $totalloops ; $d++) - { - if($d -eq 0) - { - $newstartdate = $StartDate - $newenddate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newstartdate.AddDays(1))) - } - elseif($d -eq $totaldays) - { - $newenddate = $Enddate - $newstartdate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newenddate)) - } - else { - $newstartdate = $newenddate - $newenddate = $newenddate.AddDays(+1) + $folderToProcess = $azureDevOpsActivityFolder + "\" + $dateToProcess + if ((Test-Path $folderToProcess) -eq $false){ + New-Item $folderToProcess -Type Directory } - #Refresh token - $token = Get-OAuthToken -Service AzDevOps -silent $true -LoginHint $user -Logfile $logfile - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1950a258-227b-4e31-a9cf-717495945fc2"} - if($null -eq $app) - { - "No token cache available for AzDevOps service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service AzDevOps -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1950a258-227b-4e31-a9cf-717495945fc2"} - } - "Lauching job number $($d) with startdate {0:yyyy-MM-dd} {0:HH:mm:ss} and enddate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $orgname = $org.accountName - $jobname = "AzDevOps_" + $orgname + "_" + $datetoprocess - Start-RSJob -Name $jobname -ScriptBlock $Launchsearch -FunctionsToImport write-log, Get-RestAPIResponse -ArgumentList $app, $user, $newstartdate, $newenddate, $currentpath, $orgname - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 3) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count + $outputFile = $folderToProcess + "\AzDevOps_" + $tenant + "_" + $organizationName + "_" + $outputDate + ".json" + if ($azureDevOpsActivityEvents){ + $nbAzureDevOpsActivityEvents = ($azureDevOpsActivityEvents | Measure-Object).Count + "Dumping $($nbAzureDevOpsActivityEvents) Azure DevOps activity logs events to $($outputFile)" | Write-Log -LogPath $logFile + $azureDevOpsActivityEvents | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } - $jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} - if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" + else { + "No Azure DevOps activity logs event to dump to $($outputFile)" | Write-Log -LogPath $logFile -LogLevel "Warning" } } } - #Waiting for final jobs to complete - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 1) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } + + $totalTimeSpan = (New-TimeSpan -Start $startDate -End $endDate) + + if (($totalTimeSpan.Hours -eq 0) -and ($totalTimeSpan.Minutes -eq 0) -and ($totalTimeSpan.Seconds -eq 0)){ + $totaldays = $totalTimeSpan.days + $totalLoops = $totaldays } -$jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} -if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } + else { + $totaldays = $totalTimeSpan.days + 1 + $totalLoops = $totalTimeSpan.days } -} -} - + Get-RSJob | Remove-RSJob -Force + foreach ($organization in $wantedOrganizationsNameAndId){ + Write-Host "Starting processing Azure DevOps activity logs for $($organization.'Organization Name') ($($organization.'Organization Id')) Azure DevOps organization" + "Starting processing Azure DevOps activity logs for $($organization.'Organization Name') ($($organization.'Organization Id')) Azure DevOps organization" | Write-Log -LogPath $logFile + for ($d=0; $d -le $totalLoops; $d++){ + if ($d -eq 0){ + $newStartDate = $startDate + $newEndDate = Get-Date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newStartDate.AddDays(1))) + } + elseif ($d -eq $totaldays){ + $newEndDate = $endDate + $newStartDate = Get-Date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newEndDate)) + } + else { + $newStartDate = $newEndDate + $newEndDate = $newEndDate.AddDays(1) + } + "Lauching job number $($d) with startDate {0:yyyy-MM-dd} {0:HH:mm:ss} and endDate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartDate, $newEndDate) | Write-Log -LogPath $logFile + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $organizationName = $organization.'Organization Name' + $jobName = "AzDevOps_" + $organizationName + "_" + $dateToProcess + Start-RSJob -Name $jobName -ScriptBlock $Launchsearch -FunctionsToImport Write-Log, Get-AzDevOpsAuditLogs -ArgumentList $newStartDate, $newEndDate, $currentPath, $organizationName, $appId, $tenant, $certificatePath, $certificateSecurePassword, $needPassword + $maxJobRunning = 3 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge $maxJobRunning){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } + } + # Waiting for final jobs to complete + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge 1){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } + } +} \ No newline at end of file diff --git a/DFIR-O365RC/Get-AzRMActivityLogs.ps1 b/DFIR-O365RC/Get-AzRMActivityLogs.ps1 index f85794c..2b1611a 100755 --- a/DFIR-O365RC/Get-AzRMActivityLogs.ps1 +++ b/DFIR-O365RC/Get-AzRMActivityLogs.ps1 @@ -1,268 +1,247 @@ -Function Get-AzRMActivityLogs { +function Get-AzRMActivityLogs { <# .SYNOPSIS - The Get-AzRMActivityLogs function dumps in JSON files Azure activity logs for a specific time range. + The Get-AzRMActivityLogs function dumps in JSON files Azure Resource Manager activity logs for a specific time range. .EXAMPLE - - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-30) - PS C:\>Get-AzRMActivityLogs -startdate $startdate -enddate $enddate + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-90) - Dump all Azure activity logs available for the tenant - .EXAMPLE - - Get-AzRMActivityLogs -startdate $startdate -enddate $enddate -SelectSubscription:$true - Dump Azure activity logs for a given subscription in the tenant + PS C:\>Get-AzRMActivityLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath + + Dump all Azure Resource Manager activity logs for the last 90 days. #> - + param ( [Parameter(Mandatory = $true)] - [DateTime]$Enddate, + [DateTime]$startDate, [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false)] - [boolean]$SelectSubscription=$false, - [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, [Parameter(Mandatory = $false)] - [String]$logfile = "Get-AzRMActivityLogs.log" + [String]$logFile = "Get-AzRMActivityLogs.log" ) - $currentpath = (get-location).path - $logfile = $currentpath + "\" + $logfile - "Getting AzRM Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service AzRM -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName + $currentPath = (Get-Location).path - -$totaltimespan = (New-TimeSpan -Start $StartDate -End $Enddate) + $null, $needPassword, $certificateSecurePassword = Import-Certificate -certificatePath $certificatePath -logFile $logFile -if(($totaltimespan.hours -eq 0) -and ($totaltimespan.minutes -eq 0) -and ($totaltimespan.seconds -eq 0)) - {$totaldays = $totaltimespan.days - $totalloops = $totaldays - } -else - {$totaldays = $totaltimespan.days + 1 - $totalloops = $totaltimespan.days - } + Connect-AzApplication -logFile $logFile -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId -Get-RSJob | Remove-RSJob -Force + $azureSubscriptionsFolder = $currentPath + "\azure_rm_subscriptions" -$tenant = ($user).split("@")[1] -$azsubscriptionsfolder = $currentpath + "\azure_rm_subscriptions" -if ((Test-Path $azsubscriptionsfolder) -eq $false){New-Item $azsubscriptionsfolder -Type Directory | Out-Null} -$outputfile = $azsubscriptionsfolder + "\AzRMsubscriptions_" + $tenant + ".json" -$uri = "https://management.azure.com/Subscriptions?api-version=2016-06-01" -$azsubscriptionsinfo = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.AccessToken)"} -Uri $Uri -Method Get -ContentType "application/json" -ErrorAction Stop - -$nbsubscriptions = ($azsubscriptionsinfo.value | Measure-Object).count -if($nbsubscriptions -eq 0) -{ - Write-Host "No Azure subscription to process, exiting" - "The tenant has $($nbsubscriptions) subscription, exiting" | Write-Log -LogPath $logfile -Level "ERROR" - exit -} -else - { - Write-Host "The tenant has $($nbsubscriptions) subscriptions:" - "The tenant has $($nbsubscriptions) subscriptions" | Write-Log -LogPath $logfile - $azsubscriptionsinfo.value | ForEach-Object{write-host "$($_.displayName) | $($_.subscriptionId)"} + if ((Test-Path $azureSubscriptionsFolder) -eq $false){ + New-Item $azureSubscriptionsFolder -Type Directory | Out-Null } -if($SelectSubscription -eq $false) - { - Write-Host "Processing activity logs for all subscriptions." - "Processing activity logs for all subscriptions, dumping all subscriptions information to $($outputfile) " | Write-Log -LogPath $logfile - $subidtoprocess = $azsubscriptionsinfo.value - } -else - { - Write-Host "Please enter a subscription ID:" - $subid = read-host - $subidtoprocess = $azsubscriptionsinfo.value | Where-Object{$_.subscriptionId -eq $subid} - if($subidtoprocess) - { - Write-Host "Processing activity logs only for $($subidtoprocess.displayName) subscription." - "Processing activity logs only for $($subidtoprocess.displayName) subscription, dumping all subscriptions information to $($outputfile) " | Write-Log -LogPath $logfile + + $subscriptionsRaw = Get-AzSubscription -ErrorAction Stop + $subscriptionsNameAndId = $subscriptionsRaw | Select-Object Name, Id + Write-Host "This application has access to the following subscriptions:" + "This application has access to the following subscriptions:" | Write-Log -LogPath $logFile + $subscriptionsNameAndId | Out-Host + $subscriptionsNameAndId | Write-Log -LogPath $logFile + $choice = Read-Host "Do you want to collect Azure Resource Manager activity logs for all [a], specific [s] or no [N] subscription ? [a/s/N]" + if ($choice.ToUpper() -eq "S"){ + [System.Collections.ArrayList]$wantedSubscriptionsNameAndId = @{} + $read = $True + Write-Host "Leave Blank and press 'Enter' to Stop" + while ($read){ + $potentialSubscriptionId = Read-Host "Please enter the subscription IDs, one by one, and press 'Enter'" + if ($potentialSubscriptionId){ + $selectedInput = $subscriptionsNameAndId | Where-Object {$_.Id -eq $potentialSubscriptionId} + if ($null -ne $selectedInput){ + $wantedSubscriptionsNameAndId.Add($selectedInput) | Out-Null + Write-Host "Added $potentialSubscriptionId" + } + else { + Write-Warning "Invalid subscription ID, please try again" + } + } + else { + $read = $False } - else{ - Write-Host "Subscription ID is incorrect, exiting" - "Subscription ID is incorrect, exiting" | Write-Log -LogPath $logfile -Level "ERROR" - exit } } + elseif ($choice.ToUpper() -eq "A"){ + $wantedSubscriptionsNameAndId = $subscriptionsNameAndId + } + else { + Write-Error "No subscription was selected. Exiting" + "No subscription was selected. Exiting" | Write-Log -LogPath $logFile -LogLevel Error + exit + } -$azsubscriptionsinfo.value | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 - + $outputFile = $azureSubscriptionsFolder + "\AzRMsubscriptions_" + $tenant + ".json" + $subscriptionsRaw | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 - $Launchsearch = + $launchSearch = { - Param($app, $user, $newstartdate, $newenddate ,$currentpath,$subscriptionID) - - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $logfile = $currentpath + "\AzRM_" + $subscriptionID + "_" + $datetoprocess + ".log" - $tenant = ($user).split("@")[1] - - $azRMActivityfolder = $currentpath + "\azure_rm_activity" - if ((Test-Path $azRMActivityfolder) -eq $false){New-Item $azRMActivityfolder -Type Directory} - - $totalhours = [Math]::Floor((New-TimeSpan -Start $newstartdate -End $newenddate).Totalhours) - if($totalhours -eq 24){$totalhours--} - - For ($h=0; $h -le $totalhours ; $h++) - { - if($h -eq 0) - { - $newstarthour = $newstartdate - $newendhour = $newstartdate.AddMinutes(59 - $newstartdate.Minute).AddSeconds(60 - $newstartdate.Second) - } - elseif($h -eq $totalhours) - { - $newstarthour = $newendhour - $newendhour = $newenddate - } - else { - $newstarthour = $newendhour - $newendhour = $newstarthour.addHours(1) - } - "Processing Azure activity logs between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile + param($newStartDate, $newEndDate, $currentPath, $subscriptionId, $appId, $tenant, $certificatePath, [SecureString]$certificateSecurePassword, [Bool]$needPassword) - $outputdate = "{0:yyyy-MM-dd}_{0:HH-00-00}" -f ($newstarthour) - $Auditstart = "{0:s}" -f $newstarthour + "Z" - $Auditend = "{0:s}" -f $newendhour + "Z" + Select-AzSubscription -SubscriptionID $subscriptionId -ErrorAction Stop + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $logFile = $currentPath + "\AzRM_" + $subscriptionId + "_" + $dateToProcess + ".log" + $tenant = ($user).split("@")[1] - $uri = "https://management.azure.com/subscriptions/$($subscriptionID)/providers/microsoft.insights/eventtypes/management/values?api-version=2015-04-01&`$filter=eventTimestamp ge $($Auditstart) and eventTimestamp le $($AuditEnd)" - $AzRMactivityEvents = Get-RestAPIResponse -RESTAPIService "AzRM" -uri $uri -logfile $logfile -app $app -user $user - $foldertoprocess = $azRMActivityfolder + "\" + $datetoprocess - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory} - $outputfile = $foldertoprocess + "\AzRM_" + $tenant + "_" + $subscriptionID + "_" + $outputdate + ".json" - if($AzRMactivityEvents) - { - $nbAzRMactivityEvents = ($AzRMactivityEvents | Measure-Object).count - "Dumping $($nbAzRMactivityEvents) Azure activity logs events to $($outputfile)" | Write-Log -LogPath $logfile - $AzRMactivityEvents | ConvertTo-Json -Depth 99 | out-file $outputfile -encoding UTF8 - } - else { - "No Azure activity logs event to dump to $($outputfile)" | Write-Log -LogPath $logfile -LogLevel "Warning" - } + $azureRMActivityFolder = $currentPath + "\azure_rm_activity" + if ((Test-Path $azureRMActivityFolder) -eq $false){ + New-Item $azureRMActivityFolder -Type Directory } - } -foreach($sub in $subidtoprocess) -{ - Write-Host "Starting processing activity logs for $($sub.displayName) subscription." - "Starting processing activity logs for $($sub.displayName) subscription." | Write-Log -LogPath $logfile - - For ($d=0; $d -le $totalloops ; $d++) - { - if($d -eq 0) - { - $newstartdate = $StartDate - $newenddate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newstartdate.AddDays(1))) + $totalHours = [Math]::Floor((New-TimeSpan -Start $newStartDate -End $newEndDate).TotalHours) + if ($totalHours -eq 24){ + $totalHours-- + } + for ($h=0; $h -le $totalHours; $h++){ + if ($h -eq 0){ + $newStartHour = $newStartDate + $newEndHour = $newStartDate.AddMinutes(59 - $newStartDate.Minute).AddSeconds(60 - $newStartDate.Second) } - elseif($d -eq $totaldays) - { - $newenddate = $Enddate - $newstartdate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newenddate)) + elseif ($h -eq $totalHours){ + $newStartHour = $newEndHour + $newEndHour = $newEndDate } - else { - $newstartdate = $newenddate - $newenddate = $newenddate.AddDays(+1) + else { + $newStartHour = $newEndHour + $newEndHour = $newStartHour.addHours(1) } - #Refresh token - $token = Get-OAuthToken -Service AzRM -silent $true -LoginHint $user -Logfile $logfile - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1950a258-227b-4e31-a9cf-717495945fc2"} - if($null -eq $app) - { - "No token cache available for AzRM service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service AzRM -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "1950a258-227b-4e31-a9cf-717495945fc2"} - } - "Lauching job number $($d) with startdate {0:yyyy-MM-dd} {0:HH:mm:ss} and enddate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $subscriptionID = $sub.subscriptionId - $jobname = "AzRM_" + $subscriptionID + "_" + $datetoprocess - Start-RSJob -Name $jobname -ScriptBlock $Launchsearch -FunctionsToImport write-log, Get-RestAPIResponse -ArgumentList $app, $user, $newstartdate, $newenddate, $currentpath, $subscriptionID - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 3) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count + "Processing Azure Resource Manager activity logs between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartHour, $newEndHour) | Write-Log -LogPath $logFile + + $outputDate = "{0:yyyy-MM-dd}_{0:HH-00-00}" -f ($newStartHour) + $dateStart = "{0:s}" -f $newStartHour + "Z" + $dateEnd = "{0:s}" -f $newEndHour + "Z" + + $azureRMActivityEvents = Get-AzureRMActivityLog -dateStart $dateStart -dateEnd $dateEnd -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -appId $appId -tenant $tenant -logFile $logFile + + $folderToProcess = $azureRMActivityFolder + "\" + $dateToProcess + if ((Test-Path $folderToProcess) -eq $false){ + New-Item $folderToProcess -Type Directory } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile + $outputFile = $folderToProcess + "\AzRM_" + $tenant + "_" + $subscriptionId + "_" + $outputDate + ".json" + if ($azureRMActivityEvents){ + $nbAzureRMActivityEvents = ($azureRMActivityEvents | Measure-Object).Count + "Dumping $($nbAzureRMActivityEvents) Azure Resource Manager activity logs events to $($outputFile)" | Write-Log -LogPath $logFile + for ($i=0; $i -lt $nbAzureRMActivityEvents; $i++){ + # we can't use ConvertTo-Json, cf. https://github.com/Azure/azure-powershell/issues/11353 + $azureRMActivityEvents[$i] = [Newtonsoft.Json.JsonConvert]::SerializeObject($azureRMActivityEvents[$i]) + } + $azureRMActivityEvents | Out-File $outputFile -Encoding UTF8 } - } - $jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} - if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" + else { + "No Azure Resource Manager activity logs event to dump to $($outputFile)" | Write-Log -LogPath $logFile -LogLevel "Warning" } } } - #Waiting for final jobs to complete - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 1) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } + + $totalTimeSpan = (New-TimeSpan -Start $startDate -End $endDate) + + if (($totalTimeSpan.Hours -eq 0) -and ($totalTimeSpan.Minutes -eq 0) -and ($totalTimeSpan.Seconds -eq 0)){ + $totaldays = $totalTimeSpan.days + $totalLoops = $totaldays } -$jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} -if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } + else { + $totaldays = $totalTimeSpan.days + 1 + $totalLoops = $totalTimeSpan.days } -} -} - + Get-RSJob | Remove-RSJob -Force + foreach ($subscription in $wantedSubscriptionsNameAndId){ + Write-Host "Starting processing Azure Resource Manager activity logs for $($subscription.Name) subscription" + "Starting processing Azure Resource Manager activity logs for $($subscription.Name) subscription" | Write-Log -LogPath $logFile + for ($d=0; $d -le $totalLoops; $d++){ + if ($d -eq 0){ + $newStartDate = $startDate + $newEndDate = Get-Date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newStartDate.AddDays(1))) + } + elseif ($d -eq $totaldays){ + $newEndDate = $endDate + $newStartDate = Get-Date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newEndDate)) + } + else { + $newStartDate = $newEndDate + $newEndDate = $newEndDate.AddDays(1) + } + "Lauching job number $($d) with startDate {0:yyyy-MM-dd} {0:HH:mm:ss} and endDate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartDate, $newEndDate) | Write-Log -LogPath $logFile + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $subscriptionId = $subscription.Id + $jobName = "AzRM_" + $subscriptionId + "_" + $dateToProcess + Start-RSJob -Name $jobName -ScriptBlock $launchSearch -FunctionsToImport Write-Log, Connect-AzApplication, Get-AzureRMActivityLog -ArgumentList $newStartDate, $newEndDate, $currentPath, $subscriptionId, $appId, $tenant, $certificatePath, $certificateSecurePassword, $needPassword + $maxJobRunning = 3 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge $maxJobRunning){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } + } + # Waiting for final jobs to complete + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge 1){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } + } +} \ No newline at end of file diff --git a/DFIR-O365RC/Get-DefenderforO365.ps1 b/DFIR-O365RC/Get-DefenderforO365.ps1 deleted file mode 100644 index 2832b90..0000000 --- a/DFIR-O365RC/Get-DefenderforO365.ps1 +++ /dev/null @@ -1,242 +0,0 @@ -Function Get-DefenderforO365 { - - <# - .SYNOPSIS - The Get-DefenderforO365 function dumps in JSON files all the Microsoft Defender for O365 logs from Unified Audit Logs for a specific time range. You need Microsoft Defender for Office 365 Plan 1 or 2, or Office 365 E5 to retrieve such logs. - - .EXAMPLE - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-90) - - PS C:\>Get-DefenderforO365 -startdate $startdate -enddate $enddate - - Dump all Microsoft Defender for O365 logs - #> - - param ( - - [Parameter(Mandatory = $true)] - [DateTime]$Enddate, - [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false, ParameterSetName="byrecordtype")] - [System.Array]$RecordTypes = ("ThreatIntelligence", "ThreatFinder","ThreatIntelligenceUrl","ThreatIntelligenceAtpContent","Campaign","AirInvestigation","WDATPAlerts","AirManualInvestigation","AirAdminActionInvestigation","MSTIC","MCASAlerts"), - [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, - [Parameter(Mandatory = $false)] - [String]$logfile = "Get-DefenderforO365.log" - ) - Write-Host "You need Microsoft Defender for Office 365 Plan 1 or 2, or Office 365 E5 to retrieve such logs" -ForegroundColor "Yellow" -BackgroundColor "Black" - write-host "Continue? (Y/N) " - $response = read-host - if ( $response -ne "Y" ) { exit } - - "Getting EXO Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName - $currentpath = (get-location).path - $o365existing = Get-PSSession | where-object{$_.ComputerName -eq "outlook.office365.com"} - if($o365existing){ - "Detected existing EXO session - removing and sleeping for session tear down" | Write-Log -LogPath $logfile -LogLevel "Warning" - $o365existing | remove-pssession -confirm:$false - start-sleep -seconds 15 - } - - - - $Launchsearch = - { - Param($app, $user, $newstartdate, $newenddate, $RecordTypes,$currentpath) - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $logfile = $currentpath + "\UnifiedAudit" + $datetoprocess + ".log" - $unifiedauditfolder = $currentpath + "\O365_unified_audit_logs" - if ((Test-Path $unifiedauditfolder) -eq $false){New-Item $unifiedauditfolder -Type Directory} - "Processing O365 logs for day $($datetoprocess)"| Write-Log -LogPath $logfile - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - $tenant = ($token.Account.UserName).split("@")[1] - $outputdate = "{0:yyyy-MM-dd}" -f ($datetoprocess) - $foldertoprocess = $unifiedauditfolder + "\" + $datetoprocess - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory} - - $outputfile = $foldertoprocess + "\UnifiedAuditLog_" + $tenant + "_" + $outputdate + "_DefenderforO365.json" - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile - - foreach($RecordType in $RecordTypes) - { - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - "Refreshing token - valid till " + $token.ExpiresOn.LocalDateTime.Tostring() | Write-Log -LogPath $logfile - try { - $trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -RecordType $RecordType -ResultSize 1 -ErrorAction Stop - } - catch { - "Retrieving Unified audit logs failed, rebuilding EXO session " | Write-Log -LogPath $logfile -LogLevel "Warning" - Get-PSSession | Remove-PSSession -Confirm:$false - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - Start-Sleep -Seconds 15 - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile - $trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -RecordType $RecordType -ResultSize 1 - } - if($trysearch) - { - $countobjects = $trysearch.ResultCount - "Dumping $($countobjects) $($RecordType) records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - if($countobjects -gt 50000) - { - "More than 50000 $($RecordType) records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} - some records might be missing" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile -LogLevel "Warning" - } - $sessionName = [guid]::NewGuid().ToString() - Get-LargeUnifiedAuditLog -sessionName $sessionName -StartDate $newstartdate -EndDate $newenddate -RecordType $recordtype -outputfile $outputfile -logfile $logfile -requesttype "Records" - } - } - "Removing PSSession and sleeping 10 seconds for session tear down" | Write-Log -LogPath $logfile - Get-PSSession | Remove-PSSession -Confirm:$false - Start-Sleep -Seconds 10 - } - - -$totaltimespan = (New-TimeSpan -Start $StartDate -End $Enddate) - -if(($totaltimespan.hours -eq 0) -and ($totaltimespan.minutes -eq 0) -and ($totaltimespan.seconds -eq 0)) - {$totaldays = $totaltimespan.days - $totalloops = $totaldays - } -else - {$totaldays = $totaltimespan.days + 1 - $totalloops = $totaltimespan.days - } - - -Get-RSJob | Remove-RSJob -Force - -$token = Get-OAuthToken -Service EXO -silent $true -LoginHint $user -Logfile $logfile -$app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} -if($null -eq $app) -{ - "No token cache available for EXO service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} -} - -"Checking permissions for $($user)"| Write-Log -LogPath $logfile -$sessionName = "EXO_" + [guid]::NewGuid().ToString() -$void = Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile -try { - $trysearch = Search-UnifiedAuditLog -StartDate (get-date).adddays(-90) -EndDate (get-date) -RecordType $recordtype -ResultSize 1 - -} -catch { - $errormessage = $_.Exception.Message - if ($errormessage -like "*The term 'Search-UnifiedAuditLog'*") { - "$user does not have the required permissions to get Office 365 Unified Audit Logs : doees not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. See https://learn.microsoft.com/en-us/purview/audit-log-search?view=o365-worldwide#before-you-search-the-audit-log. Cannot continue" | Write-Error - "$user does not have the required permissions to get Office 365 Unified Audit Logs : doees not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. See https://learn.microsoft.com/en-us/purview/audit-log-search?view=o365-worldwide#before-you-search-the-audit-log. Cannot continue" | Write-Log -LogPath $logfile -LogLevel "Error" - exit - } -} - -For ($d=0; $d -le $totalloops ; $d++) -{ - if($d -eq 0) - { - $newstartdate = $StartDate - $newenddate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newstartdate.AddDays(1))) - } - elseif($d -eq $totaldays) - { - $newenddate = $Enddate - $newstartdate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newenddate)) - } - else { - $newstartdate = $newenddate - $newenddate = $newenddate.AddDays(+1) - } - - $token = Get-OAuthToken -Service EXO -silent $true -LoginHint $user -Logfile $logfile - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - if($null -eq $app) - { - "No token cache available for EXO service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - } - "Lauching job number $($d) with startdate {0:yyyy-MM-dd} {0:HH:mm:ss} and enddate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $jobname = "UnifiedAudit" + $datetoprocess - - Start-RSJob -Name $jobname -ScriptBlock $Launchsearch -FunctionsToImport Connect-EXOPsearchUnified, write-log, Get-LargeUnifiedAuditLog -ArgumentList $app, $user, $newstartdate, $newenddate, $RecordTypes , $currentpath - - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 3) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } - $jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} - if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } - } - - - } - - #Waiting for final jobs to complete - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 1) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } -$jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} -if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } - } - -} \ No newline at end of file diff --git a/DFIR-O365RC/Get-O365.ps1 b/DFIR-O365RC/Get-O365.ps1 new file mode 100755 index 0000000..9036f16 --- /dev/null +++ b/DFIR-O365RC/Get-O365.ps1 @@ -0,0 +1,848 @@ +function Get-O365Purview { + + <# + .SYNOPSIS + The Get-O365Purview function is the inner function that handles the different jobs and calling of Get-UnifiedAuditLogPurview functions + #> + + param ( + [Parameter(Mandatory = $true)] + [ValidateSet("Unfiltered","Operations","RecordTypes","FreeText","IPAddresses","UserIds")] + [String]$requestType, + [Parameter(Mandatory = $false)] + [string[]]$recordTypes = @(), + [Parameter(Mandatory = $false)] + [string[]]$operations = @(), + [Parameter(Mandatory = $false)] + [string[]]$freeTexts = @(), + [Parameter(Mandatory = $false)] + [string[]]$IPAddresses = @(), + [Parameter(Mandatory = $false)] + [string[]]$userIds = @(), + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $false)] + [String]$logFile = "Get-O365Purview.log" + ) + + $currentPath = (Get-Location).path + + $launchSearch = + { + param($cert, $appId, $tenant, $newStartDate, $newEndDate, $requestType, $recordTypes, $operations, $freeTexts, $IPAddresses, $userIds, $currentPath) + + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $actualdate = $(get-date -f yyyy-MM-dd-hh-mm-ss) + $logFile = $currentPath + "\UnifiedAuditLogPurview_" + $dateToProcess + ".log" + + $unifiedAuditFolder = $currentPath + "\O365_unified_audit_logs_purview" + if ((Test-Path $unifiedAuditFolder) -eq $false){ + New-Item $unifiedAuditFolder -Type Directory + } + + $folderToProcess = $unifiedAuditFolder + "\" + $dateToProcess + if ((Test-Path $folderToProcess) -eq $false){ + New-Item $folderToProcess -Type Directory + } + + "Connecting to Microsoft Graph" | Write-Log -LogPath $logFile -LogLevel "Info" + Connect-MicrosoftGraphApplication -certificate $cert -appId $appId -tenant $tenant -logFile $logFile + + "Processing Unified Audit Log (using Purview) entries between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartDate, $newEndDate) | Write-Log -LogPath $logFile + $outputDate = "{0:yyyy-MM-dd}" -f ($dateToProcess) + $outputFile = $folderToProcess + "\UnifiedAuditLogPurview_" + $tenant + "_" + $outputDate + ".json" + $sessionName = $(New-Guid).Guid + + if ($requestType -eq "Unfiltered"){ + # Used in Get-O365Full with no parameters + Get-UnifiedAuditLogPurview -startDate $newStartDate -endDate $newEndDate -sessionName $sessionName -requestType $requestType -certificate $cert -appId $appId -tenant $tenant -logFile $logFile -outputFile $outputFile + } + elseif ($requestType -eq "RecordTypes"){ + # Used in Get-O365Full with the "recordTypes" parameter + # Used in Get-O365Defender + Get-UnifiedAuditLogPurview -startDate $newStartDate -endDate $newEndDate -sessionName $sessionName -requestType $requestType -recordTypes $recordTypes -certificate $cert -appId $appId -tenant $tenant -logFile $logFile -outputFile $outputFile + } + elseif ($requestType -eq "Operations"){ + # Used in Get-O365Light + Get-UnifiedAuditLogPurview -startDate $newStartDate -endDate $newEndDate -sessionName $sessionName -requestType $requestType -operations $operations -certificate $cert -appId $appId -tenant $tenant -logFile $logFile -outputFile $outputFile + } + elseif ($requestType -eq "UserIds"){ + # Used in Search-O365 + $outputFile = $folderToProcess + "\UnifiedAuditLogPurview_" + $tenant + "_" + $outputDate + "_" + $requestType + "_" + $actualDate + ".json" + Get-UnifiedAuditLogPurview -startDate $newStartDate -endDate $newEndDate -sessionName $sessionName -requestType $requestType -userIds $userIds -certificate $cert -appId $appId -tenant $tenant -logFile $logFile -outputFile $outputFile + } + elseif ($requestType -eq "FreeText"){ + # Used in Search-O365 + for ($i=0; $i -lt $($freeTexts.Count); $i++){ + $freeText = $freeTexts[$i] + "Collecting events (using Purview) for freeText $($i+1) (`"$freeText`") between $newStartDate - $newEndDate" | Write-Log -LogPath $logFile -LogLevel "Info" + $outputFile = $folderToProcess + "\UnifiedAuditLogPurview_" + $tenant + "_" + $outputDate + "_" + $requestType + "_" + $actualDate + "_" + $i + ".json" + Get-UnifiedAuditLogPurview -startDate $newStartDate -endDate $newEndDate -sessionName $sessionName -requestType $requestType -freeText $freeText -certificate $cert -appId $appId -tenant $tenant -logFile $logFile -outputFile $outputFile + } + } + elseif ($requestType -eq "IPAddresses"){ + # Used in Search-O365 + $outputFile = $folderToProcess + "\UnifiedAuditLogPurview_" + $tenant + "_" + $outputDate + "_" + $requestType + "_" + $actualDate + ".json" + Get-UnifiedAuditLogPurview -startDate $newStartDate -endDate $newEndDate -sessionName $sessionName -requestType $requestType -IPAddresses $IPAddresses -certificate $cert -appId $appId -tenant $tenant -logFile $logFile -outputFile $outputFile + } + } + + $cert, $null, $null = Import-Certificate -certificatePath $certificatePath -logFile $logFile + + Get-RSJob | Remove-RSJob -Force + + $newStartDate = $startDate + $newEndDate = $endDate + + "Lauching job with startDate {0:yyyy-MM-dd} and endDate {1:yyyy-MM-dd}" -f ($newStartDate, $newEndDate) | Write-Log -LogPath $logFile + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $jobName = "UnifiedAuditLogPurview" + $dateToProcess + + Start-RSJob -Name $jobName -ScriptBlock $launchSearch -FunctionsToImport Get-UnifiedAuditLogPurview, Write-Log -ArgumentList $cert, $appId, $tenant, $newStartDate, $newEndDate, $requestType, $recordTypes, $operations, $freeTexts, $IPAddresses, $userIds, $currentPath + + $maxJobRunning = 1 + + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge $maxJobRunning){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } + + # Waiting for final jobs to complete + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge 1){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } +} + +function Get-O365 { + + <# + .SYNOPSIS + The Get-O365 function is the inner function that handles the different jobs and calling of Get-LargeUnifiedAuditLog and Get-MailboxAuditLog functions + #> + + param ( + [Parameter(Mandatory = $true)] + [ValidateSet("Unfiltered","Operations","RecordTypes","FreeText","IPAddresses","UserIds")] + [String]$requestType, + [Parameter(Mandatory = $false)] + [string[]]$recordTypes = @(), + [Parameter(Mandatory = $false)] + [string[]]$operations = @(), + [Parameter(Mandatory = $false)] + [string[]]$freeTexts = @(), + [Parameter(Mandatory = $false)] + [string[]]$IPAddresses = @(), + [Parameter(Mandatory = $false)] + [string[]]$userIds = @(), + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $false)] + [String]$logFile = "Get-O365.log" + ) + + $currentPath = (Get-Location).path + + $launchSearch = + { + param($cert, $appId, $tenant, $newStartDate, $newEndDate, $requestType, $recordTypes, $operations, $freeTexts, $IPAddresses, $userIds, $currentPath) + + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $actualdate = $(get-date -f yyyy-MM-dd-hh-mm-ss) + $logFile = $currentPath + "\UnifiedAuditLog" + $dateToProcess + ".log" + + $unifiedAuditFolder = $currentPath + "\O365_unified_audit_logs" + if ((Test-Path $unifiedAuditFolder) -eq $false){ + New-Item $unifiedAuditFolder -Type Directory + } + + "Connecting to Exchange Online" | Write-Log -LogPath $logFile -LogLevel "Info" + Connect-ExchangeOnlineApplication -logFile $logFile -certificate $cert -appId $appId -organization $tenant + + "Processing Unified Audit Log entries for day $($dateToProcess)" | Write-Log -LogPath $logFile + $folderToProcess = $unifiedAuditFolder + "\" + $dateToProcess + if ((Test-Path $folderToProcess) -eq $false){ + New-Item $folderToProcess -Type Directory + } + $totalHours = [Math]::Floor((New-TimeSpan -Start $newStartDate -End $newEndDate).TotalHours) + if ($totalHours -eq 24){ + $totalHours-- + } + for ($h=0; $h -le $totalHours; $h++){ + if ($h -eq 0){ + $newStartHour = $newStartDate + $newEndHour = $newStartDate.AddMinutes(59 - $newStartDate.Minute).AddSeconds(60 - $newStartDate.Second) + } + elseif ($h -eq $totalHours){ + $newStartHour = $newEndHour + $newEndHour = $newEndDate + } + else { + $newStartHour = $newEndHour + $newEndHour = $newStartHour.addHours(1) + } + "Processing Unified Audit Log entries between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartHour, $newEndHour) | Write-Log -LogPath $logFile + $outputDate = "{0:yyyy-MM-dd}_{0:HH-00-00}" -f ($newStartHour) + $outputFile = $folderToProcess + "\UnifiedAuditLog_" + $tenant + "_" + $outputDate + ".json" + $sessionName = $(New-Guid).Guid + + if ($requestType -eq "Unfiltered"){ + # Used in Get-O365Full with no parameters + Get-LargeUnifiedAuditLog -startDate $newStartHour -endDate $newEndHour -sessionName $sessionName -outputFile $outputFile -logFile $logFile -requestType $requestType -certificate $cert -appId $appId -tenant $tenant + } + elseif ($requestType -eq "RecordTypes"){ + # Used in Get-O365Full with the "recordTypes" parameter + # Used in Get-O365Defender + foreach ($recordType in $recordTypes){ + "Collecting $recordType events for $newStartHour - $newEndHour" | Write-Log -LogPath $logFile -LogLevel "Info" + $outputFile = $folderToProcess + "\UnifiedAuditLog_" + $tenant + "_" + $outputDate + "_" + $recordType + ".json" + Get-LargeUnifiedAuditLog -startDate $newStartHour -endDate $newEndHour -sessionName $sessionName -recordType $recordType -outputFile $outputFile -logFile $logFile -requestType $requestType -certificate $cert -appId $appId -tenant $tenant + } + } + elseif ($requestType -eq "Operations"){ + # Used in Get-O365Light + Get-LargeUnifiedAuditLog -startDate $newStartHour -endDate $newEndHour -sessionName $sessionName -operations $operations -outputFile $outputFile -logFile $logFile -requestType $requestType -certificate $cert -appId $appId -tenant $tenant + } + elseif ($requestType -eq "UserIds"){ + # Used in Search-O365 + $outputFile = $folderToProcess + "\UnifiedAuditLog_" + $tenant + "_" + $outputDate + "_" + $requestType + "_" + $actualDate + ".json" + Get-LargeUnifiedAuditLog -startDate $newStartHour -endDate $newEndHour -sessionName $sessionName -userIds $userIds -outputFile $outputFile -logFile $logFile -requestType $requestType -certificate $cert -appId $appId -tenant $tenant + + $mailboxAuditFolder = $currentPath + "\Exchange_mailbox_audit_logs" + if ((Test-Path $mailboxAuditFolder) -eq $false){ + New-Item $mailboxAuditFolder -Type Directory + } + $mailboxAuditFolderToProcess = $mailboxAuditFolder + "\" + $dateToProcess + if ((Test-Path $mailboxAuditFolderToProcess) -eq $false){ + New-Item $mailboxAuditFolderToProcess -Type Directory + } + $outputfileWithoutJson = $mailboxAuditFolderToProcess + "\MailboxAuditLog_" + $tenant + "_" + $outputdate + "_" + $requesttype + "_" + $actualdate + "Processing MailboxAudit entries between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartHour, $newEndHour) | Write-Log -LogPath $logFile + Get-MailboxAuditLog -startDate $newStartHour -endDate $newEndHour -outputFileWithoutJson $outputFileWithoutJson -logFile $logFile -userIds $userIds -certificate $cert -appId $appId -tenant $tenant + } + elseif ($requestType -eq "FreeText"){ + # Used in Search-O365 + for ($i=0; $i -lt $($freeTexts.Count); $i++){ + $freeText = $freeTexts[$i] + "Collecting events for freeText $($i+1) (`"$freeText`") between $newStartHour - $newEndHour" | Write-Log -LogPath $logFile -LogLevel "Info" + $outputFile = $folderToProcess + "\UnifiedAuditLog_" + $tenant + "_" + $outputDate + "_" + $requestType + "_" + $actualDate + "_" + $i + ".json" + Get-LargeUnifiedAuditLog -startDate $newStartHour -endDate $newEndHour -sessionName $sessionName -freeText $freeText -outputFile $outputFile -logFile $logFile -requestType $requestType -certificate $cert -appId $appId -tenant $tenant + } + } + elseif ($requestType -eq "IPAddresses"){ + # Used in Search-O365 + $outputFile = $folderToProcess + "\UnifiedAuditLog_" + $tenant + "_" + $outputDate + "_" + $requestType + "_" + $actualDate + ".json" + Get-LargeUnifiedAuditLog -startDate $newStartHour -endDate $newEndHour -sessionName $sessionName -IPAddresses $IPAddresses -outputFile $outputFile -logFile $logFile -requestType $requestType -certificate $cert -appId $appId -tenant $tenant + } + } + } + + $cert, $null, $null = Import-Certificate -certificatePath $certificatePath -logFile $logFile + + $totalTimeSpan = (New-TimeSpan -Start $startDate -End $endDate) + + if (($totalTimeSpan.Hours -eq 0) -and ($totalTimeSpan.Minutes -eq 0) -and ($totalTimeSpan.Seconds -eq 0)){ + $totalDays = $totalTimeSpan.days + $totalLoops = $totalDays + } + else { + $totalDays = $totalTimeSpan.days + 1 + $totalLoops = $totalTimeSpan.days + } + + Get-RSJob | Remove-RSJob -Force + + "Checking permissions for app $($appId)"| Write-Log -LogPath $logFile + Connect-ExchangeOnlineApplication -logFile $logFile -certificate $cert -appId $appId -organization $tenant + try { + $null = Search-UnifiedAuditLog -startDate (Get-Date).AddDays(-1) -endDate (Get-Date) -ResultSize 1 + } + catch { + $errormessage = $_.Exception.Message + if ($errormessage -like "*The term 'Search-UnifiedAuditLog'*"){ + Write-Error "$appId does not have the required permissions to get Microsoft 365 Unified Audit Logs: does not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. Please delete and re-create the application. Exiting" + "$appId does not have the required permissions to get Microsoft 365 Unified Audit Logs: does not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. Please delete and re-create the application. Exiting" | Write-Log -LogPath $logFile -LogLevel "Error" + exit + } + } + + for ($d=0; $d -le $totalLoops; $d++){ + if ($d -eq 0){ + $newStartDate = $startDate + $newEndDate = Get-Date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newStartDate.AddDays(1))) + } + elseif ($d -eq $totalDays){ + $newEndDate = $endDate + $newStartDate = Get-Date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newEndDate)) + } + else { + $newStartDate = $newEndDate + $newEndDate = $newEndDate.AddDays(1) + } + + "Lauching job number $($d) with startDate {0:yyyy-MM-dd} {0:HH:mm:ss} and endDate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newStartDate, $newEndDate) | Write-Log -LogPath $logFile + $dateToProcess = ($newStartDate.ToString("yyyy-MM-dd")) + $jobName = "UnifiedAuditLog" + $dateToProcess + + Start-RSJob -Name $jobName -ScriptBlock $launchSearch -FunctionsToImport Connect-ExchangeOnlineApplication, Write-Log, Get-LargeUnifiedAuditLog, Get-MailboxAuditLog -ArgumentList $cert, $appId, $tenant, $newStartDate, $newEndDate, $requestType, $recordTypes, $operations, $freeTexts, $IPAddresses, $userIds, $currentPath + + $maxJobRunning = 1 + + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge $maxJobRunning){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } + } + + # Waiting for final jobs to complete + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + while ($jobRunningCount -ge 1){ + Start-Sleep -Seconds 1 + $jobRunningCount = (Get-RSJob | Where-Object {$_.State -eq "Running"} | Measure-Object).Count + } + $jobsDone = Get-RSJob | Where-Object {$_.State -eq "Completed"} + if ($jobsDone){ + foreach ($jobDone in $jobsDone){ + "Runspace Job $($jobDone.Name) has finished - dumping log" | Write-Log -LogPath $logFile + $logFileName = $jobDone.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobDone | Remove-RSJob + "Runspace Job $($jobDone.Name) finished - job removed" | Write-Log -LogPath $logFile + } + } + $jobsFailed = Get-RSJob | Where-Object {$_.State -eq "Failed"} + if ($jobsFailed){ + foreach ($jobFailed in $jobsFailed){ + "Runspace Job $($jobFailed.Name) failed with error $($jobFailed.Error)" | Write-Log -LogPath $logFile -LogLevel "Error" + "Runspace Job $($jobFailed.Name) failed - dumping log" | Write-Log -LogPath $logFile -LogLevel "Error" + $logFileName = $jobFailed.Name + ".log" + Get-Content $logFileName | Out-File $logFile -Encoding UTF8 -Append + Remove-Item $logFileName -Confirm:$false -Force + $jobFailed | Remove-RSJob + "Runspace Job $($jobFailed.Name) failed - job removed" | Write-Log -LogPath $logFile -LogLevel "Error" + } + } +} + +function Get-O365Full { + + <# + .SYNOPSIS + The Get-O365Full function dumps in JSON files all or some specific record types from the Unified Audit Log for a specific time range. + The list of record types can be found here: https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-schema#enum-auditlogrecordtype---type-edmint32. + Using the "-purview" switch, you can search using the Purview backend, instead of the Unified Audit Log. + + .EXAMPLE + + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-90) + + PS C:\>Get-O365Full -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath + Dump all events from the Unified Audit Log for the last 90 days. + + PS C:\>Get-O365Full -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -recordTypes "AzureActiveDirectory","OneDrive" + Dump all events related to Entra ID and OneDrive from the Unified Audit Log for the last 90 days. + + PS C:\>$startDate = $endDate.AddDays(-180) + PS C:\>Get-O365Full -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -purview + Dump all events from the Unified Audit Log for the last 180 days, using the Purview backend. + #> + + param ( + [Parameter(Mandatory = $false)] + [string[]]$recordTypes, + [Parameter(Mandatory = $false)] + [Switch]$purview = $false, + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $false)] + [String]$logFile = "Get-O365Full.log" + ) + + if ($purview){ + if ($null -ne $recordTypes){ + Get-O365Purview -startDate $startDate -endDate $endDate -recordTypes $recordTypes -requestType "RecordTypes" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + else { + Get-O365Purview -startDate $startDate -endDate $endDate -requestType "Unfiltered" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + } + else { + if ($null -ne $recordTypes){ + Get-O365 -startDate $startDate -endDate $endDate -recordTypes $recordTypes -requestType "RecordTypes" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + else { + Get-O365 -startDate $startDate -endDate $endDate -requestType "Unfiltered" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + } +} + +function Get-O365Light { + + <# + .SYNOPSIS + The Get-O365Light function dumps in JSON files a subset of events related to operations of interest from the Unified Audit Log for a specific time range. + Using the "-purview" switch, you can search using the Purview backend, instead of the Unified Audit Log. + Using the "-mailboxlogin" switch, you can add the "MailboxLogin" operations. If mailbox auditing is enabled, be aware that this can represent a lot of events. + Using the "userLogin" switch, you can add the "UserLoggedIn" and "UserLoginFailed" operations. Be aware that this can represent a lot of events. + + .EXAMPLE + + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-90) + + PS C:\>Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "all" + Dump all events related to operations of interest from the Unified Audit Log for the last 90 days. + + PS C:\>Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "allButAzureAD" + Dump all but Entra ID events related to operations of interest from the Unified Audit Log for the last 90 days. + + PS C:\>Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "ExchangeOnly" -mailboxLogin -userLogin + Dump Exchange events related to operations of interest, mailbox login events and user login events from the Unified Audit Log for the last 90 days. + + PS C:\>Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "ExchangeOnly" + Dump Exchange events related to operations of interest from the Unified Audit Log for the last 90 days. + + PS C:\>Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "OneDrive_Sharepoint_Teams_YammerOnly" + Dump OneDrive, Sharepoint, Teams and Yammer events related to operations of interest from the Unified Audit Log for the last 90 days. + + PS C:\>Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "AzureADOnly" + Dump Entra ID events related to operations of interest from the Unified Audit Log for the last 90 days. + + PS C:\>Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "SecurityAlertsOnly" + Dump Security Alerts events related to operations of interest from the Unified Audit Log for the last 90 days. + + PS C:\>$startDate = $endDate.AddDays(-180) + PS C:\>Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "all" -purview + Dump all events related to operations of interest from the Unified Audit Log for the last 180 days, using the Purview backend. + #> + + param ( + [Parameter(Mandatory = $false)] + [ValidateSet("all","allButAzureAD","ExchangeOnly","OneDrive_Sharepoint_Teams_YammerOnly", "AzureADOnly", "SecurityAlertsOnly")] + [String]$operationsSet = "all", + [Parameter(Mandatory = $false)] + [Switch]$mailboxLogin=$false, + [Parameter(Mandatory = $false)] + [Switch]$userLogin=$false, + [Parameter(Mandatory = $false)] + [Switch]$purview = $false, + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $false)] + [String]$logFile = "Get-O365Light.log" + ) + + $OneDrive_Sharepoint_Teams_YammerOnly_operations = @( + "AddedToGroup", + "AnonymousLinkCreated", + "AnonymousLinkUsed", + "AppInstalled", + "DeviceAccessPolicyChanged", + "FileMalwareDetected", + "GeoAdminAdded", + "MemberRoleChanged", + "NetworkAccessPolicyChanged", + "NetworkSecurityConfigurationUpdated", + "New-CSTeamsAppPermissionPolicy", + "PermissionLevelModified", + "Set-CSTeamsAppPermissionPolicy", + "SharingInvitationAccepted", + "SharingInvitationBlocked", + "SharingPolicyChanged", + "SiteCollectionAdminAdded", + "SoftDeleteSettingsUpdated", + "SupervisorAdminToggled", + "TeamSettingChanged", + "TeamsTenantSettingChanged", + "UnmanagedSyncClientBlocked" + ) + + $AzureAD_operations = @( + "Add application", + "Add app role assignment grant to user", + "Add app role assignment to service principal", + "Add delegated permission grant", + "Add delegation entry", + "Add domain to company", + "Add group", + "Add member to group", + "Add member to role", + "Add OAuth2PermissionGrant", + "Add partner to company", + "Add service principal", + "Add service principal credentials", + "Add unverified domain", + "Add verified domain", + "Consent to application", + "Delete group", + "Disable Desktop Sso for a specific domain", + "New-ConditionalAccessPolicy", + "Register connector", + "Remove delegation entry", + "Remove member from group", + "Remove member from role", + "Remove service principal", + "Remove service principal credentials", + "Remove verified domain", + "Set-AdminAuditLogConfig", + "Set-ConditionalAccessPolicy", + "Set delegation entry", + "Set domain authentication", + "Set federation settings on domain", + "Update application", + "Update application – Certificates and secrets management ", + "Update application – Certificates and secrets management", + "Update domain", + "Update group", + "Verify domain" + ) + + $Exchange_operations = @( + "AddFolderPermissions", + "Add-MailboxPermission", + "Add-RecipientPermission", + "Hard Delete user", + "New-InboxRule", + "New-TransportRule", + "RemoveFolderPermissions", + "Remove-MailboxPermission", + "Remove-RecipientPermission", + "SearchCreated", + "SearchExported", + "Set-CASMailbox", + "Set-InboxRule", + "Set-Mailbox", + "Set-TransportRule", + "UpdateInboxRules" + ) + + $securityAlerts_operations = @( + "AlertEntityGenerated", + "AlertTriggered" + ) + + $mailboxLogin_operation = @( + "MailboxLogin" + ) + + $userLogin_operations = @( + "UserLoggedIn", + "UserLoginFailed" + ) + + $operationsToProcess = @() + + if ($mailboxLogin){ + Write-Warning "Retrieving MailboxLogin operations. If mailbox auditing is enabled, be aware that this can represent a lot of events." + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + "Retrieving MailboxLogin operations. If mailbox auditing is enabled, be aware that this can represent a lot of events." | Write-Log -LogPath $logFile -LogLevel "Warning" + $operationsToProcess = $($operationsToProcess ; $mailboxLogin_operation) + } + } + + if ($userLogin){ + Write-Warning "Retrieving UserLoggedIn and UserLoggedInFailed operations. Be aware that this can represent a lot of events." + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + "Retrieving UserLoggedIn and UserLoggedInFailed operations. Be aware that this can represent a lot of events." | Write-Log -LogPath $logFile -LogLevel "Warning" + $operationsToProcess = $($operationsToProcess ; $userLogin_operations) + } + } + + if ($operationsSet -eq "all"){ + $operationsToProcess = $($operationsToProcess ; $OneDrive_Sharepoint_Teams_YammerOnly_operations ; $AzureAD_operations ; $Exchange_operations ; $securityAlerts_operations) + "Fetching all operations of interest, this is the default configuration" | Write-Log -LogPath $logFile + } + elseif ($operationsSet -eq "allButAzureAD"){ + $operationsToProcess = $($operationsToProcess ; $OneDrive_Sharepoint_Teams_YammerOnly_operations ; $Exchange_operations ; $securityAlerts_operations) + "Fetching all operations of interest, except Entra ID related operations" | Write-Log -LogPath $logFile + } + elseif ($operationsSet -eq "ExchangeOnly"){ + $operationsToProcess = $($operationsToProcess ; $Exchange_operations) + "Fetching only Exchange Online operations of interest" | Write-Log -LogPath $logFile + } + elseif ($operationsSet -eq "OneDrive_Sharepoint_Teams_YammerOnly"){ + $operationsToProcess = $($operationsToProcess ; $OneDrive_Sharepoint_Teams_YammerOnly_operations) + "Fetching only OneDrive, SharePoint, Teams and Yammer operations of interest" | Write-Log -LogPath $logFile + } + elseif ($operationsSet -eq "SecurityAlertsOnly"){ + $operationsToProcess = $($operationsToProcess ; $securityAlerts_operations) + "Fetching only Security Alerts operations of interest" | Write-Log -LogPath $logFile + } + elseif ($operationsSet -eq "AzureADOnly"){ + $operationsToProcess = $($operationsToProcess ; $AzureAD_operations) + "Fetching only Entra ID operations of interest" | Write-Log -LogPath $logFile + } + + if ($purview){ + Get-O365Purview -startDate $startDate -endDate $endDate -operations $operationsToProcess -requestType "Operations" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + else { + Get-O365 -startDate $startDate -endDate $endDate -operations $operationsToProcess -requestType "Operations" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + +} + +function Get-O365Defender { + + <# + .SYNOPSIS + The Get-O365Defender function dumps in JSON files events related to Microsoft Defender for Microsoft 365 in the Unified Audit Log for a specific time range. You need Defender for Microsoft 365 Plan 1 or 2, or Microsoft 365 A5/E5/F5/G5 Security to retrieve such logs. + Using the "-purview" switch, you can search using the Purview backend, instead of the Unified Audit Log. + + .EXAMPLE + + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-90) + + PS C:\>Get-O365Defender -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath + Dump all Microsoft Defender for Microsoft 365 events from the Unified Audit Log for the last 90 days. + + PS C:\>$startDate = $endDate.AddDays(-180) + PS C:\>Get-O365Defender -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -purview + Dump all Microsoft Defender for Microsoft 365 events from the Unified Audit Log for the last 180 days, using the Purview backend. + #> + + param ( + [Parameter(Mandatory = $false)] + [Switch]$purview = $false, + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $false)] + [String]$logfile = "Get-O365Defender.log" + ) + + Write-Warning "You need Defender for Office 365 Plan 1 or 2, or Microsoft 365 A5/E5/F5/G5 Security to retrieve such logs" + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + $Defender_recordTypes = @( + "AirAdminActionInvestigation", + "AirInvestigation", + "AirManualInvestigation", + "Campaign", + "MCASAlerts", + "MSTIC", + "ThreatFinder", + "ThreatIntelligence", + "ThreatIntelligenceAtpContent", + "ThreatIntelligenceUrl", + "WDATPAlerts" + ) + if ($purview){ + Get-O365Purview -startDate $startDate -endDate $endDate -recordTypes $Defender_recordTypes -requestType "RecordTypes" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + else { + Get-O365 -startDate $startDate -endDate $endDate -recordTypes $Defender_recordTypes -requestType "RecordTypes" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + } +} + +function Search-O365 { + + <# + .SYNOPSIS + The Search-O365 function dumps in JSON files events related to specific free texts, IP addresses or users. Those events come from the Unified Audit Log and, when searching for users, the Mailbox Audit. The search is restrained to a specific time range. + Using the "-purview" switch, you can search using the Purview backend, instead of the Unified Audit Log. + + .EXAMPLE + + PS C:\>$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + PS C:\>$tenant = "example.onmicrosoft.com" + PS C:\>$certificatePath = "./example.pfx" + PS C:\>$endDate = Get-Date + PS C:\>$startDate = $endDate.AddDays(-90) + + PS C:\>Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -freeTexts "Python" + PS C:\>Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -freeTexts "Python","Python3" + Search for all events in the last 90 days which contain the string "Python" and "Python3" + + PS C:\>Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -IPAddresses "8.8.8.8" + PS C:\>Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -IPAddresses "8.8.8.8","4.4.4.4" + Search for all events in the last 90 days related to the activity of IP addresses "8.8.8.8" and "4.4.4.4" + + PS C:\>Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -userIds "user1@example.onmicrosoft.com" + PS C:\>Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -userIds "user1@example.onmicrosoft.com","user2@example.onmicrosoft.com" + Search for all events in the last 90 days related to the activity of users "user1@example.onmicrosoft.com" and "user2@example.onmicrosoft.com" + + PS C:\>$startDate = $endDate.AddDays(-180) + PS C:\>Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -userIds "user1@example.onmicrosoft.com" -purview + Search for all events in the last 180 days related to the activity of user "user1@example.onmicrosoft.com" using the Purview backend + + #> + + param ( + + [Parameter(Mandatory = $false)] + [string[]]$freeTexts, + [Parameter(Mandatory = $false)] + [string[]]$IPAddresses, + [Parameter(Mandatory = $false)] + [string[]]$userIds, + [Parameter(Mandatory = $false)] + [Switch]$purview = $false, + [Parameter(Mandatory = $true)] + [DateTime]$startDate, + [Parameter(Mandatory = $true)] + [DateTime]$endDate, + [Parameter(Mandatory = $true)] + [String]$certificatePath, + [Parameter(Mandatory = $true)] + [String]$appId, + [Parameter(Mandatory = $true)] + [String]$tenant, + [Parameter(Mandatory = $false)] + [String]$logFile = "Search-O365.log" + ) + + if ($freeTexts){ + "Searching freeTexts $($freeTexts) in Unified Audit Log" | Write-Log -LogPath $logFile + if ($purview){ + Get-O365Purview -startDate $startDate -endDate $endDate -freeTexts $freeTexts -requestType "FreeText" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + else { + Get-O365 -startDate $startDate -endDate $endDate -freeTexts $freeTexts -requestType "FreeText" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + } + if ($IPAddresses){ + "Searching IPAddresses $($IPAddresses) in Unified Audit Log" | Write-Log -LogPath $logFile + if ($purview){ + Get-O365Purview -startDate $startDate -endDate $endDate -IPAddresses $IPAddresses -requestType "IPAddresses" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + else { + Get-O365 -startDate $startDate -endDate $endDate -IPAddresses $IPAddresses -requestType "IPAddresses" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + } + if ($userIds){ + "Searching userIds $($userIds) in Unified Audit Log and Mailbox Audit Log" | Write-Log -LogPath $logFile + if ($purview){ + Get-O365Purview -startDate $startDate -endDate $endDate -userIds $userIds -requestType "UserIds" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + else { + Get-O365 -startDate $startDate -endDate $endDate -userIds $userIds -requestType "UserIds" -tenant $tenant -appId $appId -certificatePath $certificatePath -logFile $logFile + } + } +} \ No newline at end of file diff --git a/DFIR-O365RC/Get-O365Full.ps1 b/DFIR-O365RC/Get-O365Full.ps1 deleted file mode 100644 index bb155d9..0000000 --- a/DFIR-O365RC/Get-O365Full.ps1 +++ /dev/null @@ -1,296 +0,0 @@ - -Function Get-O365Full { - - <# - .SYNOPSIS - The Get-O365Full function dumps in JSON files all or some record types from the O365 Unified Audit Log for a specific time range. - - .EXAMPLE - - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-7) - - PS C:\>Get-O365Full -startdate $startdate -enddate $enddate -RecordSet "All" - - Dump all unified audit logs since last week - .EXAMPLE - - Get-O365Full -startdate $startdate -enddate $enddate -RecordSet "ExchangeOnly" -logfile "UnifiedExchangeRecords.log" - Dump Exchange only records set since last week and write log to UnifiedExchangeRecords.log - .EXAMPLE - - Get-O365Full -startdate $startdate -enddate $enddate -RecordTypes "Yammer" -logfile "UnifiedYammerOnly.log" - Dump Yammer records since last week and write log to UnifiedYammerOnly.log - #> - - param ( - [Parameter(Mandatory = $false, ParameterSetName="byrecordtype")] - [System.Array]$RecordTypes = ("ExchangeAdmin", "ExchangeItem","ExchangeItemGroup","SharePoint","SyntheticProbe","SharePointFileOperation","OneDrive","DataCenterSecurityCmdlet","ComplianceDLPSharePoint","Sway","ComplianceDLPExchange","SharePointSharingOperation","SkypeForBusinessPSTNUsage","SkypeForBusinessUsersBlocked","SecurityComplianceCenterEOPCmdlet","ExchangeAggregatedOperation","PowerBIAudit","CRM","Yammer","SkypeForBusinessCmdlets","Discovery","MicrosoftTeams","ThreatIntelligence","MailSubmission","MicrosoftFlow","AeD","MicrosoftStream","ComplianceDLPSharePointClassification","ThreatFinder","Project","SharePointListOperation","SharePointCommentOperation","DataGovernance","Kaizala","SecurityComplianceAlerts","ThreatIntelligenceUrl","SecurityComplianceInsights","MIPLabel","WorkplaceAnalytics","PowerAppsApp","PowerAppsPlan","ThreatIntelligenceAtpContent","LabelContentExplorer","TeamsHealthcare","ExchangeItemAggregated","HygieneEvent","DataInsightsRestApiAudit","InformationBarrierPolicyApplication","SharePointListItemOperation","SharePointContentTypeOperation","SharePointFieldOperation","MicrosoftTeamsAdmin","HRSignal","MicrosoftTeamsDevice","MicrosoftTeamsAnalytics","InformationWorkerProtection","Campaign","DLPEndpoint","AirInvestigation","Quarantine","MicrosoftForms","ApplicationAudit","ComplianceSupervisionExchange","CustomerKeyServiceEncryption","OfficeNative","MipAutoLabelSharePointItem","MipAutoLabelSharePointPolicyLocation","MicrosoftTeamsShifts","MipAutoLabelExchangeItem","CortanaBriefing","Search","WDATPAlerts","MDATPAudit","AzureActiveDirectory","AzureActiveDirectoryAccountLogon","AzureActiveDirectoryStsLogon","AirManualInvestigation","SecurityComplianceRBAC","AirAdminActionInvestigation","MSTIC","MCASAlerts","OnPremisesFileShareScannerDlp","OnPremisesSharePointScannerDlp","ExchangeSearch","SharePointSearch","SecurityComplianceUserChange","ComplianceDLPExchangeClassification"), - [Parameter(Mandatory = $false, ParameterSetName="byrecordset")] - [ValidateSet("All","AllbutAzureAD","ExchangeOnly","SharePointOnly","AzureADOnly")] - [String]$RecordSet = "All", - [Parameter(Mandatory = $true)] - [DateTime]$Enddate, - [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, - [Parameter(Mandatory = $false)] - [String]$logfile = "Get-O365Full.log" - ) - - $nbrecordtypes = ($RecordTypes | Measure-Object).count - if($nbrecordtypes -ne 87) - {"Fetching $($nbrecordtypes) custom record types" | Write-Log -LogPath $logfile} - - else{ - if($RecordSet -eq "AllbutAzureAD") - { - $RecordTypes = "ExchangeAdmin", "ExchangeItem","ExchangeItemGroup","SharePoint","SyntheticProbe","SharePointFileOperation","OneDrive","DataCenterSecurityCmdlet","ComplianceDLPSharePoint","Sway","ComplianceDLPExchange","SharePointSharingOperation","SkypeForBusinessPSTNUsage","SkypeForBusinessUsersBlocked","SecurityComplianceCenterEOPCmdlet","ExchangeAggregatedOperation","PowerBIAudit","CRM","Yammer","SkypeForBusinessCmdlets","Discovery","MicrosoftTeams","ThreatIntelligence","MailSubmission","MicrosoftFlow","AeD","MicrosoftStream","ComplianceDLPSharePointClassification","ThreatFinder","Project","SharePointListOperation","SharePointCommentOperation","DataGovernance","Kaizala","SecurityComplianceAlerts","ThreatIntelligenceUrl","SecurityComplianceInsights","MIPLabel","WorkplaceAnalytics","PowerAppsApp","PowerAppsPlan","ThreatIntelligenceAtpContent","LabelContentExplorer","TeamsHealthcare","ExchangeItemAggregated","HygieneEvent","DataInsightsRestApiAudit","InformationBarrierPolicyApplication","SharePointListItemOperation","SharePointContentTypeOperation","SharePointFieldOperation","MicrosoftTeamsAdmin","HRSignal","MicrosoftTeamsDevice","MicrosoftTeamsAnalytics","InformationWorkerProtection","Campaign","DLPEndpoint","AirInvestigation","Quarantine","MicrosoftForms","ApplicationAudit","ComplianceSupervisionExchange","CustomerKeyServiceEncryption","OfficeNative","MipAutoLabelSharePointItem","MipAutoLabelSharePointPolicyLocation","MicrosoftTeamsShifts","MipAutoLabelExchangeItem","CortanaBriefing","Search","WDATPAlerts","MDATPAudit","AirManualInvestigation","SecurityComplianceRBAC","AirAdminActionInvestigation","MSTIC","MCASAlerts","OnPremisesFileShareScannerDlp","OnPremisesSharePointScannerDlp","ExchangeSearch","SharePointSearch","SecurityComplianceUserChange","ComplianceDLPExchangeClassification" - "Fetching all record types except Azure AD logs" | Write-Log -LogPath $logfile - } - Elseif($RecordSet -eq "ExchangeOnly") - { - $RecordTypes = "ExchangeAdmin","ExchangeAggregatedOperation","ExchangeItem","ExchangeItemGroup","ExchangeItemAggregated","ComplianceDLPExchange","ComplianceSupervisionExchange","MipAutoLabelExchangeItem","ExchangeSearch" - "Fetching only Exchange Online record types" | Write-Log -LogPath $logfile - } - Elseif($RecordSet -eq "SharePointOnly") - { - $RecordTypes = "ComplianceDLPSharePoint","SharePoint","SharePointFileOperation","SharePointSharingOperation","SharepointListOperation", "ComplianceDLPSharePointClassification","SharePointCommentOperation", "SharePointListItemOperation", "SharePointContentTypeOperation", "SharePointFieldOperation","MipAutoLabelSharePointItem","MipAutoLabelSharePointPolicyLocation","OnPremisesSharePointScannerDlp","SharePointSearch" - "Fetching only SharePoint Online record types" | Write-Log -LogPath $logfile - } - Elseif($RecordSet -eq "AzureADOnly") - { - $RecordTypes = "AzureActiveDirectory","AzureActiveDirectoryStsLogon","AzureActiveDirectoryAccountLogon" - "Fetching only Azure AD record types" | Write-Log -LogPath $logfile - } - else { - "Fetching all record types, this is the default configuration" | Write-Log -LogPath $logfile - } - } - - "Getting EXO Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName - $currentpath = (get-location).path - $o365existing = Get-PSSession | where-object{$_.ComputerName -eq "outlook.office365.com"} - if($o365existing){ - "Detected existing EXO session - removing and sleeping for session tear down" | Write-Log -LogPath $logfile -LogLevel "Warning" - $o365existing | remove-pssession -confirm:$false - start-sleep -seconds 15 - } - - $totaltimespan = (New-TimeSpan -Start $StartDate -End $Enddate) - - if(($totaltimespan.hours -eq 0) -and ($totaltimespan.minutes -eq 0) -and ($totaltimespan.seconds -eq 0)) - {$totaldays = $totaltimespan.days - $totalloops = $totaldays - } - else - {$totaldays = $totaltimespan.days + 1 - $totalloops = $totaltimespan.days - } - - Get-RSJob | Remove-RSJob -Force - - $Launchsearch = - { - Param($app, $user, $newstartdate, $newenddate, $RecordTypes,$currentpath) - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $logfile = $currentpath + "\UnifiedAudit" + $datetoprocess + ".log" - - $unifiedauditfolder = $currentpath + "\O365_unified_audit_logs" - if ((Test-Path $unifiedauditfolder) -eq $false){New-Item $unifiedauditfolder -Type Directory} - "Processing O365 logs for day $($datetoprocess)"| Write-Log -LogPath $logfile - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile - $foldertoprocess = $unifiedauditfolder + "\" + $datetoprocess - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory} - $totalhours = [Math]::Floor((New-TimeSpan -Start $newstartdate -End $newenddate).Totalhours) - if($totalhours -eq 24){$totalhours--} - For ($h=0; $h -le $totalhours ; $h++) - { - - if($h -eq 0) - { - $newstarthour = $newstartdate - $newendhour = $newstartdate.AddMinutes(59 - $newstartdate.Minute).AddSeconds(60 - $newstartdate.Second) - } - elseif($h -eq $totalhours) - { - $newstarthour = $newendhour - $newendhour = $newenddate - } - else { - $newstarthour = $newendhour - $newendhour = $newstarthour.addHours(1) - } - "Processing logs between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile - $outputdate = "{0:yyyy-MM-dd}_{0:HH-00-00}" -f ($newstarthour) - $tenant = ($token.Account.UserName).split("@")[1] - $outputfile = $foldertoprocess + "\UnifiedAuditLog_" + $tenant + "_" + $outputdate + ".json" - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - "Refreshing token - valid till " + $token.ExpiresOn.LocalDateTime.Tostring() | Write-Log -LogPath $logfile - Foreach ($recordtype in $RecordTypes) - { - try { - - $trysearch = Search-UnifiedAuditLog -StartDate $newstarthour -EndDate $newendhour -RecordType $recordtype -ResultSize 1 -ErrorAction Stop - - } - catch { - "Retrieving Unified audit logs failed, rebuilding EXO session " | Write-Log -LogPath $logfile -LogLevel "Warning" - Get-PSSession | Remove-PSSession -Confirm:$false - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - Start-Sleep -Seconds 15 - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile - $trysearch = Search-UnifiedAuditLog -StartDate $newstarthour -EndDate $newendhour -RecordType $recordtype -ResultSize 1 - } - if($trysearch) - { - $countobjects = $trysearch.ResultCount - "Dumping $($countobjects) $($recordtype) records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile - if($countobjects -gt 50000) - { - "More than 50000 $($recordtype) records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} - some records might be missing" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile -LogLevel "Warning" - } - $sessionName = [guid]::NewGuid().ToString() - Get-LargeUnifiedAuditLog -sessionName $sessionName -StartDate $newstarthour -EndDate $newendhour -RecordType $recordtype -outputfile $outputfile -logfile $logfile -requesttype "Records" - } - } - - } - "Removing PSSession and sleeping 15 seconds for session tear down" | Write-Log -LogPath $logfile - Get-PSSession | Remove-PSSession -Confirm:$false - Start-Sleep -Seconds 15 - } - - $token = Get-OAuthToken -Service EXO -silent $true -LoginHint $user -Logfile $logfile - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - if($null -eq $app) - { - "No token cache available for EXO service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - } - - "Checking permissions for $($user)"| Write-Log -LogPath $logfile - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - $void = Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile - try { - $trysearch = Search-UnifiedAuditLog -StartDate (get-date).adddays(-90) -EndDate (get-date) -RecordType $recordtype -ResultSize 1 - - } - catch { - $errormessage = $_.Exception.Message - if ($errormessage -like "*The term 'Search-UnifiedAuditLog'*") { - "$user does not have the required permissions to get Office 365 Unified Audit Logs : doees not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. See https://learn.microsoft.com/en-us/purview/audit-log-search?view=o365-worldwide#before-you-search-the-audit-log. Cannot continue" | Write-Error - "$user does not have the required permissions to get Office 365 Unified Audit Logs : doees not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. See https://learn.microsoft.com/en-us/purview/audit-log-search?view=o365-worldwide#before-you-search-the-audit-log. Cannot continue" | Write-Log -LogPath $logfile -LogLevel "Error" - exit - } - } - - For ($d=0; $d -le $totalloops ; $d++) - { - if($d -eq 0) - { - $newstartdate = $StartDate - $newenddate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newstartdate.AddDays(1))) - } - elseif($d -eq $totaldays) - { - $newenddate = $Enddate - $newstartdate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newenddate)) - } - else { - $newstartdate = $newenddate - $newenddate = $newenddate.AddDays(+1) - } - - - $token = Get-OAuthToken -Service EXO -silent $true -LoginHint $user -Logfile $logfile - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - if($null -eq $app) - { - "No token cache available for EXO service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - } - "Lauching job number $($d) with startdate {0:yyyy-MM-dd} {0:HH:mm:ss} and enddate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $jobname = "UnifiedAudit" + $datetoprocess - Start-RSJob -Name $jobname -ScriptBlock $Launchsearch -FunctionsToImport Connect-EXOPsearchUnified, write-log, Get-LargeUnifiedAuditLog -ArgumentList $app, $user, $newstartdate, $newenddate, $RecordTypes, $currentpath - - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 3) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } - $jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} - if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } - } - } - - #Waiting for final jobs to complete - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 1) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } -$jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} -if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } - } -} diff --git a/DFIR-O365RC/Get-O365Light.ps1 b/DFIR-O365RC/Get-O365Light.ps1 deleted file mode 100644 index e639051..0000000 --- a/DFIR-O365RC/Get-O365Light.ps1 +++ /dev/null @@ -1,324 +0,0 @@ -Function Get-O365Light { - - <# - .SYNOPSIS - The Get-O365Light function dumps in JSON files all or some operations set from a defined subset of the O365 Unified Audit Log for a specific time range. - - .EXAMPLE - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-90) - - PS C:\>Get-O365Light -startdate $startdate -enddate $enddate - - Dump all unified audit logs from the defined subset - .EXAMPLE - Get-O365Light -startdate $startdate -enddate $enddate -RecordSet "AzureADOnly" -logfile "UnifiedAzureADOnly.log" - Dump AzureAD only operations since last week from the defined subset and write log to UnifiedAzureADOnly.log - #> - - param ( - - [Parameter(Mandatory = $true)] - [DateTime]$Enddate, - [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false)] - [ValidateSet("All","AllbutAzureAD","ExchangeOnly","OneDrive_Sharepoint_Teams_YammerOnly", "AzureADOnly", "SecurityAlerts")] - [String]$Operationsset = "All", - [Parameter(Mandatory = $false)] - [boolean]$MailboxLogin=$false, - [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, - [Parameter(Mandatory = $false)] - [String]$logfile = "Get-O365Light.log" - ) - - "Getting EXO Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName - $currentpath = (get-location).path - $o365existing = Get-PSSession | where-object{$_.ComputerName -eq "outlook.office365.com"} - if($o365existing){ - "Detected existing EXO session - removing and sleeping for session tear down" | Write-Log -LogPath $logfile -LogLevel "Warning" - $o365existing | remove-pssession -confirm:$false - start-sleep -seconds 15 - } - - $Alloperations= @() - - $myObject = [PSCustomObject]@{ - GroupName= "OneDrive_Sharepoint_Teams_Yammer"; - Operations = '"TeamsTenantSettingChanged", "TeamSettingChanged", "Set-CSTeamsAppPermissionPolicy", "New-CSTeamsAppPermissionPolicy", "AppInstalled","FileMalwareDetected", "SupervisorAdminToggled", "NetworkSecurityConfigurationUpdated", "SoftDeleteSettingsUpdated", "SiteCollectionAdminAdded", "NetworkAccessPolicyChanged", "GeoAdminAdded", "SharingPolicyChanged", "DeviceAccessPolicyChanged", "AddedToGroup", "PermissionLevelModified", "AnonymousLinkCreated", "AnonymousLinkUsed","SharingInvitationAccepted" , "SharingInvitationBlocked" , "UnmanagedSyncClientBlocked", "MemberRoleChanged"' - } - - $Alloperations += $myObject - - - $myObject = [PSCustomObject]@{ - GroupName= "AzureAD"; - Operations = '"Register connector","Verify domain","Add verified domain","Remove verified domain","Disable Desktop Sso for a specific domain","Add application","Add app role assignment to service principal","Update application","Update application – Certificates and secrets management","Update application – Certificates and secrets management ","Add delegated permission grant","Add OAuth2PermissionGrant","Add unverified domain","Add group", "Add member to group", "Delete group", "Remove member from group", "Update group","Consent to application", "Add app role assignment grant to user", "Add delegation entry", "Add service principal", "Add service principal credentials", "Remove delegation entry", "Remove service principal", "Remove service principal credentials", "Set delegation entry", "Add member to role", "Remove member from role", "Add app role assignment grant to user", "New-ConditionalAccessPolicy", "Set-AdminAuditLogConfig", "Set-ConditionalAccessPolicy", "Update domain", "Set federation settings on domain", "Set domain authentication", "Add partner to company", "Add domain to company"' - } - - $Alloperations += $myObject - - if($MailboxLogin -eq $true) - { - - Write-Host "Retrieving MailboxLogin operations, If mailbox auditing is enabled beware that you might exceed the threshold of 50.000 Exchange Online operations results per search" -ForegroundColor "Yellow" -BackgroundColor "Black" - write-host "Continue? (Y/N) " - $response = read-host - if ( $response -ne "Y" ) { exit } - "Retrieving MailboxLogin operations, If mailbox auditing is enabled beware that you might exceed the threshold of 50.000 Exchange Online operations results per search" | Write-Log -LogPath $logfile -LogLevel "Warning" - $myObject = [PSCustomObject]@{ - GroupName= "Exchange"; - Operations = '"Add-MailboxPermission", "AddFolderPermissions", "Add-RecipientPermission", "Remove-RecipientPermission", "New-InboxRule", "Set-InboxRule", "Set-TransportRule", "New-TransportRule", "Hard Delete user", "Remove-MailboxPermission", "RemoveFolderPermissions", "UpdateInboxRules", "Set-CASMailbox", "Set-Mailbox","SearchCreated", "SearchExported","MailboxLogin"' - } - } - else - { - $myObject = [PSCustomObject]@{ - GroupName= "Exchange"; - Operations = '"Add-MailboxPermission", "AddFolderPermissions", "Add-RecipientPermission", "Remove-RecipientPermission", "New-InboxRule", "Set-InboxRule", "Set-TransportRule", "New-TransportRule", "Hard Delete user", "Remove-MailboxPermission", "RemoveFolderPermissions", "UpdateInboxRules", "Set-CASMailbox", "Set-Mailbox","SearchCreated", "SearchExported"' - } - } - $Alloperations += $myObject - - $myObject = [PSCustomObject]@{ - GroupName= "SecurityAlerts"; - Operations = '"AlertEntityGenerated", "AlertTriggered"' - } - - $Alloperations += $myObject - - if($Operationsset -eq "All") - { - $Operationstoprocess = $Alloperations - "Fetching all operations from the subset, this is the default configuration" | Write-Log -LogPath $logfile - } - Elseif($Operationsset -eq "AllbutAzureAD") - { - $Operationstoprocess = $Alloperations | where-object{$_.GroupName -ne "AzureAD"} - "Fetching all operations from the subset, except Azure AD related operations" | Write-Log -LogPath $logfile - } - Elseif($Operationsset -eq "ExchangeOnly") - { - $Operationstoprocess = $Alloperations | where-object{$_.GroupName -eq "Exchange"} - "Fetching only Exchange Online operations from the subset" | Write-Log -LogPath $logfile - } - Elseif($Operationsset -eq "OneDrive_Sharepoint_Teams_YammerOnly") - { - $Operationstoprocess = $Alloperations | where-object{$_.GroupName -eq "OneDrive_Sharepoint_Teams_Yammer"} - "Fetching only Onedrive, SharePoint, Teams and Yammer operations from the subset" | Write-Log -LogPath $logfile - } - Elseif($Operationsset -eq "SecurityAlerts") - { - $Operationstoprocess = $Alloperations | where-object{$_.GroupName -eq "SecurityAlerts"} - "Fetching Security Alerts operations from the subset" | Write-Log -LogPath $logfile - } - - Elseif($Operationsset -eq "AzureADOnly") - { - $Operationstoprocess = $Alloperations | where-object{$_.GroupName -eq "AzureAD"} - "Fetching only Azure AD operations from the subset" | Write-Log -LogPath $logfile - } - - - $Launchsearch = - { - Param($app, $user, $newstartdate, $newenddate, $Operationstoprocess,$currentpath) - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $logfile = $currentpath + "\UnifiedAudit" + $datetoprocess + ".log" - $unifiedauditfolder = $currentpath + "\O365_unified_audit_logs" - if ((Test-Path $unifiedauditfolder) -eq $false){New-Item $unifiedauditfolder -Type Directory} - "Processing O365 logs for day $($datetoprocess)"| Write-Log -LogPath $logfile - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - $tenant = ($token.Account.UserName).split("@")[1] - $outputdate = "{0:yyyy-MM-dd}" -f ($datetoprocess) - $foldertoprocess = $unifiedauditfolder + "\" + $datetoprocess - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory} - - $outputfile = $foldertoprocess + "\UnifiedAuditLog_" + $tenant + "_" + $outputdate + ".json" - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile - - foreach($operationsset in $Operationstoprocess) - { - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - "Refreshing token - valid till " + $token.ExpiresOn.LocalDateTime.Tostring() | Write-Log -LogPath $logfile - - try { - $trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -operations $operationsset.Operations -ResultSize 1 -ErrorAction Stop - - } - catch { - "Retrieving Unified audit logs failed, rebuilding EXO session " | Write-Log -LogPath $logfile -LogLevel "Warning" - Get-PSSession | Remove-PSSession -Confirm:$false - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - Start-Sleep -Seconds 15 - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile - $trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -operations $operationsset.Operations -ResultSize 1 - } - if($trysearch) - { - $countobjects = $trysearch.ResultCount - "Dumping $($countobjects) $($operationsset.GroupName) records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - if($countobjects -gt 50000) - { - "More than 50000 $($operationsset.GroupName) records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} - some records might be missing" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile -LogLevel "Warning" - } - $sessionName = [guid]::NewGuid().ToString() - Get-LargeUnifiedAuditLog -sessionName $sessionName -StartDate $newstartdate -EndDate $newenddate -operations $operationsset.Operations -outputfile $outputfile -logfile $logfile -requesttype "Operations" - } - } - - "Removing PSSession and sleeping 10 seconds for session tear down" | Write-Log -LogPath $logfile - Get-PSSession | Remove-PSSession -Confirm:$false - Start-Sleep -Seconds 10 - } - - -$totaltimespan = (New-TimeSpan -Start $StartDate -End $Enddate) - -if(($totaltimespan.hours -eq 0) -and ($totaltimespan.minutes -eq 0) -and ($totaltimespan.seconds -eq 0)) - {$totaldays = $totaltimespan.days - $totalloops = $totaldays - } -else - {$totaldays = $totaltimespan.days + 1 - $totalloops = $totaltimespan.days - } - - -Get-RSJob | Remove-RSJob -Force - -$token = Get-OAuthToken -Service EXO -silent $true -LoginHint $user -Logfile $logfile -$app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} -if($null -eq $app) -{ - "No token cache available for EXO service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} -} - -"Checking permissions for $($user)"| Write-Log -LogPath $logfile -$sessionName = "EXO_" + [guid]::NewGuid().ToString() -$void = Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile -try { - $trysearch = Search-UnifiedAuditLog -StartDate (get-date).adddays(-90) -EndDate (get-date) -RecordType $recordtype -ResultSize 1 - -} -catch { - $errormessage = $_.Exception.Message - if ($errormessage -like "*The term 'Search-UnifiedAuditLog'*") { - "$user does not have the required permissions to get Office 365 Unified Audit Logs : doees not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. See https://learn.microsoft.com/en-us/purview/audit-log-search?view=o365-worldwide#before-you-search-the-audit-log. Cannot continue" | Write-Error - "$user does not have the required permissions to get Office 365 Unified Audit Logs : doees not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. See https://learn.microsoft.com/en-us/purview/audit-log-search?view=o365-worldwide#before-you-search-the-audit-log. Cannot continue" | Write-Log -LogPath $logfile -LogLevel "Error" - exit - } -} - -For ($d=0; $d -le $totalloops ; $d++) -{ - if($d -eq 0) - { - $newstartdate = $StartDate - $newenddate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newstartdate.AddDays(1))) - } - elseif($d -eq $totaldays) - { - $newenddate = $Enddate - $newstartdate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newenddate)) - } - else { - $newstartdate = $newenddate - $newenddate = $newenddate.AddDays(+1) - } - - $token = Get-OAuthToken -Service EXO -silent $true -LoginHint $user -Logfile $logfile - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - if($null -eq $app) - { - "No token cache available for EXO service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - } - "Lauching job number $($d) with startdate {0:yyyy-MM-dd} {0:HH:mm:ss} and enddate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $jobname = "UnifiedAudit" + $datetoprocess - - Start-RSJob -Name $jobname -ScriptBlock $Launchsearch -FunctionsToImport Connect-EXOPsearchUnified, write-log, Get-LargeUnifiedAuditLog -ArgumentList $app, $user, $newstartdate, $newenddate, $Operationstoprocess , $currentpath - - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 3) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } - $jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} - if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } - } - - - } - - #Waiting for final jobs to complete - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 1) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } -$jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} -if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } - } - -} diff --git a/DFIR-O365RC/Manage-Applications.ps1 b/DFIR-O365RC/Manage-Applications.ps1 new file mode 100755 index 0000000..815cb19 --- /dev/null +++ b/DFIR-O365RC/Manage-Applications.ps1 @@ -0,0 +1,720 @@ +function Add-OrganizationPermissions { + + <# + .SYNOPSIS + The Add-OrganizationPermissions function is the inner function that handles adding the application to the Azure DevOps organizations + #> + + param ( + [Parameter(Mandatory = $true)] + [String]$servicePrincipalId, + [String]$logFile = "Add-OrganizationPermissions.log" + ) + + Write-Warning "Please log in to Entra ID using a privileged account which has access to the targeted organizations" + Connect-AzUser -logFile $logFile + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + + $tenantId = (Get-AzTenant).Id + $azureDevOpsOrganizationsRaw = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Method Get -ContentType "application/json" -ErrorAction Stop -Uri "https://aexprodweu1.vsaex.visualstudio.com/_apis/EnterpriseCatalog/Organizations?tenantId=$tenantId" + $azureDevOpsOrganizationsNameAndId = $azureDevOpsOrganizationsRaw | ConvertFrom-CSV | ForEach-Object {$_ | Select-Object "Organization Name", "Organization Id"} + + Write-Host "Your account has access to the following organizations:" + "Your account has access to the following organizations:" | Write-Log -LogPath $logFile + $azureDevOpsOrganizationsNameAndId | Out-Host + $azureDevOpsOrganizationsNameAndId | Write-Log -LogPath $logFile + $choice = Read-Host "Do you want to collect logs for all [a], specific [s] or no [N] organization ? [a/s/N]" + if ($choice.ToUpper() -eq "A" -or $choice.ToUpper() -eq "S"){ + if ($choice.ToUpper() -eq "S"){ + [System.Collections.ArrayList]$wantedOrganizationsNameAndId = @{} + $read = $True + Write-Host "Leave Blank and press 'Enter' to Stop" + while ($read){ + $potentialOrganizationId = Read-Host "Please enter the organization IDs, one by one, and press 'Enter'" + if ($potentialOrganizationId){ + $selectedInput = $azureDevOpsOrganizationsNameAndId | Where-Object {$_."Organization Id" -eq $potentialOrganizationId} + if ($null -ne $selectedInput){ + $wantedOrganizationsNameAndId.Add($selectedInput) | Out-Null + Write-Host "Added $potentialOrganizationId" + } + else { + Write-Warning "Invalid organization ID, please try again" + } + } + else { + $read = $False + } + } + } + elseif ($choice.ToUpper() -eq "A"){ + $wantedOrganizationsNameAndId = $azureDevOpsOrganizationsNameAndId + } + + foreach ($organization in $wantedOrganizationsNameAndId){ + $organizationName = $organization.'Organization Name' + Write-Host "Adding service principal to organization $organizationName ($($organization.'Organization Id'))" + "Adding service principal to organization $organizationName ($($organization.'Organization Id'))" | Write-Log -LogPath $logFile + $isSuccess = (Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Method POST -ContentType "application/json" -ErrorAction Stop -Uri "https://vsaex.dev.azure.com/$organizationName/_apis/serviceprincipalentitlements?api-version=7.1-preview.1" -Body "{`"accessLevel`": {`"accountLicenseType`": `"stakeholder`"},`"servicePrincipal`": {`"origin`": `"aad`",`"originId`": `"$servicePrincipalId`",`"subjectKind`": `"servicePrincipal`"}}").isSuccess + while ($isSuccess -ne "True"){ + try { + $isSuccess = (Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Method POST -ContentType "application/json" -ErrorAction Stop -Uri "https://vsaex.dev.azure.com/$organizationName/_apis/serviceprincipalentitlements?api-version=7.1-preview.1" -Body "{`"accessLevel`": {`"accountLicenseType`": `"stakeholder`"},`"servicePrincipal`": {`"origin`": `"aad`",`"originId`": `"$servicePrincipalId`",`"subjectKind`": `"servicePrincipal`"}}").isSuccess + } + catch { + Write-Warning "Service principal is not yet available to $organizationName ($($organization.'Organization Id'))" + "Service principal is not yet available to $organizationName ($($organization.'Organization Id'))" | Write-Log -LogPath $logFile -LogLevel Warning + Start-Sleep -Seconds 1 + } + } + $securityNamespaces = Get-AzDevOpsRestAPIResponseUser -uri "https://dev.azure.com/$organizationName/_apis/securitynamespaces" -logFile $logFile + $auditLogServiceNamescapeId = $securityNamespaces | Where-Object {$_.displayName -eq "AuditLog"} | Select-Object -ExpandProperty namespaceId + $auditLogReadBit = $securityNamespaces | Where-Object {$_.displayName -eq "AuditLog"} | Select-Object -ExpandProperty actions | Where-Object {$_.name -eq "Read"} | Select-Object -ExpandProperty bit + $servicePrincipals = Get-AzDevOpsRestAPIResponseUser -uri "https://vssps.dev.azure.com/$organizationName/_apis/graph/serviceprincipals?api-version=7.1-preview.1" -logFile $logFile + $domain = $servicePrincipals | Where-Object {$_.originId -eq $servicePrincipalId} | Select-Object -ExpandProperty domain + $null = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Method POST -ContentType "application/json" -ErrorAction Stop -Uri "https://dev.azure.com/$organizationName/_apis/AccessControlEntries/$auditLogServiceNamescapeId" -Body "{`"token`":`"AllPermissions`",`"merge`":true,`"accessControlEntries`":[{`"descriptor`":`"Microsoft.VisualStudio.Services.Claims.AadServicePrincipal;$domain\\$servicePrincipalId`",`"allow`":$auditLogReadBit,`"deny`":0}]}" + } + Write-Host "Done assigning roles on organizations for the application" + "Done assigning roles on organizations for the application" | Write-Log -LogPath $logFile + } + else { + Write-Warning "No organization was selected" + "No organization was selected" | Write-Log -LogPath $logFile -LogLevel Warning + } +} + +function Add-SubscriptionPermissions { + + <# + .SYNOPSIS + The Add-SubscriptionPermissions function is the inner function that handles adding the application to the Azure Resource Manager subscriptions + #> + + param ( + [Parameter(Mandatory = $true)] + [String]$servicePrincipalId, + [String]$logFile = "Add-SubscriptionPermissions.log" + ) + + Write-Warning "Please log in to Entra ID using a privileged account which has access to the targeted subscriptions" + Connect-AzUser -logFile $logFile + + $subscriptionsNameAndId = Get-AzSubscription -ErrorAction Stop | Select-Object Name, Id + Write-Host "Your account has access to the following subscriptions:" + "Your account has access to the following subscriptions:" | Write-Log -LogPath $logFile + $subscriptionsNameAndId | Out-Host + $subscriptionsNameAndId | Write-Log -LogPath $logFile + $choice = Read-Host "Do you want to be able to collect logs for all [a], specific [s] or no [N] subscription ? [a/s/N]" + if ($choice.ToUpper() -eq "A" -or $choice.ToUpper() -eq "S"){ + $alreadyExistingCheckRoleDefinition = Get-AzRoleDefinition -Name "LogCollectionDFIRO365RC" -ErrorAction Stop -WarningAction:SilentlyContinue + if ($null -ne $alreadyExistingCheckRoleDefinition){ + $role = $alreadyExistingCheckRoleDefinition + } + else { + $role = Get-AzRoleDefinition "Reader" -ErrorAction Stop + $role.Id = $null + } + $role.Name = "LogCollectionDFIRO365RC" + $role.Description = "Can view activity logs" + $role.Actions.Clear() + $role.Actions.Add("Microsoft.Insights/eventtypes/*") + $role.AssignableScopes.Clear() + if ($choice.ToUpper() -eq "S"){ + [System.Collections.ArrayList]$wantedSubscriptionsNameAndId = @{} + $read = $True + Write-Host "Leave Blank and press 'Enter' to Stop" + while ($read){ + $potentialSubscriptionId = Read-Host "Please enter the subscription IDs, one by one, and press 'Enter'" + if ($potentialSubscriptionId){ + $selectedInput = $subscriptionsNameAndId | Where-Object {$_.Id -eq $potentialSubscriptionId} + if ($null -ne $selectedInput){ + $wantedSubscriptionsNameAndId.Add($selectedInput) | Out-Null + Write-Host "Added $potentialSubscriptionId" + } + else { + Write-Warning "Invalid subscription ID, please try again" + } + } + else { + $read = $False + } + } + } + elseif ($choice.ToUpper() -eq "A"){ + $wantedSubscriptionsNameAndId = $subscriptionsNameAndId + } + foreach ($subscription in $wantedSubscriptionsNameAndId){ + Write-Host "Adding subscription $($subscription.Name) ($($subscription.Id)) to the list of scopes" + "Adding subscription $($subscription.Name) ($($subscription.Id)) to the list of scopes" | Write-Log -LogPath $logFile + $role.AssignableScopes.Add("/subscriptions/$($subscription.Id)") + } + if ($role.AssignableScopes.length -gt 0){ + if ($null -ne $alreadyExistingCheckRoleDefinition){ + $roleDefinition = Set-AzRoleDefinition -Role $role -ErrorAction Stop + } + else { + $roleDefinition = New-AzRoleDefinition -Role $role -ErrorAction Stop + } + } + else { + Write-Warning "No subscription was selected" + "No subscription was selected" | Write-Log -LogPath $logFile -LogLevel Warning + } + + foreach ($subscription in $wantedSubscriptionsNameAndId){ + $alreadyExistingCheckRoleAssignement = Get-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionId $roleDefinition.Id -Scope "/subscriptions/$($subscription.Id)" -ErrorAction Stop + if ($null -eq $alreadyExistingCheckRoleAssignement){ + Write-Host "Assigning role for scope $($subscription.Name) ($($subscription.Id))" + "Assigning role for scope $($subscription.Name) ($($subscription.Id))" | Write-Log -LogPath $logFile + $null = New-AzRoleAssignment -ObjectId $servicePrincipalId -Scope "/subscriptions/$($subscription.Id)" -RoleDefinitionId $roleDefinition.Id -ErrorAction Stop + } + else { + Write-Host "Scope $($subscription.Name) ($($subscription.Id)) is already assigned" + "Scope $($subscription.Name) ($($subscription.Id)) is already assigned" | Write-Log -LogPath $logFile + } + } + Write-Host "Done assigning roles on subscriptions for the application" + "Done assigning roles on subscriptions for the application" | Write-Log -LogPath $logFile + } + else { + Write-Warning "You have not selected any subscription" + "You have not selected any subscription" | Write-Log -LogPath $logFile -LogLevel Warning + } +} + +function Update-Application { + <# + .SYNOPSIS + The Update-Application function will update an existing application to add a certificate. + The "-organizations" switch will allow the application to collect logs from Azure DevOps organizations. + The "-subscriptions" switch will allow the application to collect logs from Azure Resource Manager subscriptions + + .EXAMPLE + + PS C:\>$certificateb64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes("example.der")) + PS C:\>New-Application -certificateb64 $certificateb64 + Update the application to add a certificate ("example.der"). + + PS C:\>$certificateb64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes("example.der")) + PS C:\>New-Application -certificateb64 $certificateb64 -organizations -subscriptions + Update the application to add a certificate ("example.der") and access to Azure DevOps organizations and Azure Resource Manager subscriptions. + #> + + param ( + [Parameter(Mandatory = $false)] + [String]$certificateb64, + [Parameter(Mandatory = $false)] + [Switch]$subscriptions, + [Parameter(Mandatory = $false)] + [Switch]$organizations, + [String]$logFile = "Update-Application.log" + ) + + $currentPath = (Get-Location).path + $logFile = $currentPath + "\" + $logFile + + Connect-MicrosoftGraphUser -logFile $logFile + + Write-Host "Check for already existing DFIR-O365 applications" + "Check for already existing DFIR-O365 applications" | Write-Log -LogPath $logFile + $alreadyExistingCheck = Get-MgApplication -Filter "startswith(DisplayName,'LogCollectionDFIRO365RC_')" -ErrorAction Stop + if ($alreadyExistingCheck){ + if ($alreadyExistingCheck.length -gt 1){ + Write-Error $alreadyExistingCheck.length + " LogCollectionDFIRO365RC applications are present. Please delete all but one existing applications" + $alreadyExistingCheck.length + " LogCollectionDFIRO365RC applications are present" | Write-Log -LogPath $logFile -LogLevel Error + } + else { + $applicationName = "$($alreadyExistingCheck.DisplayName)" + Write-Host "A LogCollectionDFIRO365RC application already exists: $applicationName" + "A LogCollectionDFIRO365RC application already exists: $applicationName" | Write-Log -LogPath $logFile + if ("" -ne $certificateb64){ + $confirmation = Read-Host "Do you want to add the provided certificate to the application $applicationName ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Write-Host "Loading the certificate" + "Loading the certificate" | Write-Log -LogPath $logFile + try { + $rawCertificate = [Convert]::FromBase64String($certificateb64) + $X509certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($rawCertificate) + Write-Host "Adding the provided certificate to the existing application" + "Adding the provided certificate to the existing application" | Write-Log -LogPath $logFile + $keyCreds = @{ + Type = "AsymmetricX509Cert"; + Usage = "Verify"; + key = $rawCertificate; + startDateTime = $X509certificate.NotBefore; + endDateTime = $X509certificate.NotAfter; + displayName = $(New-Guid).Guid; + } + $alreadyExistingCheck.KeyCredentials += $keyCreds + Update-MgApplication -ApplicationId $alreadyExistingCheck.Id -KeyCredentials $alreadyExistingCheck.KeyCredentials + } + catch { + Write-Warning "Error loading and adding the new certificate. - $($_.Exception.Message)" + "Error loading and adding the new certificate. - $($_.Exception.Message)" | Write-Log -LogPath $logFile -LogLevel Warning + } + } + else { + Write-Warning "Not adding the provided certificate to the existing application" + "Not adding the provided certificate to the existing application" | Write-Log -LogPath $logFile -LogLevel Warning + } + } + $alreadyExistingCheck = Get-MgServicePrincipal -ErrorAction Stop | Where-Object { $_.DisplayName.StartsWith("LogCollectionDFIRO365RC_") } + if ($alreadyExistingCheck){ + if ($alreadyExistingCheck.length -gt 1){ + Write-Warning $alreadyExistingCheck.length + " LogCollectionDFIRO365RC_ service principals are present. Please delete all but one existing service principals" + $alreadyExistingCheck.length + " LogCollectionDFIRO365RC_ service principals are present" | Write-Log -LogPath $logFile -LogLevel Warning + } + else { + if ($subscriptions){ + Add-SubscriptionPermissions -servicePrincipalId $alreadyExistingCheck.Id -logFile $logFile + } + if ($organizations){ + Add-OrganizationPermissions -servicePrincipalId $alreadyExistingCheck.Id -logFile $logFile + } + } + } + } + } + else { + Write-Error "No application was found. Please call New-Application to create an application instead" + "No application was found. Please call New-Application to create an application instead" | Write-Log -LogPath $logFile -LogLevel Error + } +} + +function New-Application { + + <# + .SYNOPSIS + The New-Application function will create an application in Entra ID and corresponding service principals in Entra ID and Exchange Online, with the right permissions. This will be used to do the log collection. + The function will take as an input a base64 (public) certificate to import into the application. + The "-organizations" switch will allow the application to collect logs from Azure DevOps organizations. + The "-subscriptions" switch will allow the application to collect logs from Azure Resource Manager subscriptions. + + .EXAMPLE + + PS C:\>$certificateb64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes("example.der")) + PS C:\>New-Application -certificateb64 $certificateb64 + Create an application with the required permissions, with a certificate ("example.der"). + + PS C:\>$certificateb64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes("example.der")) + PS C:\>New-Application -certificateb64 $certificateb64 -organizations -subscriptions + Create an application with the required permissions, with a certificate ("example.der") and access to Azure DevOps organizations and Azure Resource Manager subscriptions. + #> + + param ( + [Parameter(Mandatory = $true)] + [String]$certificateb64, + [Parameter(Mandatory = $false)] + [Switch]$subscriptions, + [Parameter(Mandatory = $false)] + [Switch]$organizations, + [String]$logFile = "New-Application.log" + ) + + $currentPath = (Get-Location).path + $logFile = $currentPath + "\" + $logFile + $applicationName = "LogCollectionDFIRO365RC_" + $(New-Guid).Guid + + Connect-MicrosoftGraphUser -logFile $logFile + + $tenantPrincipalDomain = (Get-MgDomain | Where-Object { $_.Isdefault -eq $True }).Id + + Write-Host "Check for already existing DFIR-O365 applications" + "Check for already existing CF66J756VDFIR-O365 applications" | Write-Log -LogPath $logFile + $alreadyExistingCheck = Get-MgApplication -Filter "startswith(DisplayName,'LogCollectionDFIRO365RC_')" -ErrorAction Stop + if ($alreadyExistingCheck){ + Write-Error "A LogCollectionDFIRO365RC_* application already exists. Please call Update-Application instead" + "A LogCollectionDFIRO365RC_* application already exists. Please call Update-Application instead" | Write-Log -LogPath $logFile -LogLevel Error + } + else { + "Getting Entra ID permission 'Exchange.ManageAsApp' for 'Office 365 Exchange Online'" | Write-Log -LogPath $logFile + $exchangeApi = (Get-MgServicePrincipal -Filter "AppID eq '00000002-0000-0ff1-ce00-000000000000'" -ErrorAction Stop) + $exchangeManageAsAppPermission = $exchangeApi.AppRoles | Where-Object { $_.Value -eq 'Exchange.ManageAsApp' } + $exchangeRequiredAccess = @{ + ResourceAppId = $exchangeApi.AppId ; + ResourceAccess = @( + @{ + Id = $exchangeManageAsAppPermission.Id ; + Type = "Role" + } + ) + } + "Getting Entra ID permissions 'AuditLog.Read.All', 'AuditLogsQuery.Read.All', 'Application.Read.All', 'DelegatedPermissionGrant.Read.All', Device.Read.All' and 'Organization.Read.All' for 'Microsoft Graph'" | Write-Log -LogPath $logFile + $graphApi = (Get-MgServicePrincipal -Filter "AppID eq '00000003-0000-0000-c000-000000000000'" -ErrorAction Stop) + $graphAuditLogReadAll = $graphApi.AppRoles | Where-Object { $_.Value -eq 'AuditLog.Read.All' } + $graphAuditLogsQueryReadAll = $graphApi.AppRoles | Where-Object { $_.Value -eq 'AuditLogsQuery.Read.All' } + $graphApplicationReadAll = $graphApi.AppRoles | Where-Object { $_.Value -eq 'Application.Read.All' } + $graphDelegatedPermissionGrandReadAll = $graphApi.AppRoles | Where-Object { $_.Value -eq 'DelegatedPermissionGrant.Read.All' } + $graphDeviceReadAll = $graphApi.AppRoles | Where-Object { $_.Value -eq 'Device.Read.All' } + $graphOrganizationReadAll = $graphApi.AppRoles | Where-Object { $_.Value -eq 'Organization.Read.All' } + $graphRequiredAccess = @{ + ResourceAppId = $graphApi.AppId ; + ResourceAccess = @( + @{ + Id = $graphAuditLogReadAll.Id ; + Type = "Role" + }, + @{ + Id = $graphAuditLogsQueryReadAll.Id ; + Type = "Role" + }, + @{ + Id = $graphApplicationReadAll.Id ; + Type = "Role" + }, + @{ + Id = $graphDelegatedPermissionGrandReadAll.Id ; + Type = "Role" + }, + @{ + Id = $graphDeviceReadAll.Id ; + Type = "Role" + }, + @{ + Id = $graphOrganizationReadAll.Id ; + Type = "Role" + } + ) + } + + if ($null -eq $exchangeApi){ + Write-Warning "Application 'Office 365 Exchange Online' could not be found in your tenant. You won't be able to use the Get-O365' functions" + "Application 'Office 365 Exchange Online' could not be found in your tenant. You won't be able to use the Get-O365' functions" | Write-Log -LogPath $logFile -LogLevel Warning + } + + Write-Host "Creating application $applicationName with the required permissions" + "Creating application $applicationName with the required permissions" | Write-Log -LogPath $logFile + if ($null -eq $exchangeApi){ + if ($null -ne $graphApi){ + $myApp = New-MgApplication -DisplayName $applicationName -RequiredResourceAccess $graphRequiredAccess -ErrorAction Stop + } + else { + $myApp = New-MgApplication -DisplayName $applicationName -ErrorAction Stop + } + } + else { + if ($null -ne $graphApi){ + $myApp = New-MgApplication -DisplayName $applicationName -RequiredResourceAccess $exchangeRequiredAccess,$graphRequiredAccess -ErrorAction Stop + } + else { + $myApp = New-MgApplication -DisplayName $applicationName -RequiredResourceAccess $exchangeRequiredAccess -ErrorAction Stop + } + } + + "Creating service principal" | Write-Log -LogPath $logFile + $mySP = New-MgServicePrincipal -AppId $myApp.AppID -ErrorAction Stop + + Write-Host "Loading the certificate" + "Loading the certificate" | Write-Log -LogPath $logFile + try { + $rawCertificate = [Convert]::FromBase64String($certificateb64) + $X509certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($rawCertificate) + Write-Host "Adding credentials to the application" + "Adding credentials to the application" | Write-Log -LogPath $logFile + $keyCreds = @{ + Type = "AsymmetricX509Cert"; + Usage = "Verify"; + key = $rawCertificate; + startDateTime = $X509certificate.NotBefore; + endDateTime = $X509certificate.NotAfter; + displayName = $(New-Guid).Guid; + } + Update-MgApplication -ApplicationId $myApp.Id -KeyCredentials $keyCreds + } + catch { + Write-Warning "Error loading and adding the certificate. The application will be created, but you will need to call Update-Certificate to add the certificate to the application (or do it using the GUI). - $($_.Exception.Message)" + "Error loading and adding the certificate. The application will be created, but you will need to call Update-Certificate to add the certificate to the application (or do it using the GUI). - $($_.Exception.Message)" | Write-Log -LogPath $logFile -LogLevel Warning + } + + $tenantID = (Get-MgOrganization -ErrorAction Stop).Id + Write-Host "Sleeping 30 seconds for the application to be correctly deployed" + Start-Sleep -Seconds 30 + $consentURL = "https://login.microsoftonline.com/$tenantID/adminconsent?client_id=$($myApp.AppId)" + Write-Warning "Please use a web browser to open the page $consentURL and do an admin consent for the application (error AADSTS500113 is expected after the consent)" + "Displaying the URI for the admin consent" | Write-Log -LogPath $logFile + $hasConsented = $False + while (-not $hasConsented){ + $roleAssignements = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $mySP.Id + if ($roleAssignements){ + if ($roleAssignements.length -gt 1){ + foreach ($roleAssignement in $roleAssignements){ + if ($roleAssignement.AppRoleId -eq $graphAuditLogReadAll.Id){ + Write-Host "Admin consent: done" + "Admin consent: done" | Write-Log -LogPath $logFile + $hasConsented = $True + } + } + } + else { + if ($roleAssignements.AppRoleId -eq $graphAuditLogReadAll.Id){ + Write-Host "Admin consent: done" + "Admin consent: done" | Write-Log -LogPath $logFile + $hasConsented = $True + } + } + } + Start-Sleep -Seconds 1 + } + + if ($subscriptions){ + Add-SubscriptionPermissions -servicePrincipalId $mySP.Id -logFile $logFile + } + + if ($organizations){ + Add-OrganizationPermissions -servicePrincipalId $mySP.Id -logFile $logFile + } + + Connect-ExchangeOnlineUser -logFile $logFile + + try { + $exoSP = New-ServicePrincipal -AppId $myApp.AppId -ObjectId $mySP.Id -DisplayName $applicationName -ErrorAction Stop + } + catch { + $_.Exception.Message | Write-Log -LogPath $logFile -LogLevel Error + Write-Warning "Can't create Service Principal in Exchange Online. Please check that you have access to Exchange Online using PowerShell. You won't be able to use Get-O365' functions. Exiting" + "Can't create Service Principal in Exchange Online. Please check that you have access to Exchange Online using PowerShell. You won't be able to use Get-O365' functions. Exiting" | Write-Log -LogPath $logFile -LogLevel Warning + Write-Host "Done creating the application with some of the required permissions" + Write-Host "Please use the following identifiers: " + Write-Warning "AppID: $($myApp.AppID)" + "AppID: $($myApp.AppID)" | Write-Log -LogPath $logFile + Write-Warning "Tenant: $tenantPrincipalDomain" + "Tenant: $tenantPrincipalDomain" | Write-Log -LogPath $logFile + exit + } + $roleGroupName = $applicationName + "_RG" + try { + $null = New-RoleGroup -Name $roleGroupName -Roles "View-only audit logs" -Members $exoSP.Id -ErrorAction Stop + Write-Host "Done creating the application with the required permissions" + Write-Host "Please use the following identifiers: " + Write-Warning "AppID: $($myApp.AppID)" + "AppID: $($myApp.AppID)" | Write-Log -LogPath $logFile + Write-Warning "Tenant: $tenantPrincipalDomain" + "Tenant: $tenantPrincipalDomain" | Write-Log -LogPath $logFile + } + catch { + $_.Exception.Message | Write-Log -LogPath $logFile -LogLevel Error + Write-Warning "Organization Customization was not enabled on this tenant. Enabling it" + "Organization Customization was not enabled on this tenant. Enabling it" | Write-Log -LogPath $logFile -LogLevel Warning + Enable-OrganizationCustomization + Write-Warning "Organization Customization was enabled. It may take up to 4 hours before being propagated. When it is propagated, please call Remove-Application and New-Application again" + } + } +} + +function Remove-Application { + + <# + .SYNOPSIS + The Remove-Application function will delete every application, service principal and role groups which were created in Entra ID and Exchange Online for DFIR-O365RC. + The "-organizations" switch will delete the application from Azure DevOps organizations. + The "-subscriptions" switch will delete the application from Azure Resource Manager subscriptions. + + .EXAMPLE + + PS C:\>Remove-Application + Deletes every application, service principal and role groups which were created in Entra ID and Exchange Online for DFIR-O365RC. + + PS C:\>Remove-Application -organizations -subscriptions + Deletes every application, service principal and role groups which were created in Entra ID, Exchange Online, Azure DevOps organizations and Azure Resource Manager subscriptions for DFIR-O365RC. + + #> + + param ( + [Parameter(Mandatory = $false)] + [Switch]$subscriptions, + [Parameter(Mandatory = $false)] + [Switch]$organizations, + [String]$logFile = "Remove-Application.log" + ) + + $currentPath = (Get-Location).path + $logFile = $currentPath + "\" + $logFile + + Connect-MicrosoftGraphUser -logFile $logFile + + Write-Host "Check for already existing DFIR-O365 applications" + "Check for already existing DFIR-O365 applications" | Write-Log -LogPath $logFile + $alreadyExistingCheck = Get-MgApplication -Filter "startswith(DisplayName,'LogCollectionDFIRO365RC_')" -ErrorAction Stop + if ($alreadyExistingCheck){ + if ($alreadyExistingCheck.length -gt 1){ + foreach ($application in $alreadyExistingCheck){ + Write-Host "Removing application: $($application.DisplayName)" + "Removing application: $($application.DisplayName)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-MgApplication -ApplicationId $application.Id -Confirm:$false + } + } + } + else { + Write-Host "Removing application: $($alreadyExistingCheck.DisplayName)" + "Removing application: $($alreadyExistingCheck.DisplayName)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-MgApplication -ApplicationId $alreadyExistingCheck.Id -Confirm:$false + } + } + } + + if ($subscriptions -or $organizations){ + Write-Warning "Please log in to Entra ID using a privileged account which has access to the targeted subscriptions / organizations" + Connect-AzUser -logFile $logFile + if ($subscriptions){ + Write-Host "Check for already existing DFIR-O365 Entra ID role assignments for role LogCollectionDFIRO365RC" + "Check for already existing DFIR-O365 Entra ID role assignments for role LogCollectionDFIRO365RC" | Write-Log -LogPath $logFile + try { + $alreadyExistingCheckRoleAssignement = Get-AzRoleAssignment -RoleDefinitionName "LogCollectionDFIRO365RC" -ErrorAction Stop + } + catch { + $errorMessage = $_.Exception.Message + if ($errormessage -like "*No subscription was found in the default profile*"){ + Write-Warning "No subsriptions were found" + "No subsriptions were found" | Write-Log -LogPath $logFile -LogLevel Warning + } + else { + Write-Error $errorMessage + $errorMessage | Write-Log -LogPath $logFile -LogLevel Error + } + $alreadyExistingCheckRoleAssignement = $null + } + if ($alreadyExistingCheckRoleAssignement){ + if ($alreadyExistingCheckRoleAssignement.length -gt 1){ + foreach ($roleAssignement in $alreadyExistingCheckRoleAssignement){ + Write-Host "Removing LogCollectionDFIRO365RC role assignement for object ID: $($roleAssignement.ObjectId)" + "Removing LogCollectionDFIRO365RC role assignement for object ID: $($roleAssignement.ObjectId)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-AzRoleAssignment -RoleDefinitionName "LogCollectionDFIRO365RC" -ObjectId $roleAssignement.ObjectId + } + } + } + else { + Write-Host "Removing LogCollectionDFIRO365RC role assignement for object ID: $($alreadyExistingCheckRoleAssignement.ObjectId)" + "Removing LogCollectionDFIRO365RC role assignement for object ID: $($alreadyExistingCheckRoleAssignement.ObjectId)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-AzRoleAssignment -RoleDefinitionName "LogCollectionDFIRO365RC" -ObjectId $alreadyExistingCheckRoleAssignement.ObjectId + } + } + } + + Write-Host "Check for already existing DFIR-O365 Entra ID role definitions" + "Check for already existing DFIR-O365 Entra ID role definitions" | Write-Log -LogPath $logFile + try { + $alreadyExistingCheckRoleDefinition = Get-AzRoleDefinition -Name "LogCollectionDFIRO365RC" -ErrorAction Stop -WarningAction:SilentlyContinue + } + catch { + $errorMessage = $_.Exception.Message + if ($errormessage -like "No subscription was found in the default profile*"){ + Write-Warning "No subsriptions were found" + "No subsriptions were found" | Write-Log -LogPath $logFile -LogLevel Warning + } + else { + Write-Error $errorMessage + $errorMessage | Write-Log -LogPath $logFile -LogLevel Error + } + $alreadyExistingCheckRoleDefinition = $null + } + + if ($alreadyExistingCheckRoleDefinition){ + if ($alreadyExistingCheckRoleDefinition.length -gt 1){ + foreach ($roleDefinition in $alreadyExistingCheckRoleDefinition){ + Write-Host "Removing role definition LogCollectionDFIRO365RC, ID: $($roleDefinition.Id)" + "Removing role definition LogCollectionDFIRO365RC, ID: $($roleDefinition.Id)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-AzRoleDefinition -Id $roleDefinition.Id -Confirm:$false -Force + } + } + } + else { + Write-Host "Removing role definition LogCollectionDFIRO365RC, ID: $($alreadyExistingCheckRoleDefinition.Id)" + "Removing role definition LogCollectionDFIRO365RC, ID: $($alreadyExistingCheckRoleDefinition.Id)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-AzRoleDefinition -Id $alreadyExistingCheckRoleDefinition.Id -Confirm:$false -Force + } + } + } + } + + if ($organizations){ + Write-Host "Check for already existing DFIR-O365 service principals in DevOps organizations" + "Check for already existing DFIR-O365 service principals in DevOps organizations" | Write-Log -LogPath $logFile + $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop + $tenantId = (Get-AzTenant).Id + $azureDevOpsOrganizationsRaw = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Method Get -ContentType "application/json" -ErrorAction Stop -Uri "https://aexprodweu1.vsaex.visualstudio.com/_apis/EnterpriseCatalog/Organizations?tenantId=$tenantId" + if ($null -ne $azureDevOpsOrganizationsRaw){ + $azureDevOpsOrganizationsNameAndId = $azureDevOpsOrganizationsRaw | ConvertFrom-CSV | ForEach-Object {$_ | Select-Object "Organization Name", "Organization Id"} + foreach ($organization in $azureDevOpsOrganizationsNameAndId){ + $organizationName = $organization.'Organization Name' + Write-Host "Checking presence of DFIR-O365RC Service Principals in $organizationName ($($organization.'Organization Id')) Azure DevOps organization" + "Checking presence of DFIR-O365RC Service Principals in $organizationName ($($organization.'Organization Id')) Azure DevOps organization" | Write-Log -LogPath $logFile + $servicePrincipals = Get-AzDevOpsRestAPIResponseUser -uri "https://vssps.dev.azure.com/$organizationName/_apis/graph/serviceprincipals?api-version=7.1-preview.1" -logFile $logFile + $servicePrincipalsToDelete = $servicePrincipals | Where-Object {$_.displayName -like "LogCollectionDFIRO365RC_*"} + foreach ($servicePrincipalToDelete in $servicePrincipalsToDelete){ + $storageKey = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Method GET -uri "https://vssps.dev.azure.com/$organizationName/_apis/graph/storagekeys/$($servicePrincipalToDelete.descriptor)?api-version=7.1-preview.1" -ErrorAction Stop + Write-Host "Deleting $($servicePrincipalToDelete.displayName) in $organizationName" + "Deleting $($servicePrincipalToDelete.displayName) in $organizationName" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + $null = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Method DELETE -Uri "https://vsaex.dev.azure.com/$organizationName/_apis/serviceprincipalentitlements/$($storageKey.value)?api-version=7.1-preview.1" -ErrorAction Stop + } + } + } + } + else { + Write-Error "Error while fetching Azure DevOps Organizations" + "Error while fetching Azure DevOps Organizations" | Write-Log -LogPath $logFile -LogLevel "ERROR" + } + } + } + + Connect-ExchangeOnlineUser -logFile $logFile + + Write-Host "Check for already existing DFIR-O365 service principals in Exchange Online" + "Check for already existing DFIR-O365 service principals in Exchange Online" | Write-Log -LogPath $logFile + $alreadyExistingCheck = Get-ServicePrincipal -ErrorAction Stop | Where-Object { $_.DisplayName.StartsWith("LogCollectionDFIRO365RC_") } + if ($alreadyExistingCheck){ + if ($alreadyExistingCheck.length -gt 1){ + foreach ($servicePrincipal in $alreadyExistingCheck){ + Write-Host "Removing service principal: $($servicePrincipal.DisplayName)" + "Removing service principal: $($servicePrincipal.DisplayName)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-ServicePrincipal -Id $servicePrincipal.ObjectId -Confirm:$false + } + } + } + else { + Write-Host "Removing service principal: $($alreadyExistingCheck.DisplayName)" + "Removing service principal: $($alreadyExistingCheck.DisplayName)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-ServicePrincipal -Id $alreadyExistingCheck.ObjectId -Confirm:$false + } + } + } + + Write-Host "Check for already existing DFIR-O365 role groups in Exchange Online" + "Check for already existing DFIR-O365 role groups in Exchange Online" | Write-Log -LogPath $logFile + $alreadyExistingCheck = Get-RoleGroup | Where-Object { $_.Name.StartsWith("LogCollectionDFIRO365RC_") } + if ($alreadyExistingCheck){ + if ($alreadyExistingCheck.length -gt 1){ + foreach ($roleGroup in $alreadyExistingCheck){ + Write-Host "Removing role group: $($roleGroup.Name)" + "Removing role group: $($roleGroup.Name)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-RoleGroup -Identity $roleGroup.Identity -Confirm:$false + } + } + } + else { + Write-Host "Removing role group: $($alreadyExistingCheck.Name)" + "Removing role group: $($alreadyExistingCheck.Name)" | Write-Log -LogPath $logFile + $confirmation = Read-Host "Continue ? [y/N]" + if ($confirmation.ToUpper() -eq "Y"){ + Remove-RoleGroup -Identity $alreadyExistingCheck.Identity -Confirm:$false + } + } + } +} \ No newline at end of file diff --git a/DFIR-O365RC/Search-O365.ps1 b/DFIR-O365RC/Search-O365.ps1 deleted file mode 100644 index f14fdba..0000000 --- a/DFIR-O365RC/Search-O365.ps1 +++ /dev/null @@ -1,307 +0,0 @@ -Function Search-O365 { - - <# - .SYNOPSIS - The Search-O365 function dumps in JSON files results of a freetext, IP or UserId search from the O365 Unified Audit Log for a specific time range. - - .EXAMPLE - PS C:\>$enddate = get-date - PS C:\>$startdate = $enddate.adddays(-90) - - PS C:\>Search-O365 -startdate $startdate -enddate $enddate -Freetext "Python" - - Search for Python user agent in unified audit logs - - .EXAMPLE - Search-O365 -startdate $startdate -enddate $enddate -IPAddresses X.X.X.X - Dump all the unified audit logs entries by the specified IP addresses. You specify multiple IP addresses separated by commas. - #> - - param ( - - [Parameter(Mandatory = $true)] - [DateTime]$Enddate, - [Parameter(Mandatory = $true)] - [DateTime]$StartDate, - [Parameter(Mandatory = $false, ParameterSetName="Freetext")] - [String]$Freetext, - [Parameter(Mandatory = $false, ParameterSetName="IPAddresses")] - [String]$IPAddresses, - [Parameter(Mandatory = $false, ParameterSetName="UserIds")] - [System.Array]$UserIds, - [Parameter(Mandatory = $false)] - [boolean]$DeviceCode=$false, - [Parameter(Mandatory = $false)] - [String]$logfile = "Search-O365.log" - ) - - if($Freetext) - { - $requesttype = "freetext" - $searchstring = $Freetext - "Searching freetext $($Freetext) in Unified audit logs" | Write-Log -LogPath $logfile - } - elseif($IPAddresses) - { - $requesttype = "IPAddresses" - $searchstring = $IPAddresses - "Searching IPAddresses $($IPAddresses) in Unified audit logs" | Write-Log -LogPath $logfile - } - elseif($UserIds) - { - $requesttype = "UserIds" - $searchstring = $UserIds - "Searching UserIds $($UserIds) in Unified audit logs" | Write-Log -LogPath $logfile - } - - "Getting EXO Oauth token" | Write-Log -LogPath $logfile - Clear-MsalTokenCache - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $user = $token.Account.UserName - $currentpath = (get-location).path - $o365existing = Get-PSSession | where-object{$_.ComputerName -eq "outlook.office365.com"} - if($o365existing){ - "Detected existing EXO session - removing and sleeping for session tear down" | Write-Log -LogPath $logfile -LogLevel "Warning" - $o365existing | remove-pssession -confirm:$false - start-sleep -seconds 15 - } - - - $Launchsearch = - { - Param($app, $user, $newstartdate, $newenddate, $requesttype, $searchstring, $currentpath) - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $logfile = $currentpath + "\UnifiedAudit" + $datetoprocess + ".log" - $unifiedauditfolder = $currentpath + "\O365_unified_audit_logs" - if ((Test-Path $unifiedauditfolder) -eq $false){New-Item $unifiedauditfolder -Type Directory} - "Processing O365 logs for day $($datetoprocess)"| Write-Log -LogPath $logfile - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - $tenant = ($token.Account.UserName).split("@")[1] - $outputdate = "{0:yyyy-MM-dd}" -f ($datetoprocess) - $actualdate = $(get-date -f yyyy-MM-dd-hh-mm-ss) - $foldertoprocess = $unifiedauditfolder + "\" + $datetoprocess - if ((Test-Path $foldertoprocess) -eq $false){New-Item $foldertoprocess -Type Directory} - - $outputfile = $foldertoprocess + "\UnifiedAuditLog_" + $tenant + "_" + $outputdate + "_" + $requesttype + "_" + $actualdate + ".json" - $commandNames = "Search-UnifiedAuditLog","Search-MailboxAuditLog" - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile -commandNames $commandNames - - - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - "Refreshing token - valid till " + $token.ExpiresOn.LocalDateTime.Tostring() | Write-Log -LogPath $logfile - - try { - if($requesttype -eq "freetext") - {$trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -FreeText $searchstring -ResultSize 1 -ErrorAction Stop} - elseif($requesttype -eq "IPAddresses") - {$trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -IPAddresses $searchstring -ResultSize 1 -ErrorAction Stop} - elseif($requesttype -eq "UserIds") - {$trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -UserIds $searchstring -ResultSize 1 -ErrorAction Stop} - } - catch { - "Retrieving Unified audit logs failed, rebuilding EXO session " | Write-Log -LogPath $logfile -LogLevel "Warning" - Get-PSSession | Remove-PSSession -Confirm:$false - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - Start-Sleep -Seconds 15 - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - $commandNames = "Search-UnifiedAuditLog","Search-MailboxAuditLog" - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile -commandNames $commandNames - if($requesttype -eq "freetext") - {$trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -FreeText $searchstring -ResultSize 1} - elseif($requesttype -eq "IPAddresses") - {$trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -IPAddresses $searchstring -ResultSize 1} - elseif($requesttype -eq "UserIds") - {$trysearch = Search-UnifiedAuditLog -StartDate $newstartdate -EndDate $newenddate -UserIds $searchstring -ResultSize 1} - } - if($trysearch) - { - $countobjects = $trysearch.ResultCount - "Dumping $($countobjects) UAL records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - if($countobjects -gt 50000) - { - "More than 50000 records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} - some records might be missing" -f ($newstarthour,$newendhour) | Write-Log -LogPath $logfile -LogLevel "Warning" - } - $sessionName = [guid]::NewGuid().ToString() - if($requesttype -eq "UserIds") - {Get-LargeUnifiedAuditLog -sessionName $sessionName -StartDate $newstartdate -EndDate $newenddate -searchtable $searchstring -outputfile $outputfile -logfile $logfile -requesttype $requesttype } - else { - Get-LargeUnifiedAuditLog -sessionName $sessionName -StartDate $newstartdate -EndDate $newenddate -searchstring $searchstring -outputfile $outputfile -logfile $logfile -requesttype $requesttype - } - - } - - $outputfile = $foldertoprocess + "\MailboxAuditLog_" + $tenant + "_" + $outputdate + "_" + $requesttype + "_" + $actualdate + ".json" - if($requesttype -eq "UserIds") - { - try { - Search-MailboxAuditLog -StartDate $newstartdate -EndDate $newenddate -Identity $searchstring[0] -LogonTypes Admin,Delegate,Owner -IncludeInactiveMailbox -ShowDetails -ResultSize 1 -ErrorAction Stop - } - catch { - if ($_.CategoryInfo.Reason -ne "ManagementObjectNotFoundException") - { - "Retrieving Mailbox audit logs failed, rebuilding EXO session" | Write-Log -LogPath $logfile -LogLevel "Warning" - Get-PSSession | Remove-PSSession -Confirm:$false - $token = Get-MsalToken -Silent -PublicClientApplication $app -LoginHint $user -Scopes "https://outlook.office365.com/.default" - Start-Sleep -Seconds 15 - $sessionName = "EXO_" + [guid]::NewGuid().ToString() - $commandNames = "Search-UnifiedAuditLog","Search-MailboxAuditLog" - Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile -commandNames $commandNames - } - } - "Dumping MailboxAudit records between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - Get-MailboxAuditLog -StartDate $newstartdate -EndDate $newenddate -outputfile $outputfile -logfile $logfile -UserIds $searchstring - } - - - "Removing PSSession and sleeping 10 seconds for session tear down" | Write-Log -LogPath $logfile - Get-PSSession | Remove-PSSession -Confirm:$false - Start-Sleep -Seconds 10 - } - - -$totaltimespan = (New-TimeSpan -Start $StartDate -End $Enddate) - -if(($totaltimespan.hours -eq 0) -and ($totaltimespan.minutes -eq 0) -and ($totaltimespan.seconds -eq 0)) - {$totaldays = $totaltimespan.days - $totalloops = $totaldays - } -else - {$totaldays = $totaltimespan.days + 1 - $totalloops = $totaltimespan.days - } - - -Get-RSJob | Remove-RSJob -Force - -$token = Get-OAuthToken -Service EXO -silent $true -LoginHint $user -Logfile $logfile -$app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} -if($null -eq $app) -{ - "No token cache available for EXO service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} -} - -"Checking permissions for $($user)"| Write-Log -LogPath $logfile -$sessionName = "EXO_" + [guid]::NewGuid().ToString() -$commandNames = "Search-UnifiedAuditLog","Search-MailboxAuditLog" -$void = Connect-EXOPsearchUnified -token $token -sessionName $sessionName -logfile $logfile -commandNames $commandNames -try { - $trysearch = Search-UnifiedAuditLog -StartDate (get-date).adddays(-90) -EndDate (get-date) -RecordType $recordtype -ResultSize 1 - -} -catch { - $errormessage = $_.Exception.Message - if ($errormessage -like "*The term 'Search-UnifiedAuditLog'*") { - "$user does not have the required permissions to get Office 365 Unified Audit Logs : doees not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. See https://learn.microsoft.com/en-us/purview/audit-log-search?view=o365-worldwide#before-you-search-the-audit-log. Cannot continue" | Write-Error - "$user does not have the required permissions to get Office 365 Unified Audit Logs : doees not have the 'View-Only Audit Logs' role on https://admin.exchange.microsoft.com/. See https://learn.microsoft.com/en-us/purview/audit-log-search?view=o365-worldwide#before-you-search-the-audit-log. Cannot continue" | Write-Log -LogPath $logfile -LogLevel "Error" - exit - } -} - - -For ($d=0; $d -le $totalloops ; $d++) -{ - if($d -eq 0) - { - $newstartdate = $StartDate - $newenddate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newstartdate.AddDays(1))) - } - elseif($d -eq $totaldays) - { - $newenddate = $Enddate - $newstartdate = get-date("{0:yyyy-MM-dd} 00:00:00.000" -f ($newenddate)) - } - else { - $newstartdate = $newenddate - $newenddate = $newenddate.AddDays(+1) - } - - $token = Get-OAuthToken -Service EXO -silent $true -LoginHint $user -Logfile $logfile - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - if($null -eq $app) - { - "No token cache available for EXO service asking for new token" | Write-Log -LogPath $logfile -LogLevel "Warning" - $token = Get-OAuthToken -Service EXO -Logfile $logfile -DeviceCode $DeviceCode - $app = Get-MsalClientApplication | Where-Object{$_.ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716"} - } - "Lauching job number $($d) with startdate {0:yyyy-MM-dd} {0:HH:mm:ss} and enddate {1:yyyy-MM-dd} {1:HH:mm:ss}" -f ($newstartdate,$newenddate) | Write-Log -LogPath $logfile - $datetoprocess = ($newstartdate.ToString("yyyy-MM-dd")) - $jobname = "UnifiedAudit" + $datetoprocess - - Start-RSJob -Name $jobname -ScriptBlock $Launchsearch -FunctionsToImport Connect-EXOPsearchUnified, write-log, Get-LargeUnifiedAuditLog,Get-MailboxAuditLog -ArgumentList $app, $user, $newstartdate, $newenddate, $requesttype, $searchstring , $currentpath - - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 3) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } - $jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} - if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } - } - - - } - - #Waiting for final jobs to complete - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - while($nbjobrunning -ge 1) - { - start-sleep -seconds 2 - $nbjobrunning = (Get-RSJob | where-object {$_.State -eq "running"} | Measure-Object).count - } - $jobsok = Get-RSJob | where-object {$_.State -eq "Completed"} - if($jobsok) - { - foreach($jobok in $jobsok) - { - "Runspace Job $($jobok.Name) finished - dumping log" | Write-Log -LogPath $logfile - $logfilename = $jobok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobok | remove-rsjob - "Runspace Job $($jobok.Name) finished - job removed" | Write-Log -LogPath $logfile - } - } -$jobsnok = Get-RSJob | where-object {$_.State -eq "Failed"} -if($jobsnok) - { - foreach($jobnok in $jobsnok) - { - "Runspace Job $($jobnok.Name) failed with error $($jobnok.Error)" | Write-Log -LogPath $logfile -LogLevel "Error" - "Runspace Job $($jobnok.Name) failed - dumping log" | Write-Log -LogPath $logfile -LogLevel "Error" - $logfilename = $jobnok.Name + ".log" - get-content $logfilename | out-file $logfile -Encoding UTF8 -append - remove-item $logfilename -confirm:$false -force - $jobnok | remove-rsjob - "Runspace Job $($jobnok.Name) failed - job removed" | Write-Log -LogPath $logfile -LogLevel "Error" - } - } - -} diff --git a/Dockerfile b/Dockerfile index 79f8dcf..19fdc43 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,17 @@ From mcr.microsoft.com/powershell RUN pwsh -command Set-PSRepository PSGallery -InstallationPolicy Trusted -RUN pwsh -command Install-Module PSWSMan -RUN pwsh -command Install-WSMan -RUN pwsh -command Install-Module MSAL.PS -AcceptLicense -RUN pwsh -command Install-Module PoshRSJob -RUN pwsh -command Install-Module ExchangeOnlineManagement +RUN pwsh -command Install-Module Az.Accounts -RequiredVersion 3.0.2 +RUN pwsh -command Install-Module Az.Monitor -RequiredVersion 5.2.1 +RUN pwsh -command Install-Module Az.Resources -RequiredVersion 7.2.0 +RUN pwsh -command Install-Module ExchangeOnlineManagement -RequiredVersion 3.5.1 +RUN pwsh -command Install-Module Microsoft.Graph.Authentication -RequiredVersion 2.20.0 +RUN pwsh -command Install-Module Microsoft.Graph.Applications -RequiredVersion 2.20.0 +RUN pwsh -command Install-Module Microsoft.Graph.Beta.Reports -RequiredVersion 2.20.0 +RUN pwsh -command Install-Module Microsoft.Graph.Beta.Security -RequiredVersion 2.20.0 +RUN pwsh -command Install-Module Microsoft.Graph.Identity.DirectoryManagement -RequiredVersion 2.20.0 +RUN pwsh -command Install-Module PoshRSJob -RequiredVersion 1.7.4.4 RUN mkdir -p /root/.config/powershell -RUN echo 'Write-Host -ForegroundColor Yellow "DFIR-O365RC: PowerShell module for Office 365 and Azure log collection"' > /root/.config/powershell/Microsoft.PowerShell_profile.ps1 +RUN echo 'Write-Host -ForegroundColor Yellow "DFIR-O365RC: PowerShell module for Microsoft 365 and Entra ID log collection"' > /root/.config/powershell/Microsoft.PowerShell_profile.ps1 RUN echo 'Write-Host -ForegroundColor Yellow "https://github.com/ANSSI-FR/DFIR-O365RC"' >> /root/.config/powershell/Microsoft.PowerShell_profile.ps1 ADD DFIR-O365RC /root/.local/share/powershell/Modules/DFIR-O365RC RUN pwsh -noprofile -command Import-Module DFIR-O365RC diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 5a4bbd7..6c2381c --- a/README.md +++ b/README.md @@ -1,328 +1,569 @@ + ![DFIR-O365RC](./logo.png) [![Docker Image CI](https://github.com/ANSSI-FR/DFIR-O365RC/actions/workflows/docker-image.yml/badge.svg)](https://github.com/ANSSI-FR/DFIR-O365RC/actions/workflows/docker-image.yml) --- ## Table of contents: -1. [Module description](#description) -2. [Installation and pre-requisites](#install) -3. [Roles and license requirements](#roleslic) -4. [Functions included in the module](#functions) -5. [Files generated](#files) +1. [Module description](#module-description) +2. [Installation and prerequisites](#installation-and-prerequisites) + 1. [Using Docker](#using-docker) + 2. [Manual Installation](#manual-installation) +3. [Managing the DFIR-O365RC application](#managing-the-dfir-o365rc-application) + 1. [Creating the application](#creating-the-application) + 2. [Updating the application](#updating-the-application) + 3. [Removing the application](#removing-the-application) + +4. [Permissions and license requirements](#permissions-and-license-requirements) +5. [Functions included in the module](#functions-included-in-the-module) +6. [Files generated](#files-generated) + + + +DFIR-O365RC was presented at SSTIC 2021 (Symposium sur la sécurité des technologies de l'information et des communications). Slides and a recording of the presentation, in French, are available [here](https://www.sstic.org/2021/presentation/collecte_de_journaux_office_365_avec_dfir-o365rc/). -DFIR-O365RC was presented at the SSTIC 2021 (Symposium sur la sécurité des technologies de l'information et des communications). Slides and a recording of the presentation, in french language, are available [here](https://www.sstic.org/2021/presentation/collecte_de_journaux_office_365_avec_dfir-o365rc/). +⚠️ On March 31, 2024, [Microsoft deprecated the authentication method](https://techcommunity.microsoft.com/t5/exchange-team-blog/mfa-app-id-deprecation-in-exchange-online/ba-p/4036067) we used for DFIR-O365RC. This led to the release of the version 2.0.0 in August 2024, with **breaking changes** regarding authentication and a global refactoring of the code. ⚠️ -## Module description -The DFIR-O365RC PowerShell module is a set of functions that allow the DFIR analyst to collect logs relevant for Office 365 Business Email Compromise and Azure investigations. + +## Module description + +The DFIR-O365RC PowerShell module is a set of functions that allow a forensic analyst to collect logs relevant for Microsoft 365 compromises and conduct Entra ID investigations. The logs are generated in JSON format and retrieved from two main data sources: -- Office 365 [Unified Audit Logs](https://docs.microsoft.com/en-us/microsoft-365/compliance/search-the-audit-log-in-security-and-compliance?view=o365-worldwide#search-the-audit-log). -- Azure AD [sign-ins logs](https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/concept-sign-ins) and [audit logs](https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/concept-audit-logs). +- Microsoft 365 [Unified Audit Log](https://learn.microsoft.com/en-us/purview/audit-search?tabs=microsoft-purview-portal) ; +- Microsoft Entra [sign-ins logs](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-sign-ins) and [audit logs](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-audit-logs). -The two data sources can be queried from different endpoints: +Those two data sources can be queried from different endpoints: -| **Data source / Endpoint** | **History** | **Performance** | **Scope** | **Pre-requisites (OS or Azure)** | -|---|---|---|---|---| -| Unified Audit Logs / [Exchange Online PowerShell](https://docs.microsoft.com/en-us/powershell/module/exchange/search-unifiedauditlog?view=exchange-ps) | 90 days | Poor | All Office 365 logs (Azure AD included) | None | -| Unified Audit Logs / [Office 365 Management API](https://docs.microsoft.com/en-us/office/office-365-management-api/office-365-management-apis-overview) | 7 days | Good | All Office 365 logs (Azure AD included) | Azure App registration | -| Azure AD Logs / [Azure AD PowerShell Preview](https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/reference-powershell-reporting) | 30 days | Good | Azure AD sign-ins and audit events only | Windows OS only | -| Azure AD Logs / [MS Graph API](https://docs.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) | 30 days | Good | Azure AD sign-ins and audit events only | None | +| **Data source / Endpoint** | **Retention** | **Performance** | **Scope** | +|---|---|---|---| +| Unified Audit Log / [Exchange Online PowerShell](https://learn.microsoft.com/en-us/powershell/module/exchange/search-unifiedauditlog?view=exchange-ps) | 90 days | Poor | All Microsoft 365 logs (Entra included) | +| Unified Audit Log / [Purview](https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.beta.security/new-mgbetasecurityauditlogquery?view=graph-powershell-beta) | 180 days | Good | All Microsoft 365 logs (Entra included) | +| Unified Audit Log / [Office 365 Management API](https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-apis-overview) \* | 7 days | Good | All Microsoft 365 logs (Entra included) | +| Microsoft Entra logs / [Microsoft Graph PowerShell](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/reference-powershell-reporting) | 30 days | Good | Entra sign-ins and audit logs only | +| Microsoft Entra logs / [Microsoft Graph REST API](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) | 30 days | Good | Entra sign-ins and audit logs only | + +\* The *Office 365 Management API* is intended to analyze data in real time with a SIEM. DFIR-O365RC is a forensic tool, its aim is not to monitor a Microsoft 365 environment in real time. -DFIR-O365RC is a forensic tool, its aim is not to monitor in real time your Office 365 infrastructure: Please use the *Office 365 Management API* if you want to analyze data in real time with a SIEM. DFIR-O365RC will fetch data from: -- Azure AD Logs using the *MS Graph API* because performance is good, history is 30 days and it works on *PowerShell Core*. -- Unified Audit Logs using *Exchange online PowerShell* despite poor performance, history is 90 days and it works on *PowerShell Core*. +- Microsoft Entra Logs using _Microsoft Graph PowerShell_ because performance is good and it wraps around the _Microsoft Graph REST API_ ; +- By default, Unified Audit Log using *Exchange Online PowerShell*: despite poor performance this is the only usable option for now ; +- Optionally, Unified Audit Log using *Purview*. The retention is 180 days, it has good performance but it is still in beta and bugs in the back-end make it unusable for now. -If you are investigating Exchange Online malicious activity, the Search-O365 function will also fetch the Mailbox Audit Log, using Exchange Online PowerShell. +If you are investigating Microsoft 365 malicious activity, the `Search-O365` (from _Exchange Online PowerShell_) will also fetch the Mailbox Audit Log, although the `Search-MailboxAuditLog` cmdlet is [being deprecated](https://techcommunity.microsoft.com/t5/security-compliance-and-identity/update-on-the-deprecation-of-admin-audit-log-cmdlets/ba-p/4172019). -In case you are also investigating other Azure resources (IaaS, PaaS...) DFIR-O365RC can also fetch data from Azure [Activity logs](https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log) using the *[Azure Monitor RESTAPI](https://docs.microsoft.com/en-us/rest/api/monitor/)*. History is 90 days and it works on *PowerShell Core*. The Azure Activity log is primarily for activities that occur in Azure Resource Manager. +If you are investigating other Azure resources, with DFIR-O365RC: -Additionally you can dump Azure [DevOps Activity logs](https://docs.microsoft.com/en-us/azure/devops/organizations/audit/azure-devops-auditing) using the *[Azure DevOps services RESTAPI](https://docs.microsoft.com/en-us/rest/api/azure/devops/audit/audit%20log/query?view=azure-devops-rest-6.1)*. History is 90 days and it works on *PowerShell Core*. +- you can get the [Azure Monitor Activity log](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log?tabs=powershell) using the [Az.Monitor PowerShell module](https://learn.microsoft.com/en-us/powershell/module/az.monitor/get-azactivitylog), with a retention of 90 days. This log focuses on activities in _Azure Resource Manager_ (related to an Azure subscription) ; +- you can get the [Azure DevOps audit log](https://learn.microsoft.com/en-us/azure/devops/organizations/audit/azure-devops-auditing?view=azure-devops&tabs=preview-page) using the [Azure DevOps Services REST API](https://learn.microsoft.com/en-us/rest/api/azure/devops/audit/audit-log/query?view=azure-devops-rest-7.2&tabs=HTTP), with a retention of 90 days. This log focuses on activities in _Azure DevOps_ (related to an Azure DevOps organization). -Because all functions can run on *PowerShell Core*, DFIR-O365RC works also on Linux or Mac, as long as you have have a browser in order to use device login. -## Installation and pre-requisites -### Manual Installation. +## Installation and prerequisites -Clone the DFIR-O365RC repository. The tool works on *PowerShell Desktop* and *PowerShell Core*. Please note that the new `Connect-ExchangeOnline` cmdlet [requires Microsoft .NET Framework 4.7.1 or later](https://learn.microsoft.com/en-us/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#windows). +### Using Docker -DFIR-O365 uses Jason Thompson's [MSAL.PS](https://github.com/AzureAD/MSAL.PS) and Boe Prox's [PoshRSJob](https://github.com/proxb/PoshRSJob) modules as well as the [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.1.0) module To install them run the following commands: +_This is the recommended way of using DFIR-O365RC_ -``` -Install-Module -Name MSAL.PS -RequiredVersion '4.37.0.0' -Install-Module -Name PoshRSJob -RequiredVersion '1.7.4.4' -Install-Module -Name ExchangeOnlineManagement -RequiredVersion '3.1.0' -``` +Clone the repository and use `docker compose` (or the legacy `docker-compose`) to build the image, run the container and mount a volume (in the `output/` folder): -If [MSAL.PS](https://github.com/AzureAD/MSAL.PS) module installation fails with the following message: -``` -WARNING: The specified module ‘MSAL.PS’ with PowerShellGetFormatVersion ‘2.0’ is not supported by the current version of PowerShellGet. Get the latest version of the PowerShellGet module to install this module, ‘MSAL.PS’. +```bash +sudo docker compose run dfir-o365rc +# using legacy Compose V1 +sudo docker-compose run dfir-o365rc ``` -Update PowerShellGet with the following commands: -``` -Install-PackageProvider Nuget -Force -Install-Module -Name PowerShellGet -Force +DFIR-O365RC is ready to use: + +```bash +PowerShell 7.4.2 +DFIR-O365RC: PowerShell module for Microsoft 365 and Entra ID log collection +https://github.com/ANSSI-FR/DFIR-O365RC +PS /mnt/host/output> ``` -Once both modules are installed, launch a PowerShell prompt and locate your Powershell modules path with the following command: +### Manual Installation + +Clone the DFIR-O365RC repository. The module works on *PowerShell Desktop* and *PowerShell Core*. + +Please note that the `Connect-ExchangeOnline` cmdlet [requires Microsoft .NET Framework 4.7.2 or later](https://learn.microsoft.com/en-us/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#windows). + +DFIR-O365RC uses Boe Prox's [PoshRSJob](https://github.com/proxb/PoshRSJob) module as well as a lot of Microsoft modules to interact with the required SDKs. + +Install them by running: + +```powershell +Install-Module Az.Accounts -RequiredVersion 3.0.2 +Install-Module Az.Monitor -RequiredVersion 5.2.1 +Install-Module Az.Resources -RequiredVersion 7.2.0 +Install-Module ExchangeOnlineManagement -RequiredVersion 3.5.1 +Install-Module Microsoft.Graph.Authentication -RequiredVersion 2.20.0 +Install-Module Microsoft.Graph.Applications -RequiredVersion 2.20.0 +Install-Module Microsoft.Graph.Beta.Reports -RequiredVersion 2.20.0 +Install-Module Microsoft.Graph.Beta.Security -RequiredVersion 2.20.0 +Install-Module Microsoft.Graph.Identity.DirectoryManagement -RequiredVersion 2.20.0 +Install-Module PoshRSJob -RequiredVersion 1.7.4.4 ``` + +Once the modules are installed, launch a PowerShell prompt and locate your Powershell modules path: + +```powershell PS> $env:PSModulePath ``` -Copy the DFIR-O365RC directory in one of your modules path, for example on Windows: -- *%USERPROFILE%\Documents\WindowsPowerShell\Modules* -- *%ProgramFiles%\WindowsPowerShell\Modules* -- *%SYSTEMROOT%\system32\WindowsPowerShell\v1.0\Modules* -Modules path examples on Linux: -- */home/%USERNAME%/.local/share/powershell/Modules* -- */usr/local/share/powershell/Modules* -- */opt/microsoft/powershell/7/Modules* +Copy the [DFIR-O365RC directory](DFIR-O365RC/) in one of your modules path, for example: -On PowerShell Core, the installation of the WSMan client might also be required: +- on Windows: -``` -Install-Module PSWSMan -Install-WSMan -``` -The DFIR-O365RC module is installed, restart the PowerShell prompt and load the module: + - `%UserProfile%\Documents\WindowsPowerShell\Modules` -``` -PS> Import-module DFIR-O365RC -``` + - `%ProgramFiles%\WindowsPowerShell\Modules` -### Use Docker to run DFIR-O365RC. + - `%SystemRoot%\system32\WindowsPowerShell\v1.0\Modules` -Use *docker-compose* to build the image, run the container and mount a volume to retrieve logs: +- on Linux: -``` -sudo docker-compose run dfir-o365rc -``` + - `/home/%username%/.local/share/powershell/Modules` -The module is ready to use: + - `/usr/local/share/powershell/Modules` -``` -PowerShell 7.1.4 -Copyright (c) Microsoft Corporation. + - `/opt/microsoft/powershell/7/Modules` -https://aka.ms/powershell -Type 'help' to get help. -DFIR-O365RC: PowerShell module for Office 365 and Azure log collection -https://github.com/ANSSI-FR/DFIR-O365RC -Loading personal and system profiles took 854ms. -PS /mnt/host/output> +Restart the PowerShell prompt and import the DFIR-O365RC module: + +```powershell +PS> Import-Module DFIR-O365RC ``` -## Roles and license requirements -The user launching the tool should have the following roles: +## Managing the DFIR-O365RC application + +### Creating the application + +Once the module is imported, you will need to create an Entra application, which will handle the log collection process for you. + +To do so: + +1) Create a self-signed certificate and get the base64-encoded public part: + + On Linux, using PowerShell Core or the Docker container: + + ```bash + openssl req -new -x509 -newkey rsa:2048 -sha256 -days 365 -nodes -out exampleDFIRO365RC.crt -keyout exampleDFIRO365RC.key -batch + openssl pkcs12 -inkey exampleDFIRO365RC.key -in exampleDFIRO365RC.crt -export -out exampleDFIRO365RC.pfx # Enter a password for the certificate + openssl x509 -in exampleDFIRO365RC.crt -outform DER -out - | base64 | tr -d "\n" + ``` + + On Windows, using PowerShell: + + ```powershell + $certificate = New-SelfSignedCertificate -Subject "CN=exampleDFIRO365RC" -KeySpec KeyExchange -NotBefore (Get-Date) -NotAfter (Get-Date).AddDays(365) + $certificatePassword = Read-Host -MaskInput "Please enter a password for the certificate" + $certificateSecurePassword = ConvertTo-SecureString -String $certificatePassword -AsPlainText -Force + Export-PfxCertificate -Cert $certificate -FilePath exampleDFIRO365RC.pfx -Password $certificateSecurePassword + Write-Host ([System.Convert]::ToBase64String($certificate.GetRawCertData())) + ``` + +2) Use the `New-Application` cmdlet from the DFIR-O365RC module: + + ```powershell + $certificateb64="" + New-Application -certificateb64 $certificateb64 + ``` + + Optionally, if you would like to be able to gather logs in the subscriptions of the tenant (not needed if you do not plan to use `Get-AzRMActivityLogs`): + + ```powershell + New-Application -certificateb64 $certificateb64 -subscriptions + ``` + + Optionally, if you would like to be able to gather logs in the Azure DevOps organizations of the tenant (this can take a long time and is not needed if you do not plan to use `Get-AzDevOpsActivityLogs`): + + ```powershell + New-Application -certificateb64 $certificateb64 -organizations + ``` + + To create the application, you will need to log in to Azure several times, using a **highly privileged** account. + + One the application is created, you will get an output similar to: + + ```powershell + Done creating the application with the required permissions + Please use the following identifiers: + WARNING: AppID: xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + WARNING: Tenant: example.onmicrosoft.com + ``` + +### Updating the application + +Once the application is created, you can still, using the `Update-Application` cmdlet from the module, update its credentials and permissions: + +- You can add a new certificate to the application: + + `Update-Application -certificateb64 ` + +- You can specify new subscriptions in which you would like to be able to gather logs: + + `Update-Application -subscriptions` + +- You can specify new Azure DevOps organizations in which you would like to be able to gather logs: + + `Update-Application -organizations` + +### Removing the application + +Once you are done with the log collection you can delete the application using the `Remove-Application` cmdlet from the module. + +To remove the application, you will need to log in to Azure several times, using a **highly privileged** account. + +If the application was able to gather logs of subscriptions or Azure DevOps organizations, you will need to add the `-organizations` and/or `-subscriptions` switches. + + + +## Permissions and license requirements + +⚠️ - - Microsoft 365 role (portal.microsoft.com): **Global reader** - - Exchange Online role (admin.exchange.microsoft.com): **View-Only Audit Logs** +Starting with version 2.0.0, the tool is now running in the context of a Service Principal with [App-only access / Application permissions](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview#app-only-access-access-without-a-user). -In order to retrieve Azure AD [sign-ins logs](https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/concept-sign-ins) with the *MS Graph API* you need at least one user with an [Azure AD Premium P1](https://azure.microsoft.com/en-us/pricing/details/active-directory/) license. This license can be purchased at additional cost for a single user and is sometimes included in some license plans such as the *Microsoft 365 Business Premium* for small and medium-sized businesses. +To use version 2.0.0 and up, you will need to [create an application](#creating-the-application). -If you need to retrieve also the Azure Activity logs you need the **Log Analytics Reader** role for the Azure subscription you are dumping the logs from. +Once the application is created, the script will run using the application's credentials and permissions. -Finally if you need to collect the Azure DevOps Activity logs you need to grant the **Auditing\View audit log** permission to the user in the Azure DevOps organization you are dumping the logs from. +⚠️ - ## Functions included in the module +The application will be created with the least possible required permission set: + +- `Exchange.ManageAsApp` for the `Office 365 Exchange Online` API (required to be able to run Exchange Online PowerShell cmdlets) +- `AuditLog.Read.All` for the `Microsoft Graph` API (required for Microsoft Entra log collection) +- `AuditLogsQuery.Read.All` for the `Microsoft Graph` API (required for Unified Audit Log collection using Purview) +- `Application.Read.All` and `DelegatedPermissionGrant.Read.All` for the `Microsoft Graph` API (required for the enrichment of Microsoft Entra logs related to applications and service principals) +- `Device.Read.All` for the `Microsoft Graph` API (required for the enrichment of Microsoft Entra logs related to devices) +- `Organization.Read.All` for the `Microsoft Graph` API (required for getting general information on the tenant) +- `View-only audit logs` in `Exchange Online` (required to use the `Search-UnifiedAuditLog` cmdlet) + +Optionally (if using the `-subscriptions` switch): + +- For the selected subset of subscriptions: `Reader` role on `Microsoft.Insights/eventtypes/*` (required to get the Azure Monitor Activity log) + +Optionally (if using the `-organizations` switch): + +- For the selected subset of Azure DevOps subscriptions: `View audit log` (required to get the Azure DevOps audit log) + + + +In order to retrieve Microsoft Entra logs with the Microsoft Graph API you need at least one user with a [Microsoft Entra ID P1](https://www.microsoft.com/en-us/security/business/microsoft-entra-pricing) license. This license can be purchased for a single user or can be included in some license plans such as the *Microsoft 365 Business Premium* plan. + + + + ## Functions included in the module The module has 9 functions: -| **Function name** | **Data Source/History** | **Performance** | **Completeness** | **Details** | -|---|---|---|---|---| -| Get-O365Full | Unified audit logs/90 days | Poor | All unified audit logs | A subset of logs per *record type* can be retrieved. Use only on a small tenant or a short period of time | -| Get-O365Light | Unified audit logs/90 days | Good | A subset of unified audit logs only | Only a subset of *operations* considered of interest is retrieved. | -| Get-DefenderforO365 | Unified audit logs/90 days | Good | A subset of unified audit logs only | Retrieves Defender for Office 365 related logs. Requires at least an [E5 license](https://www.microsoft.com/en-us/microsoft-365/enterprise/office-365-e5?activetab=pivot:overviewtab) or a license plan such as [Microsoft Defender for Office 365 Plan](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/office-365-atp?view=o365-worldwide#microsoft-defender-for-office-365-plan-1-and-plan-2) or [cloud app security](https://www.microsoft.com/en-us/microsoft-365/enterprise-mobility-security/cloud-app-security) | -| Get-AADLogs | Azure AD Logs/30 days | Good | All Azure AD logs | Get tenant general information, all Azure sign-ins and audit logs. Azure AD sign-ins logs have more information than Azure AD logs retrieved via Unified audit logs. | -| Get-AADApps | Azure AD Logs/30 days | Good | A subset of Azure AD logs only | Get Azure audit logs related to Azure applications and service principals only. The logs are enriched with application or service principal object information. | -| Get-AADDevices | Azure AD Logs/30 days | Good | A subset of Azure AD logs only | Get Azure audit logs related to Azure AD joined or registered devices only. The logs are enriched with device object information. | -| Search-O365 | Unified audit logs/90 days | Depends on the query | A subset of unified audit logs only | Search for activity related to a particular user, IP address or use the *freetext* query. When searching **user** activity this cmdlet will also fetch the Mailbox Audit Log| -| Get-AzRMActivityLogs | Azure Activity logs/90 days | Good | All Azure Activity logs | Get all Azure activity logs for a given subscription or on every subscription the account running the function has access to | -| Get-AzDevOpsActivityLogs | Azure DevOps Activity logs/90 days | Good | All Azure DevOps Activity logs | Get all Azure DevOps activity logs for a given DevOps organization or on every DevOps organization the account running the function has access to | +| **Function** | **Data Source** | Retention | **Performance** | **Completeness** | **Details** | +|---|---|---|---|---|---| +| `Get-O365Full` | Unified Audit Log | 90 days / 180 days* | Poor | All Unified Audit Log | By default, retrieve the whole Unified Audit Log. This should only be used on a small tenant or a short period of time.
You can also use this cmdlet to gather events for some specific [record types](https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-schema#enum-auditlogrecordtype---type-edmint32). | +| `Get-O365Light` | Unified Audit Log | 90 days / 180 days* | Good | A subset of Unified Audit Log only | Only a subset of *operations*, which are considered of interest, are retrieved. | +| `Get-O365Defender` | Unified Audit Log | 90 days / 180 days* | Good | A subset of Unified Audit Log only | Retrieves Microsoft Defender for Microsoft 365 related events. Requires at least one [Office 365 E5](https://www.microsoft.com/en-us/microsoft-365/enterprise/office-365-e5?activetab=pivot:overviewtab) license or a license plan which includes Microsoft Defender for Office 365. | +| `Get-AADLogs` | Microsoft Entra Logs | 30 days | Good | All Microsoft Entra Logs | Get tenant information and all Microsoft Entra logs: sign-ins logs and audit logs. | +| `Get-AADApps` | Microsoft Entra Logs + Entra ID | 30 days | Good | A subset of Microsoft Entra Logs only | Get Microsoft Entra audit logs related to Entra applications and service principals only.
The logs are enriched with application or service principal object information. | +| `Get-AADDevices` | Microsoft Entra Logs + Entra ID | 30 days | Good | A subset of Microsoft Entra Logs only | Get Microsoft Entra audit logs related to Entra ID joined or registered devices only.
The logs are enriched with device object information. | +| `Search-O365` | Unified Audit Log / Mailbox Audit Log** | 90 days / 180 days* | Poor | A subset of Unified Audit Log only | Search for activity related to specific users, IP addresses or free texts. | +| `Get-AzRMActivityLogs` | Azure Monitor Activity log | 90 days | Good | All Azure Monitor Activity log | Get all Azure Monitor Activity log for a selected subset of subscriptions. | +| `Get-AzDevOpsActivityLogs` | Azure DevOps audit log | 90 days | Good | All Azure DevOps audit log | Get all Azure DevOps audit log for a selected subset of Azure DevOps organizations. | +\* You can get 180 days of retention using Purview, compared to the default 90 days of retention using Exchange Online. - When querying *Unified audit logs* you are limited to 3 concurrent *Exchange Online Powershell* sessions. DFIR-O365RC will try to use all available sessions, please close any existing session before launching the log collection. +** When searching for users, the `Search-O365` cmdlet will also search in the Mailbox Audit Log. -Each function as a comment based help which you can invoke with the *get-help* cmdlet. -``` -#Display comment based help -PS> Get-help Get-O365Full -#Display comment based help with examples -PS> Get-help Get-O365Full -examples -``` -Each function takes as a parameter a start date and an end date. -In order to retrieve Azure AD audit logs, sign-ins logs from the past 30 days and tenant information launch the following command: +Each function as a comment-based help which you can invoke with the *Get-Help* cmdlet. +```powershell +# Display comment-based help +PS> Get-Help Get-O365Full +# Display comment-based help with examples +PS> Get-Help Get-O365Full -Examples ``` -$enddate = get-date -$startdate = $enddate.adddays(-30) -Get-AADLogs -startdate $startdate -enddate $enddate -``` +Each function takes as a parameter: -In order to retrieve enriched Azure AD audit logs related to Azure applications and service principals from the past 30 days launch the following command: +- a start date (`-startDate`) ; +- an end date (`-endDate`) ; +- the application identifier of the application (`-appId`), which is obtained when [creating the application](#creating-the-application) ; +- the tenant name (`-tenant`) ; +- the path to the certificate in PFX format (`-certificatePath`), which is obtained when [creating the application](#creating-the-application). -``` -$enddate = get-date -$startdate = $enddate.adddays(-30) -Get-AADApps -startdate $startdate -enddate $enddate -``` -In order to retrieve enriched Azure AD audit logs related to Azure AD joined or registered devices from the past 30 days launch the following command: -``` -$enddate = get-date -$startdate = $enddate.adddays(-30) -Get-AADDevices -startdate $startdate -enddate $enddate -``` +**Examples**: -In order to retrieve all unified audit logs considered of interest from the past 30 days, except those related to Azure AD, which were already retrieved by the first command, launch: +For readability, we will assume that: -``` -$enddate = get-date -$startdate = $enddate.adddays(-30) -Get-O365Light -startdate $startdate -enddate $enddate -Operationsset "AllbutAzureAD" +```powershell +$appId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +$tenant = "example.onmicrosoft.com" +$certificatePath = "./example.pfx" ``` -In order to retrieve all unified audit logs considered of interest in a time window between -90 days and -30 days from now launch the following command: +On a real case, those parameters are gathered when [creating the application](#creating-the-application). -``` -$enddate = (get-date).adddays(-30) -$startdate = (get-date).adddays(-90) -Get-O365Light -StartDate $startdate -Enddate $enddate -Operationsset All -``` -If mailbox audit is enabled and you want also to retrieve *Mailboxlogin* operations you can use the dedicated switch, on large tenants beware of a 50.000 events per day limit retrieval. -``` -Get-O365Light -StartDate $startdate -Enddate $enddate -Operationsset All -MailboxLogin $true +In order to retrieve Microsoft Entra Logs from the past 30 days as well as general information on the tenant: + +```powershell +$endDate = Get-Date +$startDate = $endDate.AddDays(-30) +Get-AADLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath ``` -If there are users with Enterprise 5 licenses or if there is a Microsoft Defender for Office 365 Plan you can retrieve Microsoft Defender related logs with the following command: +Get Microsoft Entra audit logs related to Entra applications and service principals from the past 30 days: +```powershell +$endDate = Get-Date +$startDate = $endDate.AddDays(-30) +Get-AADApps -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath ``` -$enddate = get-date -$startdate = $enddate.adddays(-90) -Get-DefenderforO365 -StartDate $startdate -Enddate $enddate + +Get Microsoft Entra audit logs related to Entra ID joined or registered devices from the past 30 days: + +```powershell +$endDate = Get-Date +$startDate = $endDate.AddDays(-30) +Get-AADDevices -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath ``` -To retrieve all Exchange Online related records from the unified audit logs between Christmas eve and Boxing day, beware that performance might be poor on a large tenant: + +Retrieve Unified Audit log events considered of interest from the past 30 days, except those related to Entra ID, which were already retrieved by the first command: + +```powershell +$endDate = Get-Date +$startDate = $endDate.AddDays(-30) +Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -operationsSet "allButAzureAD" ``` -$startdate = get-date "12/24/2020" -$enddate = get-date "12/26/2020" -Get-O365Full -StartDate $startdate -Enddate $enddate -RecordSet ExchangeOnly + +Retrieve Unified Audit log events considered of interest in a time window between -90 days and -30 days from now: + +```powershell +$endDate = Get-Date.AddDays(-30) +$startDate = Get-Date.AddDays(-90) +Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath ``` -You can use the search function to look for IP addresses, activity related to specific users or perfrom a freetext search in the unified audit logs: +If mailbox audit is enabled you can also retrieve `MailboxLogin` operations using the dedicated switch: +_Beware of a global limit of 50.000 events per search_ + +```powershell +Get-O365Light -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -mailboxLogin ``` -$enddate = get-date -$startdate = $enddate.adddays(-90) -#Retrieve events using the Exchange online Powershell AppId -Search-O365 -StartDate $startdate -Enddate $enddate -FreeText "a0c73c16-a7e3-4564-9a95-2bdf47383716" -#Search for events related to the X.X.X.X and Y.Y.Y.Y IP adresses, argument is a string separated by comas. -Search-O365 -StartDate $startdate -Enddate $enddate -IPAddresses "X.X.X.X,Y.Y.Y.Y" +If there are users with Office 365 E5 licenses or if there is a Microsoft Defender for Office 365 Plan in the tenant you can retrieve Microsoft Defender related logs from the past 90 days: -#Retrieve events related to users user1@contoso.com and user2@constoso.com , argument is a system.array object -Search-O365 -StartDate $startdate -Enddate $enddate -UserIds "user1@contoso.com", "user2@contoso.com" +```powershell +$endDate = Get-Date +$startDate = $endDate.AddDays(-90) +Get-O365Defender -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath ``` -When searching for specific **users**, `Search-O365` will also search in the Mailbox Audit Log. Because depending on the user's licence level and settings, audit logs might not be present in the unified audit logs. +To retrieve all Unified Audit Log events between Christmas Eve 2020 and Boxing day 2020: +_Beware that performance using that cmdlet is poor_ -To retrieve all Azure Activity logs the account has access to, launch the following command, available subscriptions will be displayed: +```powershell +$endDate = Get-Date "12/26/2020" +$startdate = Get-Date "12/24/2020" +Get-O365Full -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath ``` -$enddate = get-date -$startdate = $enddate.adddays(-90) -Get-AzRMActivityLogs -StartDate $startdate -Enddate $enddate + +You can use the search function to look for IP addresses, activity related to specific users or perform a freetext search in the Unified Audit Log: + +```powershell +$endDate = Get-Date +$startDate = $endDate.AddDays(-90) + +# Retrieve events which contains the "Python" or "Python3" free text +Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -freeTexts "Python","Python3" + +# Retrieve events related to the IP adresses 8.8.8.8 and 4.4.4.4. +Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -IPAddresses "8.8.8.8","4.4.4.4" + +# Retrieve events related to users user1@example.onmicrosoft.com and user2@example.onmicrosoft.com +Search-O365 -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath -userIds "user1@example.onmicrosoft.com","user2@example.onmicrosoft.com" ``` -To retrieve all Azure DevOps Activity logs the account has access to, launch the following command, available Azure DevOps organizations will be displayed: +When searching for specific **users**, `Search-O365` will also search in the Mailbox Audit Log. That's because, depending on the user's license level and settings, some of the mailbox logs might not be present in the Unified Audit Log. + + + + +To retrieve all Azure Resource Manager activity logs from the subscriptions the application has access to: +```powershell +$endDate = Get-Date +$startDate = $endDate.AddDays(-90) +Get-AzRMActivityLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath ``` -$enddate = get-date -$startdate = $enddate.adddays(-90) -Get-AzDevOpsActivityLogs -StartDate $startdate -Enddate $enddate + + + +To retrieve all Azure DevOps activity logs from the organizations the application has access to: + +```powershell +$endDate = Get-Date +$startDate = $endDate.AddDays(-90) +Get-AzDevOpsActivityLogs -startDate $startDate -endDate $endDate -appId $appId -tenant $tenant -certificatePath $certificatePath ``` -When using *PowerShell Core* the authentication process will require a *device code*, you will need to use the *devicecode* parameter and launch your browser, open the *https://microsoft.com/devicelogin* URL and enter the code provided by the following message: - - ``` - PS> Get-O365Light -StartDate $startdate -Enddate $enddate -DeviceCode:$true - To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code XXXXXXXX to authenticate. - ``` - - ## Files generated + + + + ## Files generated All files generated are in JSON format. -- Get-AADApps creates a file named *AADApps_%FQDN%.json* in the *azure_ad_apps* folder where *FQDN* is the domain name part of the account used to collect the logs. -- Get-AADDevices creates a file named *AADDevices_%FQDN%.json* in the *azure_ad_devices* folder. -- Get-AADLogs creates folders named after the current date using the *YYYY-MM-DD* format in the *azure_ad_signin* folder, in each directory a file called *AADSigninLog_%FQDN%_YYYY-MM-DD_HH-00-00.json* is created for Azure AD sign-ins logs. A folder *azure_ad_audit* is also created and results are dumped in files named *AADAuditLog_%FQDN%_YYYY-MM-DD.json* for Azure AD audit logs. Finally a folder called *azure_ad_tenant* is created and the general tenant information written in a file named *AADTenant_%FQDN%.json*. -- Get-AzRMActivityLogs creates folders named after the current date using the *YYYY-MM-DD* format in the *azure_rm_activity* folder, in each directory a file called *AzRM_%FQDN%_%SubscriptionID%_YYYY-MM-DD_HH-00-00.json* is created where %SubscriptionID% is the Azure subscription ID. A folder called *azure_rm_subscriptions* is created and each subscription information written in a file named *AzRMsubscriptions_%FQDN%.json*. -- Get-AzDevOpsActivityLogs creates folders named after the current date using the *YYYY-MM-DD* format in the *azure_DevOps_activity* folder, in each directory a file called *AzDevOps_%FQDN%_%DevOpsOrganizationname%_YYYY-MM-DD_HH-00-00.json* is created where %DevOpsOrganizationname% is the Azure DevOps organization name. A folder called *azure_DevOps_orgs* is created and each azure DevOps organization information written in a file named *AzdevopsOrgs_%FQDN%.json. -- Get-O365Full creates folders named after the current date using the *YYYY-MM-DD* format in the *O365_unified_audit_logs*, in each directory a file called *UnifiedAuditLog_%FQDN%_YYYY-MM-DD_HH-00-00.json* is created. -- Get-O365Light creates folders named after the current date using the *YYYY-MM-DD* format in the *O365_unified_audit_logs*, in each directory a file called *UnifiedAuditLog_%FQDN%_YYYY-MM-DD.json* is created. -- Get-DefenderforO365 creates folders named after the current date using the *YYYY-MM-DD* format in the *O365_unified_audit_logs*, in each directory a file called *UnifiedAuditLog_%FQDN%_YYYY-MM-DD_DefenderforO365.json* is created. -- Search-O365 creates folders named after the current date using the *YYYY-MM-DD* format in the *O365_unified_audit_logs*, in each directory a file called *UnifiedAuditLog_%FQDN%_YYYY-MM-DD_%searchtype%.json* is created, where *searchtype* can have the values "*Freetext*", "*IPAddresses*" or "*UserIds*". *MailboxAuditLog_%FQDN%_YYYY-MM-DD_UserIds.json* files can also be created when investigating activity from users. +_Launching several cmdlet which uses Purview and will write to the same output file can result in an invalid JSON because of a "naive" concatenation_ + +- `Get-AADApps` will create in the `azure_ad_apps` folder: + - a JSON file containing deleted applications: `AADApps_example.onmicrosoft.com_deleted_applications_raw.json`; + - a JSON file containing existing applications: `AADApps_example.onmicrosoft.com_existing_applications_raw.json`; + - a JSON file containing existing service principals: `AADApps_example.onmicrosoft.com_service_principals_raw.json`; + - a JSON file containing the enriched events: `AADApps_example.onmicrosoft.com.json`. +- `Get-AADDevices` will create in the `azure_ad_devices` folder: + - a JSON file containing joined or registered devices: `AADDevices_example.onmicrosoft.com_devices_raw.json`; + - a JSON file containing the enriched events: `AADDevices_example.onmicrosoft.com.json`. + +- `Get-AADLogs` will create: + - in the `azure_ad_tenant` folder: + - a JSON file containing general information on the tenant: `AADTenant_example.onmicrosoft.com.json`. + + - in the `azure_ad_audit` folder: + - JSON files containing Microsoft Entra audit logs: `AADAuditLog_example.onmicrosoft.com_YYYY-MM-DD.json`. + + - in the `azure_ad_signin` folder: + - JSON files containing Microsoft Entra sign-in logs: `YYYY-MM-DD/AADSigninLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00.json`. -Launching the various functions will generate a similar directory structure: +- `Get-AzRMActivityLogs` will create: + - in the `azure_rm_subscriptions` folder: + - a JSON file containing general information on the subscriptions: `AzRMsubscriptions_example.onmicrosoft.com.json`. + + - in the `azure_rm_activity` folder: + - JSON files containing Azure Monitor Activity logs: `YYYY-MM-DD/AzRM_example.onmicrosoft.com_%SubscriptionID%_YYYY-MM-DD_HH-00-00.json`. + +- `Get-AzDevOpsActivityLogs` will create: + - in the `azure_DevOps_orgs` folder: + - a JSON file containing general information on the Azure DevOps organizations: `AzdevopsOrgs_example.onmicrosoft.com.json`. + + - in the `azure_DevOps_activity` folder: + - JSON files containing Azure DevOps audit logs: `YYYY-MM-DD/AzDevOps_example.onmicrosoft.com_%AzureDevOpsOrg%_YYYY-MM-DD_HH-00-00.json`. + +- `Get-O365Full` will create in the `O365_unified_audit_logs` folder (respectively `O365_unified_audit_logs_purview` when using Purview): + - JSON files containing Unified Audit logs: `YYYY-MM-DD/UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00.json` (respectively `*/UnifiedAuditLogPurview_*` when using Purview); + - JSON files containing Unified Audit logs for specified [RecordTypes](https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-schema#enum-auditlogrecordtype---type-edmint32): `YYYY-MM-DD/UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_%RecordType%.json` (respectively `*/UnifiedAuditLogPurview_*` when using Purview). + +- `Get-O365Light` will create in the `O365_unified_audit_logs` folder (respectively `O365_unified_audit_logs_purview` when using Purview): + - JSON files containing Unified Audit logs for a subset of *operations*, which are considered of interest: `YYYY-MM-DD/UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00.json` (respectively `*/UnifiedAuditLogPurview_*` when using Purview). +- `Get-O365Defender` will create in the `O365_unified_audit_logs` folder (respectively `O365_unified_audit_logs_purview` when using Purview): + - JSON files containing Unified Audit logs for the [RecordTypes](https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-schema#enum-auditlogrecordtype---type-edmint32) associated with Defender: `YYYY-MM-DD/UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_%RecordType%.json` (respectively `*/UnifiedAuditLogPurview_*` when using Purview). +- `Search-O365` will create: + - in the `O365_unified_audit_logs` folder (respectively `O365_unified_audit_logs_purview` when using Purview): + - JSON files containing Unified Audit logs for the specified `RequestType` (`FreeText`, `IPAddresses`, or`UserIds`): `YYYY-MM-DD/UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_%RequestType%_YYYY-MM-DD-HH-MM-SS.json` (respectively `*/UnifiedAuditLogPurview_*` when using Purview). `YYYY-MM-DD-HH-MM-SS` represents the time when the collect was done. When searching for `FreeText`, an additional `_%i` is added at the end, which indicates this is the result from the search of the `%i`-th free text. + + - in the `Exchange_mailbox_audit_logs` folder: + - JSON files containing Mailbox Audit logs (only when searching for UserIDs): `YYYY-MM-DD/MailboxAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_UserIds_YYYY-MM-DD-HH-MM-SS_%UserID%.json`. `YYYY-MM-DD-HH-MM-SS` represents the time when the collect was done. `%UserID%` indicates this is the result from the search of this UserID. + + + + +Launching the various functions will generate a directory structure similar to this one: ``` -DFIR-O365_Logs +output │ Get-AADApps.log │ Get-AADDevices.log │ Get-AADLogs.log -| Get-AzRMActivityLogs -│ Get-DefenderforO365.log -│ Get-O365Light.log +│ Get-AzDevOpsActivityLogs.log +│ Get-AzRMActivityLogs.log +│ Get-O365Defender.log +│ Get-O365Full.log +│ Get-O365Light.log │ Search-O365.log -└───azure_ad_apps -│ │ AADApps_%FQDN%.json -└───azure_ad_audit -│ │ AADAuditLog_%FQDN%_YYYY-MM-DD.json -│ │ ... -└───azure_ad_devices -│ │ AADDevices_%FQDN%.json -└───azure_ad_signin +│ +├───azure_ad_apps +│ AADApps_example.onmicrosoft.com.json +│ AADApps_example.onmicrosoft.com_deleted_applications_raw.json +│ AADApps_example.onmicrosoft.com_existing_applications_raw.json +│ AADApps_example.onmicrosoft.com_service_principals_raw.json +│ +├───azure_ad_audit +│ AADAuditLog_example.onmicrosoft.com_YYYY-MM-DD.json +│ [...] +│ +├───azure_ad_devices +│ AADDevices_example.onmicrosoft.com.json +│ AADDevices_example.onmicrosoft.com_devices_raw.json +│ +├───azure_ad_signin +│ ├───YYYY-MM-DD +│ │ AADSigninLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00.json +│ │ [...] │ │ -│ └───YYYY-MM-DD -│ │ AADSigninLog_%FQDN%_YYYY-MM-DD_HH-00-00.json -│ │ ... -└───azure_ad_tenant -│ │ AADTenant_%FQDN%.json -└───azure_rm_activity +│ ├───[...] +│ +├───azure_ad_tenant +│ AADTenant_example.onmicrosoft.com.json +│ +├───azure_DevOps_activity +│ ├───YYYY-MM-DD +│ │ AzDevOps_example.onmicrosoft.com_%AzureDevOpsOrg%_YYYY-MM-DD_HH-00-00.json +│ │ [...] │ │ -│ └───YYYY-MM-DD -│ │ AzRM_%FQDN%_%SubscriptionID%_YYYY-MM-DD_HH-00-00.json -│ │ ... -└───azure_rm_subscriptions -│ │ AzRMsubscriptions_%FQDN%.json -└───azure_DevOps_activity +│ ├───[...] +│ +├───azure_DevOps_orgs +│ AzdevopsOrgs_example.onmicrosoft.com.json +├───azure_rm_activity +│ ├───YYYY-MM-DD +│ │ AzRM_example.onmicrosoft.com_%SubscriptionID%_YYYY-MM-DD_HH-00-00.json +│ │ [...] │ │ +│ ├───[...] +│ +├───azure_rm_subscriptions +│ AzRMsubscriptions_example.onmicrosoft.com.json +│ +├───Exchange_mailbox_audit_logs │ └───YYYY-MM-DD -│ │ AzDevOps_%FQDN%_%DevOpsOrganisationname%_YYYY-MM-DD_HH-00-00.json -│ │ ... -└───azure_DevOps_orgs -│ │ AzdevopsOrgs_%FQDN%.json -└───O365_unified_audit_logs +│ │ MailboxAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_UserIds_YYYY-MM-DD-HH-MM-SS_%UserID%.json +│ │ [...] │ │ -│ └───YYYY-MM-DD -│ │ UnifiedAuditLog_%FDQN%_YYYY-MM-DD.json -│ │ UnifiedAuditLog_%FQDN%_YYYY-MM-DD_freetext.json -│ │ MailboxAuditLog_%FQDN%_YYYY-MM-DD_UserIds.json -│ │ UnifiedAuditLog_%FQDN%_YYYY-MM-DD_DefenderforO365.json -│ │ UnifiedAuditLog_%FQDN%_YYYY-MM-DD_HH-00-00.json -│ │ ... - +│ ├───[...] +│ +├───O365_unified_audit_logs +│ ├───YYYY-MM-DD +│ │ UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00.json +│ │ UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_%RecordType%.json +│ │ UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_UserIds_YYYY-MM-DD-HH-MM-SS.json +│ │ UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_IPAddresses_YYYY-MM-DD-HH-MM-SS.json +│ │ UnifiedAuditLog_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_FreeText_YYYY-MM-DD-HH-MM-SS_%i.json +│ │ [...] +│ │ +│ ├───[...] +│ +└───O365_unified_audit_logs_purview +│ ├───YYYY-MM-DD +│ │ UnifiedAuditLogPurview_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00.json +│ │ UnifiedAuditLogPurview_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_%RecordType%.json +│ │ UnifiedAuditLogPurview_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_UserIds_YYYY-MM-DD-HH-MM-SS.json +│ │ UnifiedAuditLogPurview_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_IPAddresses_YYYY-MM-DD-HH-MM-SS.json +│ │ UnifiedAuditLogPurview_example.onmicrosoft.com_YYYY-MM-DD_HH-00-00_FreeText_YYYY-MM-DD-HH-MM-SS_%i.json +│ │ [...] +│ │ +│ ├───[...] ``` diff --git a/docker-compose.yaml b/docker-compose.yaml index 829ed3f..7c4377c 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,3 @@ -version: "3" services: dfir-o365rc: build: