diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc826a5..984c728 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,7 @@ on: - push: + pull_request: branches: - master - jobs: build: runs-on: windows-2019 diff --git a/.github/workflows/psscriptanalyzer.yml b/.github/workflows/psscriptanalyzer.yml new file mode 100644 index 0000000..95466bc --- /dev/null +++ b/.github/workflows/psscriptanalyzer.yml @@ -0,0 +1,96 @@ +name: PSScriptAnalyzer +on: + pull_request: + paths: + - "**.ps1" + - "**.psm1" + - "**.psd1" + push: + paths: + - "**.ps1" + - "**.psm1" + - "**.psd1" + +jobs: + analyze: + name: PSScriptAnalyzer + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for getting changed files + + - name: Get changed files + shell: pwsh + run: | + if ($env:GITHUB_EVENT_NAME -eq 'pull_request') { + $baseCommit = git rev-parse $env:GITHUB_EVENT.pull_request.base.sha + $headCommit = git rev-parse HEAD + $changedFiles = git diff --name-only $baseCommit..$headCommit + } else { + $changedFiles = git diff --name-only HEAD^1 HEAD + } + + $powershellFiles = $changedFiles | Where-Object { + $_ -match '\.(ps1|psm1|psd1)$' + } + + $powershellFiles | Out-File -FilePath $env:GITHUB_WORKSPACE/changed_files.txt + Write-Host "Changed PowerShell files:" + $powershellFiles | ForEach-Object { Write-Host " $_" } + + - name: Install PSScriptAnalyzer + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module PSScriptAnalyzer -Force + + - name: Run PSScriptAnalyzer + shell: pwsh + run: | + $settingsPath = Join-Path $env:GITHUB_WORKSPACE 'Hawk' 'internal' 'configurations' 'PSScriptAnalyzerSettings.psd1' + + Write-Output "Using settings file: $settingsPath" + if (-not (Test-Path $settingsPath)) { + Write-Error "PSScriptAnalyzer settings file not found at: $settingsPath" + exit 1 + } + + $changedFiles = Get-Content -Path "$env:GITHUB_WORKSPACE/changed_files.txt" + if (-not $changedFiles) { + Write-Output "No PowerShell files were changed" + $null > (Join-Path $env:GITHUB_WORKSPACE 'psscriptanalyzer-results.txt') + exit 0 + } + + $results = @() + foreach ($file in $changedFiles) { + $fullPath = Join-Path $env:GITHUB_WORKSPACE $file + if (Test-Path $fullPath) { + Write-Output "Analyzing $fullPath" + $fileResults = Invoke-ScriptAnalyzer -Path $fullPath -Settings $settingsPath + if ($fileResults) { + $results += $fileResults + } + } + } + + if ($results) { + Write-Output "Found $($results.Count) issues in changed files:" + $results | Format-Table -AutoSize | Out-String | Write-Output + $results | Format-Table -AutoSize | Out-File (Join-Path $env:GITHUB_WORKSPACE 'psscriptanalyzer-results.txt') + exit 1 + } else { + Write-Output "No PSScriptAnalyzer issues found in changed files" + $null > (Join-Path $env:GITHUB_WORKSPACE 'psscriptanalyzer-results.txt') + exit 0 + } + + - name: Upload Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: psscriptanalyzer-results + path: psscriptanalyzer-results.txt + if-no-files-found: warn diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1835e53..326d406 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,4 +1,13 @@ -on: [pull_request] +on: + pull_request: + branches: + - master + - Development + push: + branches: + - master + - Development + - bugfix/162-modernize-authentication-to-replace-azuread-with-microsoft-graph jobs: validate: diff --git a/.gitignore b/.gitignore index 0d24381..61b077c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ - -# ignore the settings folder and files for VSCode and PSS +# ignore the settings folder and files for VSCode and PSS .vscode/* *.psproj *TempPoint* @@ -19,4 +18,11 @@ Hawk/Hawk.psproj TestResults/* # ignore the publishing Directory -publish/* \ No newline at end of file +publish/* + +# Ignore all .csv, .json, .docx, and .xlsx files +*.csv +*.json +*.docx +*.doc +*.xlsx diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..57a46e1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: local + hooks: + - id: powershell-script-analyzer + name: PowerShell Script Analyzer + entry: pwsh + args: + - -NoProfile + - -ExecutionPolicy + - Bypass + - -File + - Hawk/internal/scripts/pre_commit_hook_scripts/Invoke-PowerShellScriptAnalyzer.ps1 + language: system + types: [powershell] diff --git a/Hawk/Hawk.psd1 b/Hawk/Hawk.psd1 index 9266860..1589cd3 100644 --- a/Hawk/Hawk.psd1 +++ b/Hawk/Hawk.psd1 @@ -1,40 +1,42 @@ @{ # Script module or binary module file associated with this manifest - RootModule = 'Hawk.psm1' + RootModule = 'Hawk.psm1' # Version number of this module. - ModuleVersion = '3.1.0' + ModuleVersion = '3.2.4' # ID used to uniquely identify this module - GUID = '1f6b6b91-79c4-4edf-83a1-66d2dc8c3d85' + GUID = '1f6b6b91-79c4-4edf-83a1-66d2dc8c3d85' # Author of this module - Author = 'Paul Navarro' + Author = 'Paul Navarro, Jonathan Butler' # Company or vendor of this module - CompanyName = 'Cloud Forensicator' + CompanyName = 'Cloud Forensicator' # Copyright statement for this module - Copyright = 'Copyright (c) 2023 Paul Navarro' + Copyright = 'Copyright (c) 2025 Paul Navarro' # Description of the functionality provided by this module - Description = 'Microsoft 365 Incident Response and Threat Hunting PowerShell tool. + Description = 'Microsoft 365 Incident Response and Threat Hunting PowerShell tool. The Hawk is designed to ease the burden on M365 administrators who are performing Cloud forensic tasks for their organization. It accelerates the gathering of data from multiple sources in the service that be used to quickly identify malicious presence and activity.' # Minimum version of the Windows PowerShell engine required by this module - PowerShellVersion = '5.0' + PowerShellVersion = '5.0' # Modules that must be imported into the global environment prior to importing # this module - RequiredModules = @( - @{ModuleName = 'PSFramework'; ModuleVersion = '1.4.150'}, - @{ModuleName = 'PSAppInsights'; ModuleVersion = '0.9.6'}, - @{ModuleName = 'ExchangeOnlineManagement'; ModuleVersion = '3.0.0'}, - @{ModuleName = 'RobustCloudCommand'; ModuleVersion = '2.0.1'}, - @{ModuleName = 'AzureAD'; ModuleVersion = '2.0.2.140'}, - @{ModuleName = 'Microsoft.Graph.Authentication'; ModuleVersion = '1.23.0'}, - @{ModuleName = 'Microsoft.Graph.Identity.DirectoryManagement'; ModuleVersion = '1.23.0'} + RequiredModules = @( + @{ModuleName = 'PSFramework'; ModuleVersion = '1.12.346' }, + @{ModuleName = 'PSAppInsights'; ModuleVersion = '0.9.6' }, + @{ModuleName = 'ExchangeOnlineManagement'; ModuleVersion = '3.0.0' }, + @{ModuleName = 'Microsoft.Graph.Authentication'; ModuleVersion = '2.25.0' }, + @{ModuleName = 'Microsoft.Graph.Identity.DirectoryManagement'; ModuleVersion = '2.25.0' }, + @{ModuleName = 'Microsoft.Graph.Users'; ModuleVersion = '2.25.0' }, + @{ModuleName = 'Microsoft.Graph.Applications'; ModuleVersion = '2.25.0' }, + @{ModuleName = 'Microsoft.Graph.Identity.Signins'; ModuleVersion = '2.25.0' }, + @{ModuleName = 'Microsoft.Graph.Reports'; ModuleVersion = '2.25.0' } ) # Assemblies that must be loaded prior to importing this module @@ -47,40 +49,44 @@ # FormatsToProcess = @('xml\Hawk.Format.ps1xml') # Functions to export from this module - FunctionsToExport = - 'Get-HawkTenantConfiguration', - 'Get-HawkTenantEDiscoveryConfiguration', - 'Get-HawkTenantInboxRules', - 'Get-HawkTenantConsentGrants', - 'Get-HawkTenantRBACChanges', - 'Get-HawkTenantAzureAuditLog', - 'Get-HawkUserAuthHistory', - 'Get-HawkUserConfiguration', - 'Get-HawkUserEmailForwarding', - 'Get-HawkUserInboxRule', - 'Get-HawkUserMailboxAuditing', - 'Initialize-HawkGlobalObject', - 'Search-HawkTenantActivityByIP', - 'Search-HawkTenantEXOAuditLog', - 'Show-HawkHelp', - 'Start-HawkTenantInvestigation', - 'Start-HawkUserInvestigation', - 'Update-HawkModule', - 'Get-HawkUserAdminAudit', - 'Get-HawkTenantAuthHistory', - 'Get-HawkUserHiddenRule', - 'Get-HawkMessageHeader', - 'Get-HawkUserPWNCheck', - 'Get-HawkUserAutoReply', - 'Get-HawkUserMessageTrace', - 'Get-HawkUserMobileDevice', - 'Get-HawkTenantAZAdmins', - 'Get-HawkTenantEXOAdmins', - 'Get-HawkTenantMailItemsAccessed', - 'Get-HawkTenantAppAndSPNCredentialDetails', - 'Get-HawkTenantAzureADUsers', - 'Get-HawkTenantDomainActivity', - 'Get-HawkTenantEDiscoveryLogs' + FunctionsToExport = + 'Get-HawkTenantConfiguration', + 'Get-HawkTenantEDiscoveryConfiguration', + 'Get-HawkTenantInboxRule', + 'Get-HawkTenantConsentGrant', + 'Get-HawkTenantRBACChange', + 'Get-HawkTenantAzureAppAuditLog', + 'Get-HawkUserAuthHistory', + 'Get-HawkUserConfiguration', + 'Get-HawkUserEmailForwarding', + 'Get-HawkUserInboxRule', + 'Get-HawkUserMailboxAuditing', + 'Search-HawkTenantActivityByIP', + 'Get-HawkTenantAdminInboxRuleCreation', + 'Get-HawkTenantAdminInboxRuleModification', + 'Get-HawkTenantAdminInboxRuleRemoval', + 'Get-HawkTenantAdminMailboxPermissionChange', + 'Get-HawkTenantAdminEmailForwardingChange', + 'Show-HawkHelp', + 'Start-HawkTenantInvestigation', + 'Start-HawkUserInvestigation', + 'Update-HawkModule', + 'Get-HawkUserAdminAudit', + 'Get-HawkTenantAuditLog', + 'Get-HawkTenantAuthHistory', + 'Get-HawkUserHiddenRule', + 'Get-HawkMessageHeader', + 'Get-HawkUserPWNCheck', + 'Get-HawkUserAutoReply', + 'Get-HawkUserMessageTrace', + 'Get-HawkUserMobileDevice', + 'Get-HawkTenantEntraIDAdmin', + 'Get-HawkTenantEXOAdmin', + 'Get-HawkTenantMailItemsAccessed', + 'Get-HawkTenantAppAndSPNCredentialDetail', + 'Get-HawkTenantEntraIDUser', + 'Get-HawkTenantDomainActivity', + 'Get-HawkTenantEDiscoveryLog' # Cmdlets to export from this module # CmdletsToExport = '' @@ -92,31 +98,31 @@ # AliasesToExport = '' # List of all modules packaged with this module - ModuleList = @() + ModuleList = @() # List of all files packaged with this module - FileList = @() + FileList = @() # Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ + PrivateData = @{ #Support for PowerShellGet galleries. PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. - Tags = @("O365","Security","Audit","Breach","Investigation","Exchange","EXO","Compliance","Logon","M365","Incident-Response","Solarigate") + Tags = @("O365", "Security", "Audit", "Breach", "Investigation", "Exchange", "EXO", "Compliance", "Logon", "M365", "Incident-Response", "Solarigate") # A URL to the license for this module. - LicenseUri = 'https://github.com/T0pCyber/Hawk/LICENSE' + LicenseUri = 'https://github.com/T0pCyber/hawk/blob/master/LICENSE' # A URL to the main website for this project. - ProjectUri = 'https://github.com/T0pCyber/Hawk' + ProjectUri = 'https://github.com/T0pCyber/Hawk' # A URL to an icon representing this module. - IconUri = 'https://i.ibb.co/XXH4500/Hawk.png' + IconUri = 'https://i.ibb.co/XXH4500/Hawk.png' # ReleaseNotes of this module - ReleaseNotes = 'https://github.com/T0pCyber/Hawk/Hawk/changelog.md' + ReleaseNotes = 'https://github.com/T0pCyber/hawk/blob/master/Hawk/changelog.md' } # End of PSData hashtable diff --git a/Hawk/changelog.md b/Hawk/changelog.md index 0812d85..223a63c 100644 --- a/Hawk/changelog.md +++ b/Hawk/changelog.md @@ -1,39 +1,90 @@ # Changelog ## 2.0.0 (2021-01-05) + - Initial Transmigrated Release with new owner + ## 2.0.1 (2021-02-07) + - Incorporated workflow and pester tests - Readme file updated with https://cloudforensicator.com link - Updated Azure AD SKU options that identity "Premium Licensing" - Issue #25 - Unified Audit Log AuditData JSON parsing added to "Exchange_UAL_Audit.csv" ## 2.0.2 (2021-05-05) + - Fixed Hidden Mailbox Rule EWS Credential - Updated Robust Cloud Command version to 2.0.1 - Updated Get-HawkTenantInboxRules.ps1 to new switch in update Robust Cloud Command - Deprecate "Get-HawkTenantAzureAuthenticationLogs" from Hawk. Azure AD Graph was deprecated and no longer supported. Currently -seeking alternate solution to retrieve Azure AD Sign-in logs. + seeking alternate solution to retrieve Azure AD Sign-in logs. - Removed dependency of Cloud Connect - Added dependency of Exchange Online Management V2 PowerShell module and updated functions to reflect ## 2.0.3.1 (2021-05-05) + - Fixed MSOnline Requirement to manifest ## 3.0.0 (2022-04-09) + - Updated community pull requests -a. Encoding to UTF8 - Enhancement - TakayukiTomatsuri -b. Updated $RangeEnd to datetime - Bug - cfc-zcarter -c. Updated Sweep variable - Bug -d. Added Default Tenant Name to Hawk folder name - Issue#86 - Enhancement - Snickasaurus -e. Updated Get-HawkTenantEXOAdmins to accurately list admins that is a group +- Encoding to UTF8 - Enhancement - TakayukiTomatsuri +- Updated $RangeEnd to datetime - Bug - cfc-zcarter +- Updated Sweep variable - Bug +- Added Default Tenant Name to Hawk folder name - Issue#86 - Enhancement - Snickasaurus +- Updated Get-HawkTenantEXOAdmins to accurately list admins that is a group ## 3.1.0 (2023-03-30) -a. Updated community pull requests fixing typo -b. Updated Get-HawkTenantAuditLog.ps1 to Get-HawkTenantAppAuditLog.ps1 -c. Added "Get-HawkTenantDomainActivity" function - This function will pull domain config changes from the UAL -d. Added "Get-HawkTenantEDiscoveryLogs" function - This function will pull EDiscovery logs from the UAL -e. Added Export of JSON to "Out-Multifileype" function. This will export returned results to JSON file for further ingestion into a SIEM or other data analysis platform -f. Remove MSOnline requirements -g. Added MS Graph requirements to replace MSOnline -h. Fixed path for System.Net.IPNetwork.dll \ No newline at end of file + +- Updated community pull requests fixing typo +- Updated Get-HawkTenantAuditLog.ps1 to Get-HawkTenantAppAuditLog.ps1 +- Added "Get-HawkTenantDomainActivity" function - This function will pull domain config changes from the UAL +- Added "Get-HawkTenantEDiscoveryLog" function - This function will pull EDiscovery logs from the UAL +- Added Export of JSON to "Out-Multifileype" function. This will export returned results to JSON file for further ingestion into a SIEM or other data analysis platform +- Remove MSOnline requirements +- Added MS Graph requirements to replace MSOnline +- Fixed path for System.Net.IPNetwork.dll + +## 3.1.1 (2024-10-24) + +- Removed Cloud Connect references +- Removed Zipcode from Get-HawkUserAuthHistory.ps1 - IPStack doesn't retrieve Zipcode +- Removed Continent Code from Get-IPGeoLocation +- Removed Test-MSOnlineConnection.ps1 - MSOnline requirements have been removed from Hawk +- Added logging filepath checking the Start-HawkUserInvestigation.ps1 +- Updated Get-HawkTenantAZAdmins.ps1. Removed AzureAD module. Added MS Graph cmdlets. +- Updated contact email + +## 3.1.2 (2024-12-01) + +- Removed Robust Cloud Command from build as it was not being used in the code base anymore +- Updated PowerShell API key in GitHub to fix build.yml issue where the Hawk would not publish to gallery on merge to main + +## 3.2.3 (2025-1-07) + +- Replaced all AzureAD functionality with Microsoft Graph commands. +- Updated Get-HawkTenantAZAdmins to use Microsoft Graph. +- Migrated Get-HawkTenantConsentGrant to Graph commands. +- Removed AzureAD Dependencies: Eliminated AzureAD references in the Hawk.psd1 manifest and removed the deprecated Test-AzureADConnection.ps1. +- Simplified Authentication: Streamlined Graph API connections by removing unnecessary commands like Select-MgProfile and improving Test-GraphConnection. +- Improved Logging and Naming: Standardized log outputs (e.g., AzureADUsers to EntraIDUsers) and aligned function outputs with updated naming conventions. +- Removed Search-HawkTenantEXOAuditLog as it was deprecated and replaced with modern, modular functions, as listed below: +- Added Get-HawkTenantAdminInboxRuleModification, which retrieves audit log entries for inbox rules that were historically modified within the tenant. +- Added Get-HawkTenantAdminEmailForwardingChange, which retrieves audit log entries for email forwarding changes made within the tenant. +- Added Get-HawkTenantAdminInboxRuleCreation, which retrieves audit log entries for inbox rules that were historically created within the tenant. +- Added Get-HawkTenantAdminInboxRuleRemoval, which retrieves audit log entries for inbox rules that were removed within the tenant. +- Added Get-HawkTenantAdminMailboxPermissionChange, which retrieves audit log entries for mailbox permission changes within the tenant. +- Added internal helper function Test-SuspiciousInboxRule, which detects suspicious inbox rule patterns. +- Removed ability to detect RBAC Application Impersonation as this is being deprecated / removed in FEB 2025. +- Updated Out-Log file, adding -Information parameter for tagging prompts with INFO for status updates. +- Updated Out-Log file, modifying -Notice parameter for tagging prompts with INVESTIGATE in brackets instead of asterisks, for uniformity purposes. +- Corrected HawkUserPWNCheck to accept proper input and provide user with accurate errors. + +## 3.2.4 (2025-1-08) + +- Fixed critical lack of avian artwork in startup sequence by adding ASCII welcome banner. +- Updated all post Global Hawk Variable Initialized code to use prompt tagging. +- Modified Out-MultipleFileType to not output "appending to file" to STDOUT repetitively. +- Updated Get-HawkTenantEDiscoveryConfiguration to use non-deprecated means of collecting / analyzing eDiscovery role assignments. +- Updated Change Log URI. +- Removed improperly formatted JSON from Get-HawkTenantAdminInboxRuleHistory, Get-HawkTenantAdminInboxRuleRemoval, Get-HawkTenantRBACChange, Get-HawkUserAdminAudit, Search-HawkTenantEXOAuditLog diff --git a/Hawk/functions/Tenant/Get-HawkTenantAZAdmins.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAZAdmins.ps1 deleted file mode 100644 index 46b1deb..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantAZAdmins.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -Function Get-HawkTenantAZAdmins{ -<# -.SYNOPSIS - Tenant Azure Active Directory Administrator export. Must be connected to Azure-AD using the Connect-AzureAD cmdlet -.DESCRIPTION - Tenant Azure Active Directory Administrator export. Reviewing administrator access is key to knowing who can make changes - to the tenant and conduct other administrative actions to users and applications. -.EXAMPLE - Get-HawkTenantAZAdmins - Gets all Azure AD Admins -.OUTPUTS - AzureADAdministrators.csv -.LINK - https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureaddirectoryrolemember?view=azureadps-2.0 -.NOTES -#> -BEGIN{ - #Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { - Initialize-HawkGlobalObject - } - Out-LogFile "Gathering Azure AD Administrators" - - Test-AzureADConnection - Send-AIEvent -Event "CmdRun" -} -PROCESS{ - $roles = foreach ($role in Get-AzureADDirectoryRole){ - $admins = (Get-AzureADDirectoryRoleMember -ObjectId $role.objectid).userprincipalname - if ([string]::IsNullOrWhiteSpace($admins)) { - [PSCustomObject]@{ - AdminGroupName = $role.DisplayName - Members = "No Members" - } - } - foreach ($admin in $admins){ - [PSCustomObject]@{ - AdminGroupName = $role.DisplayName - Members = $admin - } - } - } - $roles | Out-MultipleFileType -FilePrefix "AzureADAdministrators" -csv -json - -} -END{ - Out-LogFile "Completed exporting Azure AD Admins" -} -}#End Function \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 new file mode 100644 index 0000000..097a8c1 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 @@ -0,0 +1,212 @@ +Function Get-HawkTenantAdminEmailForwardingChange { + <# + .SYNOPSIS + Retrieves audit log entries for email forwarding changes made within the tenant. + + .DESCRIPTION + This function queries the Microsoft 365 Unified Audit Log for events related to email + forwarding configuration changes (Set-Mailbox with forwarding parameters). It focuses on + tracking when and by whom forwarding rules were added or modified, helping identify potential + unauthorized data exfiltration attempts. + + Key points: + - Monitors changes to both ForwardingAddress and ForwardingSMTPAddress settings + - Resolves recipient information for ForwardingAddress values + - Flags all forwarding changes for review as potential security concerns + - Provides historical context for forwarding configuration changes + + .OUTPUTS + File: Simple_Forwarding_Changes.csv/.json + Path: \Tenant + Description: Simplified view of forwarding configuration changes. + + File: Forwarding_Changes.csv/.json + Path: \Tenant + Description: Detailed audit log data for forwarding changes. + + File: Forwarding_Recipients.csv/.json + Path: \Tenant + Description: List of unique forwarding destinations configured. + + .EXAMPLE + Get-HawkTenantAdminEmailForwardingChange + + Retrieves all email forwarding configuration changes from the audit logs within the specified + search window. + #> + [CmdletBinding()] + param() + + # Test the Exchange Online connection to ensure the environment is ready for operations. + Test-EXOConnection + # Log the execution of the function for audit and telemetry purposes. + Send-AIEvent -Event "CmdRun" + + # Initialize timing variables for status updates + $startTime = Get-Date + $lastUpdate = $startTime + + # Log the start of the analysis process for email forwarding configuration changes. + Out-LogFile "Analyzing email forwarding configuration changes from audit logs" -Action + + # Ensure the tenant-specific folder exists to store output files. If not, create it. + $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $TenantPath)) { + New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null + } + + try { + # Define both operations and broader search terms to cast a wider net. + $searchCommand = @" +Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations @( + 'Set-Mailbox', + 'Set-MailUser', + 'Set-RemoteMailbox', + 'Enable-RemoteMailbox' +) +"@ + + # Fetch all specified operations from the audit log + [array]$AllMailboxChanges = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand + + # Log search completion time + Out-LogFile "Unified Audit Log search completed" -Information + + Out-LogFile "Filtering results for forwarding changes..." -Action + + # Enhanced filtering to catch more types of forwarding changes + [array]$ForwardingChanges = $AllMailboxChanges | Where-Object { + $auditData = $_.AuditData | ConvertFrom-Json + $parameters = $auditData.Parameters + ($parameters | Where-Object { + $_.Name -in @( + 'ForwardingAddress', + 'ForwardingSMTPAddress', + 'ExternalEmailAddress', + 'PrimarySmtpAddress', + 'RedirectTo', # Added from other LLM suggestion + 'DeliverToMailboxAndForward', # Corrected parameter name + 'DeliverToAndForward' # Alternative parameter name + ) -or + # Check for parameter changes enabling forwarding + ($_.Name -eq 'DeliverToMailboxAndForward' -and $_.Value -eq 'True') -or + ($_.Name -eq 'DeliverToAndForward' -and $_.Value -eq 'True') + }) + } + + Out-LogFile "Completed filtering for forwarding changes" -Information + + if ($ForwardingChanges.Count -gt 0) { + # Log the number of forwarding configuration changes found. + Out-LogFile ("Found " + $ForwardingChanges.Count + " change(s) to user email forwarding") -Information + + # Parse the audit data into a simpler format for further processing and output. + $ParsedChanges = $ForwardingChanges | Get-SimpleUnifiedAuditLog + if ($ParsedChanges) { + # Write the simplified data for quick analysis and review. + $ParsedChanges | Out-MultipleFileType -FilePrefix "Simple_Forwarding_Changes" -csv -json -Notice + + # Write the full audit log data for comprehensive records. + $ForwardingChanges | Out-MultipleFileType -FilePrefix "Forwarding_Changes" -csv -json -Notice + + # Initialize an array to store processed forwarding destination data. + $ForwardingDestinations = @() + + Out-LogFile "Beginning detailed analysis of forwarding changes..." -Action + foreach ($change in $ParsedChanges) { + # Add a status update every 30 seconds + $currentTime = Get-Date + if (($currentTime - $lastUpdate).TotalSeconds -ge 30) { + Out-LogFile "Processing forwarding changes... ($($ForwardingDestinations.Count) destinations found so far)" -Action + $lastUpdate = $currentTime + } + + $targetUser = $change.ObjectId + + # Process ForwardingSMTPAddress changes if detected in the audit log. + if ($change.Parameters -match "ForwardingSMTPAddress") { + $smtpAddress = ($change.Parameters | Select-String -Pattern "ForwardingSMTPAddress:\s*([^,]+)").Matches.Groups[1].Value + if ($smtpAddress) { + # Add the SMTP forwarding configuration to the destinations array. + $ForwardingDestinations += [PSCustomObject]@{ + UserModified = $targetUser + TargetSMTPAddress = $smtpAddress.Split(":")[-1].Trim() # Remove "SMTP:" prefix if present. + ChangeType = "SMTP Forwarding" + ModifiedBy = $change.UserId + ModifiedTime = $change.CreationTime + } + } + } + + # Process ForwardingAddress changes if detected in the audit log. + if ($change.Parameters -match "ForwardingAddress") { + $forwardingAddress = ($change.Parameters | Select-String -Pattern "ForwardingAddress:\s*([^,]+)").Matches.Groups[1].Value + if ($forwardingAddress) { + try { + # Attempt to resolve the recipient details from Exchange Online. + $recipient = Get-EXORecipient $forwardingAddress -ErrorAction Stop + + # Determine the recipient's type and extract the appropriate address. + $targetAddress = switch ($recipient.RecipientType) { + "MailContact" { $recipient.ExternalEmailAddress.Split(":")[-1] } + default { $recipient.PrimarySmtpAddress } + } + + # Add the recipient forwarding configuration to the destinations array. + $ForwardingDestinations += [PSCustomObject]@{ + UserModified = $targetUser + TargetSMTPAddress = $targetAddress + ChangeType = "Recipient Forwarding" + ModifiedBy = $change.UserId + ModifiedTime = $change.CreationTime + } + } + catch { + # Log a warning if the recipient cannot be resolved. + Out-LogFile "Unable to resolve forwarding recipient: $forwardingAddress" -isError + # Add an unresolved entry for transparency in the output. + $ForwardingDestinations += [PSCustomObject]@{ + UserModified = $targetUser + TargetSMTPAddress = "UNRESOLVED:$forwardingAddress" + ChangeType = "Recipient Forwarding (Unresolved)" + ModifiedBy = $change.UserId + ModifiedTime = $change.CreationTime + } + } + } + } + } + + + Out-LogFile "Completed processing forwarding changes" -Information + + if ($ForwardingDestinations.Count -gt 0) { + # Log the total number of forwarding destinations detected. + Out-LogFile ("Found " + $ForwardingDestinations.Count + " forwarding destinations configured") -Information + # Write the forwarding destinations data to files for review. + $ForwardingDestinations | Out-MultipleFileType -FilePrefix "Forwarding_Recipients" -csv -json -Notice + + # Log details about each forwarding destination for detailed auditing. + foreach ($dest in $ForwardingDestinations) { + Out-LogFile "Forwarding configured: $($dest.UserModified) -> $($dest.TargetSMTPAddress) ($($dest.ChangeType)) by $($dest.ModifiedBy) at $($dest.ModifiedTime)" -Notice + } + } + } + else { + # Log a warning if the parsing of audit data fails. + Out-LogFile "Error: Failed to parse forwarding change audit data" -isError + } + } + else { + # Log a message if no forwarding changes are found in the logs. + Out-LogFile "No forwarding changes found in filtered results" -Information + Out-LogFile "Retrieved $($AllMailboxChanges.Count) total operations, but none involved forwarding changes" -Information + } + } + catch { + # Log an error if the analysis encounters an exception. + Out-LogFile "Error analyzing email forwarding changes: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } +} + diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 new file mode 100644 index 0000000..8824351 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 @@ -0,0 +1,106 @@ +Function Get-HawkTenantAdminInboxRuleCreation { + <# + .SYNOPSIS + Retrieves audit log entries for inbox rules that were historically created within the tenant. + + .DESCRIPTION + This function queries the Microsoft 365 Unified Audit Log for events classified as inbox + rule creation (New-InboxRule). It focuses on historical record-keeping and identifying + potentially suspicious rules that were created. The logged events do not indicate the + specific method or interface used to create the rules. + + Key points: + - Displays creation events for inbox rules, including who created them and when. + - Flags created rules that appear suspicious (e.g., rules that forward externally, delete + messages, or filter based on suspicious keywords). + - Does not confirm whether the rules are currently active or still exist. + + For current, active rules, use Get-HawkTenantInboxRules. + + .OUTPUTS + File: Simple_Admin_Inbox_Rules_Creation.csv/.json + Path: \Tenant + Description: Simplified view of created inbox rule events. + + File: Admin_Inbox_Rules_Creation.csv/.json + Path: \Tenant + Description: Detailed audit log data for created inbox rules. + + File: _Investigate_Admin_Inbox_Rules_Creation.csv/.json + Path: \Tenant + Description: A subset of historically created rules flagged as suspicious. + + .EXAMPLE + Get-HawkTenantAdminInboxRuleCreation + + Retrieves events for all admin inbox rules created and available within the audit logs within the configured search window. + + Remarks: This basic example pulls all inbox rule creations from the audit log and analyzes them for + suspicious patterns. Output files will be created in the configured Hawk output directory under + the Tenant subfolder. + #> + [CmdletBinding()] + param() + + Test-EXOConnection + Send-AIEvent -Event "CmdRun" + + Out-LogFile "Analyzing admin inbox rule creation from audit logs" -Action + + # Create tenant folder if it doesn't exist + $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $TenantPath)) { + New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null + } + + try { + # Search for new inbox rules + Out-LogFile "Searching audit logs for inbox rule creation events" -Action + $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations 'New-InboxRule'" + [array]$NewInboxRules = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand + + if ($NewInboxRules.Count -gt 0) { + Out-LogFile ("Found " + $NewInboxRules.Count + " admin inbox rule changes in audit logs") -Information + + # Process and output the results + $ParsedRules = $NewInboxRules | Get-SimpleUnifiedAuditLog + if ($ParsedRules) { + Out-LogFile "Writing parsed admin inbox rule creation data" -Action + $ParsedRules | Out-MultipleFileType -FilePrefix "Simple_Admin_Inbox_Rules_Creation" -csv -json + $NewInboxRules | Out-MultipleFileType -FilePrefix "Admin_Inbox_Rules_Creation" -csv -json + + # Check for suspicious rules using the helper function + $SuspiciousRules = $ParsedRules | Where-Object { + $reasons = @() + Test-SuspiciousInboxRule -Rule $_ -Reasons ([ref]$reasons) + } + + if ($SuspiciousRules) { + Out-LogFile "Found suspicious admin inbox rule creation requiring investigation" -Notice + + Out-LogFile "Writing suspicious rule creation data" -Action + $SuspiciousRules | Out-MultipleFileType -FilePrefix "_Investigate_Admin_Inbox_Rules_Creation" -csv -json -Notice + + # Log details about why each rule was flagged + foreach ($rule in $SuspiciousRules) { + $reasons = @() + if (Test-SuspiciousInboxRule -Rule $rule -Reasons ([ref]$reasons)) { + Out-LogFile "Found suspicious rule creation: '$($rule.Param_Name)' created by $($rule.UserId) at $($rule.CreationTime)" -Notice + Out-LogFile "Reasons for investigation: $($reasons -join '; ')" -Notice + } + } + } + } + else { + Out-LogFile "Error: Failed to parse inbox rule audit data" -isError + } + } + else { + Out-LogFile "No admin inbox rule creation events found in audit logs" -Information + } + } + catch { + Out-LogFile "Error analyzing admin inbox rule creation: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 new file mode 100644 index 0000000..ccb1829 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 @@ -0,0 +1,109 @@ +Function Get-HawkTenantAdminInboxRuleModification { + <# + .SYNOPSIS + Retrieves audit log entries for inbox rules that were historically modified within the tenant. + + .DESCRIPTION + This function queries the Microsoft 365 Unified Audit Logs for events classified as + inbox rule modification (Set-InboxRule). It focuses on past changes to existing rules, + helping identify suspicious modifications (e.g., forwarding to external addresses, + enabling deletion, or targeting sensitive keywords). + + The logged events do not indicate how or where the modification took place, only that + an inbox rule was changed at a given time by a specific account. + + Key points: + - Shows modification events for inbox rules, including who modified them and when. + - Flags modifications that may be suspicious based on predefined criteria. + - Does not indicate whether the rules are currently active or still exist. + + For current, active rules, use Get-HawkTenantInboxRules. + + .OUTPUTS + File: Simple_Admin_Inbox_Rules_Modification.csv/.json + Path: \Tenant + Description: Simplified view of inbox rule modification events. + + File: Admin_Inbox_Rules_Modification.csv/.json + Path: \Tenant + Description: Detailed audit log data for modified inbox rules. + + File: _Investigate_Admin_Inbox_Rules_Modification.csv/.json + Path: \Tenant + Description: A subset of historically modified rules flagged as suspicious. + + .EXAMPLE + Get-HawkTenantAdminInboxRuleModification + + Retrieves events for all admin inbox rules modified and available within the audit logs within the configured search window. + + Remarks: This basic example pulls all inbox rule modification logs from the audit log and analyzes them for + suspicious patterns. Output files will be created in the configured Hawk output directory under + the Tenant subfolder. + #> + #> + [CmdletBinding()] + param() + + Test-EXOConnection + Send-AIEvent -Event "CmdRun" + + Out-LogFile "Analyzing admin inbox rule modifications from audit logs" -Action + + # Create tenant folder if it doesn't exist + $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $TenantPath)) { + New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null + } + + try { + # Search for modified inbox rules + Out-LogFile "Searching audit logs for inbox rule modification events" -Action + $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations 'Set-InboxRule'" + [array]$ModifiedInboxRules = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand + + if ($ModifiedInboxRules.Count -gt 0) { + Out-LogFile ("Found " + $ModifiedInboxRules.Count + " admin inbox rule modifications in audit logs") -Information + + # Process and output the results + $ParsedRules = $ModifiedInboxRules | Get-SimpleUnifiedAuditLog + if ($ParsedRules) { + Out-LogFile "Writing parsed admin inbox rule modification data" -Action + $ParsedRules | Out-MultipleFileType -FilePrefix "Simple_Admin_Inbox_Rules_Modification" -csv -json + $ModifiedInboxRules | Out-MultipleFileType -FilePrefix "Admin_Inbox_Rules_Modification" -csv -json + + # Check for suspicious modifications using the helper function + $SuspiciousModifications = $ParsedRules | Where-Object { + $reasons = @() + Test-SuspiciousInboxRule -Rule $_ -Reasons ([ref]$reasons) + } + + if ($SuspiciousModifications) { + Out-LogFile "Found suspicious rule modifications requiring investigation" -Notice + + Out-LogFile "Writing suspicious rule modification data" -Action + $SuspiciousModifications | Out-MultipleFileType -FilePrefix "_Investigate_Admin_Inbox_Rules_Modification" -csv -json -Notice + + # Log details about why each modification was flagged + foreach ($rule in $SuspiciousModifications) { + $reasons = @() + if (Test-SuspiciousInboxRule -Rule $rule -Reasons ([ref]$reasons)) { + Out-LogFile "Found suspicious rule modification: '$($rule.Param_Name)' modified by $($rule.UserId) at $($rule.CreationTime)" -Notice + Out-LogFile "Reasons for investigation: $($reasons -join '; ')" -Notice + } + } + } + } + else { + Out-LogFile "Error: Failed to parse inbox rule audit data" -isError + } + } + else { + Out-LogFile "No inbox rule modifications found in audit logs" -Information + } + } + catch { + Out-LogFile "Error analyzing admin inbox rule modifications: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 new file mode 100644 index 0000000..2bfbc4d --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 @@ -0,0 +1,109 @@ +Function Get-HawkTenantAdminInboxRuleRemoval { + <# + .SYNOPSIS + Retrieves audit log entries for inbox rules that were removed within the tenant. + + .DESCRIPTION + This function queries the Microsoft 365 Unified Audit Log for events classified as inbox + rule removal (Remove-InboxRule). It focuses on historical record-keeping and identifying + when inbox rules were removed and by whom. The logged events do not indicate the + specific method or interface used to remove the rules. + + Key points: + - Displays removal events for inbox rules, including who removed them and when. + - Flags removals that might be suspicious (e.g., rules that were forwarding externally). + - Provides historical context for rule removals during investigations. + + For current, active rules, use Get-HawkTenantInboxRules. + + .OUTPUTS + File: Simple_Admin_Inbox_Rules_Removal.csv/.json + Path: \Tenant + Description: Simplified view of removed inbox rule events. + + File: Admin_Inbox_Rules_Removal.csv/.json + Path: \Tenant + Description: Detailed audit log data for removed inbox rules. + + File: _Investigate_Admin_Inbox_Rules_Removal.csv/.json + Path: \Tenant + Description: A subset of historically removed rules flagged as suspicious. + + .EXAMPLE + Get-HawkTenantAdminInboxRuleRemoval + + Retrieves events for all removed inbox rules from the audit logs within the specified + search window, highlighting any that appear suspicious. + #> + [CmdletBinding()] + param() + + Test-EXOConnection + Send-AIEvent -Event "CmdRun" + + Out-LogFile "Analyzing admin inbox rule removals from audit logs" -Action + + # Create tenant folder if it doesn't exist + $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $TenantPath)) { + New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null + } + + try { + # Search for removed inbox rules + Out-LogFile "Searching audit logs for inbox rule removals" -action + $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations 'Remove-InboxRule'" + [array]$RemovedInboxRules = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand + + if ($RemovedInboxRules.Count -gt 0) { + Out-LogFile ("Found " + $RemovedInboxRules.Count + " admin inbox rule removals in audit logs") -Information + + # Process and output the results + $ParsedRules = $RemovedInboxRules | Get-SimpleUnifiedAuditLog + if ($ParsedRules) { + # Output simple format for easy analysis + $ParsedRules | Out-MultipleFileType -FilePrefix "Simple_Admin_Inbox_Rules_Removal" -csv -json + + # Output full audit logs for complete record + $RemovedInboxRules | Out-MultipleFileType -FilePrefix "Admin_Inbox_Rules_Removal" -csv -json + + # Check for suspicious removals + $SuspiciousRemovals = $ParsedRules | Where-Object { + $reasons = @() + Test-SuspiciousInboxRule -Rule $_ -Reasons ([ref]$reasons) + } + + if ($SuspiciousRemovals) { + Out-LogFile "Found suspicious admin inbox rule removals requiring investigation" -Notice + + # Output files with timestamps + $csvPath = Join-Path -Path $TenantPath -ChildPath "_Investigate_Admin_Inbox_Rules_Removal.csv" + $jsonPath = Join-Path -Path $TenantPath -ChildPath "_Investigate_Admin_Inbox_Rules_Removal.json" + Out-LogFile "Additional Information: $csvPath" -Notice + Out-LogFile "Additional Information: $jsonPath" -Notice + + $SuspiciousRemovals | Out-MultipleFileType -FilePrefix "_Investigate_Admin_Inbox_Rules_Removal" -csv -json -Notice + + # Log details about why each removal was flagged + foreach ($rule in $SuspiciousRemovals) { + $reasons = @() + if (Test-SuspiciousInboxRule -Rule $rule -Reasons ([ref]$reasons)) { + Out-LogFile "Found suspicious rule removal: '$($rule.Param_Name)' removed by $($rule.UserId) at $($rule.CreationTime)" -Notice + Out-LogFile "Reasons for investigation: $($reasons -join '; ')" -Notice + } + } + } + } + else { + Out-LogFile "Error: Failed to parse inbox rule removal audit data" -isError + } + } + else { + Out-LogFile "No inbox rule removals found in audit logs" -Information + } + } + catch { + Out-LogFile "Error analyzing admin inbox rule removals: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 new file mode 100644 index 0000000..2de5c15 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 @@ -0,0 +1,108 @@ +Function Get-HawkTenantAdminMailboxPermissionChange { + <# + .SYNOPSIS + Retrieves audit log entries for mailbox permission changes within the tenant. + + .DESCRIPTION + Searches the Unified Audit Log for mailbox permission changes and flags any grants + of FullAccess, SendAs, or Send on Behalf permissions for investigations. + Excludes normal system operations on Discovery Search Mailboxes. + + .OUTPUTS + File: Simple_Mailbox_Permission_Change.csv/.json + Path: \Tenant + Description: Simplified view of mailbox permission changes. + + File: Mailbox_Permission_Change.csv/.json + Path: \Tenant + Description: Detailed audit log data for permission changes. + + File: _Investigate_Mailbox_Permission_Change.csv/.json + Path: \Tenant + Description: Permission changes that granted sensitive rights. + + .EXAMPLE + Get-HawkTenantAdminMailboxPermissionChange + + Retrieves mailbox permission change events from the audit logs. + #> + [CmdletBinding()] + param() + + Test-EXOConnection + Send-AIEvent -Event "CmdRun" + + Out-LogFile "Analyzing mailbox permission changes from audit logs" -Action + + # Create tenant folder if it doesn't exist + $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $TenantPath)) { + New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null + } + + try { + # Search for mailbox permission changes + Out-LogFile "Searching audit logs for mailbox permission changes" -action + $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations 'Add-MailboxPermission','Add-RecipientPermission','Add-ADPermission'" + [array]$PermissionChanges = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand + + if ($PermissionChanges.Count -gt 0) { + Out-LogFile ("Found " + $PermissionChanges.Count + " mailbox permission changes in audit logs") -Information + + # Process and output the results + $ParsedChanges = $PermissionChanges | Get-SimpleUnifiedAuditLog + if ($ParsedChanges) { + # Output simple format for easy analysis + $ParsedChanges | Out-MultipleFileType -FilePrefix "Simple_Mailbox_Permission_Change" -csv -json + + # Output full audit logs for complete record + $PermissionChanges | Out-MultipleFileType -FilePrefix "Mailbox_Permission_Change" -csv -json + + # Check for sensitive permissions, excluding Discovery Search Mailbox system operations + $SensitiveGrants = $ParsedChanges | Where-Object { + # First check if this is potentially sensitive permission + ($_.Param_AccessRights -match 'FullAccess|SendAs' -or + $_.Operation -eq 'Add-ADPermission' -or + $_.Operation -match 'Add-RecipientPermission') -and + # Then exclude DiscoverySearchMailbox system operations + -not ( + $_.UserId -eq "NT AUTHORITY\SYSTEM (Microsoft.Exchange.ServiceHost)" -and + $_.ObjectId -like "*DiscoverySearchMailbox*" -and + $_.Param_User -like "*Discovery Management*" + ) + } + + if ($SensitiveGrants) { + Out-LogFile "Found sensitive permission grants requiring investigation" -Notice + $SensitiveGrants | Out-MultipleFileType -FilePrefix "_Investigate_Mailbox_Permission_Change" -csv -json -Notice + + # Log details about sensitive permission grants + foreach ($change in $SensitiveGrants) { + $permType = if ($change.Param_AccessRights -match 'FullAccess') { + "FullAccess" + } elseif ($change.Param_AccessRights -match 'SendAs' -or + $change.Operation -eq 'Add-ADPermission' -or + $change.Operation -match 'Add-RecipientPermission') { + "SendAs/Send on Behalf" + } else { + "Other sensitive permission" + } + + Out-LogFile "Permission change by $($change.UserId) at $($change.CreationTime)" -Notice + Out-LogFile "Details: Granted $permType to $($change.Param_User) on mailbox $($change.Param_Identity)" -Notice + } + } + } + else { + Out-LogFile "Error: Failed to parse mailbox permission audit data" -isError + } + } + else { + Out-LogFile "No mailbox permission changes found in audit logs" -Information + } + } + catch { + Out-LogFile "Error analyzing mailbox permission changes: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 new file mode 100644 index 0000000..bf29009 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 @@ -0,0 +1,150 @@ +Function Get-HawkTenantAppAndSPNCredentialDetail { + <# + .SYNOPSIS + Tenant Azure Active Directory Applications and Service Principal Credential details export using Microsoft Graph. + .DESCRIPTION + Tenant Azure Active Directory Applications and Service Principal Credential details export. Credential details can be used to + review when credentials were created for an Application or Service Principal. If a malicious user created a certificate or password + used to access corporate data, then knowing the key creation time will be instrumental to determining the time frame of when an attacker + had access to data. + .EXAMPLE + Get-HawkTenantAppAndSPNCredentialDetail + Gets all Tenant Application and Service Principal Details + .OUTPUTS + SPNCertsAndSecrets.csv + ApplicationCertsAndSecrets + .LINK + https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list + https://learn.microsoft.com/en-us/graph/api/application-list + .NOTES + Updated to use Microsoft Graph API instead of AzureAD module + #> + [CmdletBinding()] + param() + + BEGIN { + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + + # Create Tenant folder path if it doesn't exist + $tenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $tenantPath)) { + New-Item -Path $tenantPath -ItemType Directory -Force | Out-Null + } + + Test-GraphConnection + Send-AIEvent -Event "CmdRun" + + # Initialize arrays to collect all results + $spnResults = @() + $appResults = @() + + Out-LogFile "Collecting Entra ID Service Principals" -Action + try { + $spns = Get-MgServicePrincipal -All | Sort-Object -Property DisplayName + Out-LogFile "Collecting Entra ID Registered Applications" -Action + $apps = Get-MgApplication -All | Sort-Object -Property DisplayName + } + catch { + Out-LogFile "Error retrieving Service Principals or Applications: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } + } + + PROCESS { + try { + Out-LogFile "Exporting Service Principal Certificate and Password details" -Action + foreach ($spn in $spns) { + # Process key credentials + foreach ($key in $spn.KeyCredentials) { + $newapp = [PSCustomObject]@{ + AppName = $spn.DisplayName + AppObjectID = $spn.Id + KeyID = $key.KeyId + StartDate = $key.StartDateTime + EndDate = $key.EndDateTime + KeyType = $key.Type + CredType = "X509Certificate" + } + # Add to array for JSON output + $spnResults += $newapp + # Output individual record to CSV + $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -append + } + + # Process password credentials + foreach ($pass in $spn.PasswordCredentials) { + $newapp = [PSCustomObject]@{ + AppName = $spn.DisplayName + AppObjectID = $spn.Id + KeyID = $pass.KeyId + StartDate = $pass.StartDateTime + EndDate = $pass.EndDateTime + KeyType = $null + CredType = "PasswordSecret" + } + # Add to array for JSON output + $spnResults += $newapp + # Output individual record to CSV + $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -append + } + } + + # Output complete SPN results array as single JSON + if ($spnResults.Count -gt 0) { + $spnResults | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $tenantPath -ChildPath "SPNCertsAndSecrets.json") + } + + Out-LogFile "Exporting Registered Applications Certificate and Password details" -Action + foreach ($app in $apps) { + # Process key credentials + foreach ($key in $app.KeyCredentials) { + $newapp = [PSCustomObject]@{ + AppName = $app.DisplayName + AppObjectID = $app.Id + KeyID = $key.KeyId + StartDate = $key.StartDateTime + EndDate = $key.EndDateTime + KeyType = $key.Type + CredType = "X509Certificate" + } + # Add to array for JSON output + $appResults += $newapp + # Output individual record to CSV + $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -append + } + + # Process password credentials + foreach ($pass in $app.PasswordCredentials) { + $newapp = [PSCustomObject]@{ + AppName = $app.DisplayName + AppObjectID = $app.Id + KeyID = $pass.KeyId + StartDate = $pass.StartDateTime + EndDate = $pass.EndDateTime + KeyType = $pass.Type + CredType = "PasswordSecret" + } + # Add to array for JSON output + $appResults += $newapp + # Output individual record to CSV + $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -append + } + } + + # Output complete application results array as single JSON + if ($appResults.Count -gt 0) { + $appResults | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $tenantPath -ChildPath "ApplicationCertsAndSecrets.json") + } + } + catch { + Out-LogFile "Error processing credentials: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } + } + + END { + Out-Logfile "Completed exporting Azure AD Service Principal and App Registration Certificate and Password Details" -Information + } +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetails.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetails.ps1 deleted file mode 100644 index f26ce81..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetails.ps1 +++ /dev/null @@ -1,111 +0,0 @@ -Function Get-HawkTenantAppAndSPNCredentialDetails { -<# -.SYNOPSIS - Tenant Azure Active Directory Applications and Service Principal Credential details export. Must be connected to Azure-AD using the Connect-AzureAD cmdlet -.DESCRIPTION - Tenant Azure Active Directory Applications and Service Principal Credential details export. Credential details can be used to - review when credentials were created for an Application or Service Principal. If a malicious user created a certificat or password - used to access corporate data, then knowing the key creation time will intrumental to determing the time frame of when an attacker - had access to data. -.EXAMPLE - Get-HawkTenantAppAndSPNCredentialDetails - Gets all Tenant Application and Service Principal Details -.OUTPUTS - SPNCertsAndSecrets.csv - ApplicationCertsAndSecrets -.LINK - https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals - https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureadapplicationkeycredential?view=azureadps-2.0 -.NOTES -#> -BEGIN{ - #Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { - Initialize-HawkGlobalObject - } - Test-AzureADConnection - - Out-LogFile "Collecting Azure AD Service Principals" - $spns = get-azureadserviceprincipal -all $true | Sort-Object -Property DisplayName - Out-LogFile "Collecting Azure AD Registered Applications" - $apps = Get-AzureADApplication -all $true | Sort-Object -Property DisplayName -} - -PROCESS{ - Out-LogFile "Exporting Service Principal Certificate and Password details" - foreach ($spn in $spns) { - $keys = $spn.keycredentials - foreach ($key in $keys){ - $newapp = [PSCustomObject]@{ - AppName = $spn.DisplayName - AppObjectID = $spn.ObjectID - KeyID = $key.KeyID - StartDate = $key.startdate - EndDate = $key.endDate - KeyType = $Key.Type - CredType = "X509Certificate" - - } - $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -json -append - - } - } - foreach ($spn in $spns) { - $passwords = $spn.PasswordCredentials - foreach ($pass in $passwords){ - $newapp = [PSCustomObject]@{ - AppName = $spn.DisplayName - AppObjectID = $spn.ObjectID - KeyID = $pass.KeyID - StartDate = $pass.startdate - EndDate = $pass.endDate - KeyType = $null - CredType = "PasswordSecret" - } - $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -json -append - - } - - } - Out-LogFile "Exporting Registered Applications Certificate and Password details" - foreach ($app in $apps) { - $keys = $app.keycredentials - foreach ($key in $keys){ - $newapp = [PSCustomObject]@{ - AppName = $app.DisplayName - AppObjectID = $app.ObjectID - KeyID = $key.KeyID - StartDate = $key.startdate - EndDate = $key.endDate - KeyType = $Key.Type - CredType = "X509Certificate" - - } - $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -json -append - - } - - } - foreach ($app in $apps) { - $passwords = $app.PasswordCredentials - foreach ($pass in $passwords){ - $newapp = [PSCustomObject]@{ - AppName = $app.DisplayName - AppObjectID = $app.ObjectID - KeyID = $pass.KeyID - StartDate = $pass.startdate - EndDate = $pass.endDate - KeyType = $pass.Type - CredType = "PasswordSecret" - - } - $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -json -append - - } - } -}#End Process -END{ - Out-Logfile "Completed exporting Azure AD Service Principal and App Registration Certificate and Password Details" -} #End End - -}#End Function diff --git a/Hawk/functions/Tenant/Get-HawkTenantAuditLog.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAuditLog.ps1 new file mode 100644 index 0000000..11273cf --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAuditLog.ps1 @@ -0,0 +1,52 @@ +Function Get-HawkTenantAuditLog{ +<# +.SYNOPSIS +Retrieves all Azure AD audit logs for a specified tenant and exports them to a CSV file. + +.DESCRIPTION +The Get-HawkTenantAuditLogs function retrieves all Azure AD audit logs for a specified tenant using the Microsoft Graph API. The audit logs are then exported to a CSV file using the Out-MultipleFileType function from the Hawk module. + +.EXAMPLE +PS C:\> Get-HawkTenantAuditLogs + +This example retrieves all Azure AD audit logs for the "contoso.onmicrosoft.com" tenant and exports them to a CSV file. + +.NOTES +This function requires the Microsoft Graph PowerShell module and the Hawk module to be installed. You can install these modules using the following commands: + +Install-Module -Name Microsoft.Graph +Install-Module -Name Hawk + +.LINK +https://docs.microsoft.com/en-us/graph/api/resources/auditlog?view=graph-rest-1.0 + +#> +BEGIN{ + #Initializing Hawk Object if not present + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + Out-LogFile "Gathering Azure AD Audit Logs events" -Action +} +PROCESS{ + $auditLogsResponse = Get-MgAuditLogDirectoryAudit -All + foreach ($auditLog in $auditLogsResponse) { + $auditLogs += [PSCustomObject]@{ + Id = $auditLog.Id + Category = $auditLog.Category + Result = $auditLog.Result + ResultReason = $auditLog.ResultReason + ActivityDisplayName = $auditLog.ActivityDisplayName + ActivityDateTime = $auditLog.ActivityDateTime + Target = $auditLog.TargetResources[0].DisplayName + Type = $auditLog.Target.TargetResources[0].Type + UserPrincipalName = $auditLog.TargetResources[0].UserPrincipalName + UserType = $auditLog.UserType + } + } + } + END{ + $auditLogs | Sort-Object -Property ActivityDateTime | Out-MultipleFileType -FilePrefix "AzureADAuditLog" -csv -json + Out-Logfile "Completed exporting Azure AD audit logs" -Information + } +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 index 34c61b6..9d195fd 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 @@ -38,12 +38,11 @@ # Make sure the start date isn't more than 90 days in the past if ((Get-Date).adddays(-91) -gt $StartDate) { - Out-Logfile "[ERROR] - Start date is over 90 days in the past" + Out-Logfile "Start date is over 90 days in the past" -isError break } Test-EXOConnection - Send-AIEvent -Event "CmdRun" # Setup inial start and end time for the search [datetime]$CurrentStart = $StartDate @@ -65,7 +64,7 @@ # See if we have results if so push to csv file if ($null -eq $output) { - Out-LogFile ("No results found for time period " + $CurrentStart + " - " + $CurrentEnd) + Out-LogFile ("No results found for time period " + $CurrentStart + " - " + $CurrentEnd) -Information } else { $output | Out-MultipleFileType -FilePrefix "Audit_Log_Full_$prefix" -Append -csv -json diff --git a/Hawk/functions/Tenant/Get-HawkTenantAzureADUsers.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAzureADUsers.ps1 deleted file mode 100644 index 2a13527..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantAzureADUsers.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -Function Get-HawkTenantAzureADUsers{ -<# -.SYNOPSIS - This function will export all the Azure Active Directory users. -.DESCRIPTION - This function will export all the Azure Active Directory users to .csv file. This data can be used - as a reference for user presence and details about the user for additional context at a later time. This is a point - in time users enumeration. Date created can be of help when determining account creation date. -.EXAMPLE - PS C:\>Get-HawkTenantAzureADUsers - Exports all Azure AD users to .csv -.OUTPUTS - AzureADUPNS.csv -.LINK - https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureaduser?view=azureadps-2.0 -.NOTES -#> -BEGIN{ - #Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { - Initialize-HawkGlobalObject - } - Out-LogFile "Gathering Azure AD Users" - - Test-AzureADConnection - Send-AIEvent -Event "CmdRun" - -}#End BEGIN -PROCESS{ - $users = foreach ($user in (Get-AzureADUser -All $True)){ - $userproperties = $user | Select-Object userprincipalname, objectid, usertype, userstatechangedon, DirSyncEnabled, ExtensionProperty - foreach ($properties in $userproperties){ - [PSCustomObject]@{ - UserPrincipalname = $userproperties.userprincipalname - ObjectID = $userproperties.objectid - UserType = $userproperties.UserType - DateCreated = $userproperties.ExtensionProperty.createdDateTime - UserStateChangedOn = $userproperties.UserStateChangedOn - DirSyncEnabled = $userproperties.DirSyncEnabled - } - } - } - $users | Sort-Object -property UserPrincipalname | Out-MultipleFileType -FilePrefix "AzureADUsers" -csv -json -}#End PROCESS -END{ - Out-Logfile "Completed exporting Azure AD users" -}#End END - - -}#End Function \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 index 3ade16b..c68d08a 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 @@ -10,7 +10,7 @@ Get-HawkTenantConfigurationn Basic Tenant information Get-HawkTenantEDiscoveryConfiguration Looks for changes to ediscovery configuration Search-HawkTenantEXOAuditLog Searches the EXO audit log for activity - Get-HawkTenantRBACChanges Looks for changes to Roles Based Access Control + Get-HawkTenantRBACChange Looks for changes to Roles Based Access Control .OUTPUTS See help from individual cmdlets for output list. All outputs are placed in the $Hawk.FilePath directory @@ -19,15 +19,21 @@ Runs all of the tenant investigation cmdlets. #> +Begin { + #Initializing Hawk Object if not present + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + Out-LogFile "Gathering Tenant information" -Action + Test-EXOConnection +}#End BEGIN -Test-EXOConnection -Send-AIEvent -Event "CmdRun" - +PROCESS{ # Make sure our variables are null $AzureApplicationActivityEvents = $null Out-LogFile "Searching Unified Audit Logs Azure Activities" -Action -Out-LogFile "Searching for Application Activities" +Out-LogFile "Searching for Application Activities" -Action # Search the unified audit log for events related to application activity # https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants @@ -35,7 +41,7 @@ $AzureApplicationActivityEvents = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("S # If null we found no changes to nothing to do here if ($null -eq $AzureApplicationActivityEvents){ - Out-LogFile "No Application related events found in the search time frame." + Out-LogFile "No Application related events found in the search time frame." -Information } # If not null then we must have found some events so flag them @@ -59,4 +65,8 @@ else { | Out-MultipleFileType -fileprefix "Azure_Application_Audit" -csv -json -append } } +}#End PROCESS +END{ +Out-LogFile "Completed gathering Tenant App Audit Logs" -Action +}#End END } diff --git a/Hawk/functions/Tenant/Get-HawkTenantConfiguration.ps1 b/Hawk/functions/Tenant/Get-HawkTenantConfiguration.ps1 index 65eb741..a312581 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantConfiguration.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantConfiguration.ps1 @@ -64,18 +64,18 @@ #Check Audit Log Config Setting and make sure it is enabled Out-LogFile "Gathering Tenant Configuration Information" -action - Out-LogFile "Admin Audit Log" + Out-LogFile "Gathering Admin Audit Log" -action Get-AdminAuditLogConfig | Out-MultipleFileType -FilePrefix "AdminAuditLogConfig" -txt -xml - Out-LogFile "Organization Configuration" + Out-LogFile "Gathering Organization Configuration" -action Get-OrganizationConfig| Out-MultipleFileType -FilePrefix "OrgConfig" -xml -txt - Out-LogFile "Remote Domains" + Out-LogFile "Gathering Remote Domains" -action Get-RemoteDomain | Out-MultipleFileType -FilePrefix "RemoteDomain" -xml -csv -json - Out-LogFile "Transport Rules" + Out-LogFile "Gathering Transport Rules" -action Get-TransportRule | Out-MultipleFileType -FilePrefix "TransportRules" -xml -csv -json - Out-LogFile "Transport Configuration" + Out-LogFile "Gathering Transport Configuration" -action Get-TransportConfig | Out-MultipleFileType -FilePrefix "TransportConfig" -xml -csv -json } \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 b/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 new file mode 100644 index 0000000..5e5e682 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 @@ -0,0 +1,56 @@ +Function Get-HawkTenantConsentGrant { + <# +.SYNOPSIS + Gathers application grants using Microsoft Graph + +.DESCRIPTION + Uses Microsoft Graph to gather information about application and delegate grants. + Attempts to detect high risk grants for review. This function is used to identify + potentially risky application permissions and consent grants in your tenant. + +.EXAMPLE + Get-HawkTenantConsentGrant + Gathers and analyzes all OAuth grants in the tenant. + +.OUTPUTS + File: Consent_Grants.csv + Path: \Tenant + Description: Output of all consent grants with details about permissions and access + +.NOTES + This function requires the following Microsoft Graph permissions: + - Application.Read.All + - Directory.Read.All +#> + [CmdletBinding()] + param() + + Out-LogFile "Gathering OAuth / Application Grants" -Action + + Test-GraphConnection + + # Gather the grants using the internal Graph-based implementation + [array]$Grants = Get-AzureADPSPermission -ShowProgress + [bool]$flag = $false + + # Search the Grants for the listed bad grants that we can detect + if ($Grants.ConsentType -contains 'AllPrincipals') { + Out-LogFile "Found at least one 'AllPrincipals' Grant" -notice + $flag = $true + } + if ([bool]($Grants.Permission -match 'all')) { + Out-LogFile "Found at least one 'All' Grant" -notice + $flag = $true + } + + if ($flag) { + Out-LogFile 'Review the information at the following link to understand these results' -Information + Out-LogFile 'https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants' -Information + } + else { + Out-LogFile "To review this data follow:" -Information + Out-LogFile "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants" -Information + } + + $Grants | Out-MultipleFileType -FilePrefix "Consent_Grants" -csv -json +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantConsentGrants.ps1 b/Hawk/functions/Tenant/Get-HawkTenantConsentGrants.ps1 deleted file mode 100644 index 3c0baa9..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantConsentGrants.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -Function Get-HawkTenantConsentGrants { -<# -.SYNOPSIS - Gathers application grants -.DESCRIPTION - Used the script from https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants to gather information about - application and delegate grants. Attempts to detect high risk grants for review. -.OUTPUTS - File: Consent_Grants.csv - Path: \Tenant - Description: Output of all consent grants -.EXAMPLE - Get-HawkTenantConsentGrants - - Gathers Grants -#> - - Out-LogFile "Gathering Oauth / Application Grants" - - Test-AzureADConnection - Send-AIEvent -Event "CmdRun" - - # Gather the grants - # Using the script from the article https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants - [array]$Grants = Get-AzureADPSPermissions -ShowProgress - [bool]$flag = $false - - # Search the Grants for the listed bad grants that we can detect - if ($Grants.consenttype -contains 'AllPrinciples') { - Out-LogFile "Found at least one `'AllPrinciples`' Grant" -notice - $flag = $true - } - if ([bool]($Grants.permission -match 'all')){ - Out-LogFile "Found at least one `'All`' Grant" -notice - $flag = $true - } - - if ($flag){ - Out-LogFile 'Review the information at the following link to understand these results' -notice - Out-LogFile 'https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants#inventory-apps-with-access-in-your-organization' -notice - } - else { - Out-LogFile "To review this data follow:" - Out-LogFile "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants#inventory-apps-with-access-in-your-organization" - } - - $Grants | Out-MultipleFileType -FilePrefix "Consent_Grants" -csv -json -} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 b/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 index 4c455b5..e512b61 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 @@ -36,7 +36,7 @@ Function Get-HawkTenantDomainActivity { $DomainConfigurationEvents = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -RecordType 'AzureActiveDirectory' -Operations 'Set-AcceptedDomain','Add-FederatedDomain','Update Domain','Add verified domain', 'Add unverified domain', 'remove unverified domain'") # If null we found no changes to nothing to do here if ($null -eq $DomainConfigurationEvents){ - Out-LogFile "No Domain configuration changes found." + Out-LogFile "No Domain configuration changes found." -Information } # If not null then we must have found some events so flag them else{ @@ -79,6 +79,6 @@ Function Get-HawkTenantDomainActivity { } } END{ - Out-LogFile "Completed gathering Domain configuration changes" + Out-LogFile "Completed gathering Domain configuration changes" -Information } }#End Function Get-HawkTenantDomainActivity diff --git a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 index 5ed681c..3cd9472 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 @@ -1,64 +1,137 @@ Function Get-HawkTenantEDiscoveryConfiguration { -<# -.SYNOPSIS - Looks for users that have e-discovery rights. - Find any roles that have access to key edisocovery cmdlets and output the users who have those rights -.DESCRIPTION - Searches for all roles that have e-discovery cmdlets. - Searches for all users / groups that have access to those roles. -.OUTPUTS - File: EDiscoveryRoles.csv - Path: \ - Description: All roles that have access to the New-MailboxSearch and Search-Mailbox cmdlets - - File: EDiscoveryRoles.xml - Path: \XML - Description: All roles that have access to the New-MailboxSearch and Search-Mailbox cmdlets as CLI XML - - File: EDiscoveryRoleAssignments.csv - Path: \ - Description: All users that are assigned one of the discovered roles - - File: EDiscoveryRoleAssignments.xml - Path: \XML - Description: All users that are assigned one of the discovered roles as CLI XML -.EXAMPLE - Get-HawkTenantEDiscoveryConfiguration - - Runs the cmdlet against the current logged in tenant and outputs ediscovery information -#> - - Test-EXOConnection - Send-AIEvent -Event "CmdRun" - - Out-LogFile "Gathering Tenant information about E-Discovery Configuration" -action - - # Nulling our our role arrays - [array]$Roles = $null - [array]$RoleAssignements = $null - - # Look for E-Discovery Roles and who they might be assigned to - $EDiscoveryCmdlets = "New-MailboxSearch", "Search-Mailbox" - - # Find any roles that have these critical ediscovery cmdlets in them - # Bad actors with sufficient rights could have created new roles so we search for them - Foreach ($cmdlet in $EDiscoveryCmdlets) { - [array]$Roles = $Roles + (Get-ManagementRoleEntry ("*\" + $cmdlet)) - } + <# + .SYNOPSIS + Gets complete eDiscovery configuration data across built-in and custom role assignments. + + .DESCRIPTION + Retrieves comprehensive eDiscovery permissions data from two distinct sources in Exchange Online: + + 1. Built-in Exchange Online Role Groups: + - Standard eDiscovery roles like "Discovery Management" + - Pre-configured with specific eDiscovery capabilities + - Managed through Exchange admin center + - Typically used for organization-wide eDiscovery access + - Includes mailbox search and hold capabilities + - Part of Microsoft's default security model + + 2. Custom Management Role Entries: + - User-created roles with eDiscovery permissions + - Can be tailored for specific business needs + - May include subset of eDiscovery capabilities + - Often created for specialized teams or scenarios + - Requires careful monitoring for security + - May grant permissions through role assignments + - Can include cmdlets like: + * New-MailboxSearch + * Search-Mailbox + + The function captures all properties and relationships to provide a complete + view of who has eDiscovery access and how those permissions were granted. + This helps security teams audit and manage eDiscovery permissions effectively. + + .OUTPUTS + File: EDiscoveryRoles.csv/.json + Path: \Tenant + Description: Complete data about standard Exchange Online eDiscovery role groups + Contains: Role names, members, assigned permissions, creation dates, and all + associated properties for built-in eDiscovery roles + + File: CustomEDiscoveryRoles.csv/.json + Path: \Tenant + Description: Complete data about custom roles with eDiscovery permissions + Contains: Custom role definitions, assignments, scope, creation dates, and all + configurable properties for user-created roles with eDiscovery access + + .EXAMPLE + Get-HawkTenantEDiscoveryConfiguration + + Returns complete, unfiltered eDiscovery permission data showing both built-in + role groups and custom role assignments that grant eDiscovery access. + + .NOTES + Built-in roles provide consistent, pre-configured access while custom roles + offer flexibility but require more oversight. Regular review of both types + is recommended for security compliance. + #> + [CmdletBinding()] + param() - # Select just the unique entries based on role name - $UniqueRoles = Select-UniqueObject -ObjectArray $Roles -Property Role + #TO DO: UPDATE THIS FUNCTION TO FIND E-Discovery roles created via the graph API - Out-LogFile ("Found " + $UniqueRoles.count + " Roles with E-Discovery Rights") - $UniqueRoles | Out-MultipleFileType -FilePrefix "EDiscoveryRoles" -csv -xml -json + BEGIN { + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } - # Get everyone who is assigned one of these roles - Foreach ($Role in $UniqueRoles) { - [array]$RoleAssignements = $RoleAssignements + (Get-ManagementRoleAssignment -Role $Role.role -Delegating $false) + Test-EXOConnection + Send-AIEvent -Event "CmdRun" + + Out-LogFile "Gathering complete E-Discovery Configuration" -action + + # Create tenant folder if needed + $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $TenantPath)) { + New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null + } + + # Null out role arrays + [array]$Roles = $null + [array]$RoleAssignements = $null } - Out-LogFile ("Found " + $RoleAssignements.count + " Role Assignements for these Roles") - $RoleAssignements | Out-MultipleFileType -FilePreFix "EDiscoveryRoleAssignments" -csv -xml -json + PROCESS { + try { + #region Exchange Online Role Groups - Full Data + Out-LogFile "Gathering all Exchange Online role entries with eDiscovery cmdlets" -Action + + # Find any roles that have eDiscovery cmdlets + $EDiscoveryCmdlets = "New-MailboxSearch", "Search-Mailbox" + + foreach ($cmdlet in $EDiscoveryCmdlets) { + [array]$Roles = $Roles + (Get-ManagementRoleEntry ("*\" + $cmdlet)) + } + + # Select just the unique entries based on role name + if ($Roles) { + $UniqueRoles = $Roles | Sort-Object -Property Role -Unique + + Out-LogFile ("Found " + $UniqueRoles.Count + " Roles with E-Discovery Rights") -Information + + # Save complete role data + $UniqueRoles | ConvertTo-Json -Depth 100 | + Out-File (Join-Path -Path $TenantPath -ChildPath "EDiscoveryRoles.json") + $UniqueRoles | Export-Csv -Path (Join-Path -Path $TenantPath -ChildPath "EDiscoveryRoles.csv") -NoTypeInformation + # Get everyone who is assigned one of these roles + foreach ($Role in $UniqueRoles) { + [array]$RoleAssignements = $RoleAssignements + (Get-ManagementRoleAssignment -Role $Role.Role -Delegating $false) + } + if ($RoleAssignements) { + Out-LogFile ("Found " + $RoleAssignements.Count + " Role Assignments for these Roles") -Information + + # Save complete assignment data + $RoleAssignements | ConvertTo-Json -Depth 100 | + Out-File (Join-Path -Path $TenantPath -ChildPath "CustomEDiscoveryRoles.json") + $RoleAssignements | Export-Csv -Path (Join-Path -Path $TenantPath -ChildPath "CustomEDiscoveryRoles.csv") -NoTypeInformation + } + else { + Out-LogFile "No role assignments found" -Information + } + } + else { + Out-LogFile "No roles with eDiscovery cmdlets found" -Information + } + + #endregion + } + catch { + Out-LogFile "Error gathering eDiscovery configuration: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } + } + + END { + Out-LogFile "Completed gathering eDiscovery configuration" -Information + } } \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 new file mode 100644 index 0000000..fb601e8 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 @@ -0,0 +1,76 @@ +Function Get-HawkTenantEDiscoveryLog { + <# + .SYNOPSIS + Gets Unified Audit Logs (UAL) data for eDiscovery + .DESCRIPTION + Searches the Unified Audit Log (UAL) for eDiscovery events and activities. + This includes searches, exports, and management activities related to + eDiscovery cases. The function checks for any eDiscovery activities within + the timeframe specified in the Hawk global configuration object. + + The results can help identify: + * When eDiscovery searches were performed + * Who performed eDiscovery activities + * Which cases were accessed or modified + * What operations were performed + + .EXAMPLE + Get-HawkTenantEDiscoveryLog + + This will search for all eDiscovery-related activities in the Unified Audit Log + for the configured time period and export the results to CSV format. + + .EXAMPLE + $logs = Get-HawkTenantEDiscoveryLog + $logs | Where-Object {$_.Operation -eq "SearchCreated"} + + This example shows how to retrieve eDiscovery logs and filter for specific + operations like new search creation. + + .OUTPUTS + File: eDiscoveryLogs.csv + Path: \Tenant + Description: Contains all eDiscovery activities found in the UAL with fields for: + - CreationTime: When the activity occurred + - Id: Unique identifier for the activity + - Operation: Type of eDiscovery action performed + - Workload: The workload where the activity occurred + - UserID: User who performed the action + - Case: eDiscovery case name + - CaseId: Unique identifier for the eDiscovery case + - Cmdlet: Command that was executed (if applicable) + #> + # Search UAL audit logs for any Domain configuration changes + Test-EXOConnection + Send-AIEvent -Event "CmdRun" + + Out-LogFile "Gathering any eDiscovery logs" -action + + # Search UAL audit logs for any Domain configuration changes + $eDiscoveryLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -RecordType 'Discovery'") + # If null we found no changes to nothing to do here + if ($null -eq $eDiscoveryLogs) { + Out-LogFile "No eDiscovery Logs found" -Information + } + + # If not null then we must have found some events so flag them + else { + Out-LogFile "eDiscovery Log have been found." -Notice + Out-LogFile "Please review these eDiscoveryLogs.csv to validate the activity is legitimate." -Notice + # Go thru each even and prepare it to output to CSV + Foreach ($log in $eDiscoveryLogs) { + $log1 = $log.auditdata | ConvertFrom-Json + $report = $log1 | Select-Object -Property CreationTime, + Id, + Operation, + Workload, + UserID, + Case, + @{Name = 'CaseID'; Expression = { ($_.ExtendedProperties | Where-Object { $_.Name -eq 'CaseId' }).value } }, + @{Name = 'Cmdlet'; Expression = { ($_.Parameters | Where-Object { $_.Name -eq 'Cmdlet' }).value } } + + $report | Out-MultipleFileType -fileprefix "eDiscoveryLogs" -csv -append + } + + } +} diff --git a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLogs.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLogs.ps1 deleted file mode 100644 index b3126d2..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLogs.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -Function Get-HawkTenantEDiscoveryLogs{ - <# - .SYNOPSIS - Gets Unified Audit Logs (UAL) data for eDiscovery - .DESCRIPTION - Searches the UAL for eDiscovery events - - #> - # Search UAL audit logs for any Domain configuration changes - Test-EXOConnection - Send-AIEvent -Event "CmdRun" - - Out-LogFile "Gathering any eDiscovery logs" -action - - # Search UAL audit logs for any Domain configuration changes - $eDiscoveryLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -RecordType 'Discovery'") - # If null we found no changes to nothing to do here - if ($null -eq $eDiscoveryLogs){ - Out-LogFile "No eDiscovery Logs found" - } - - # If not null then we must have found some events so flag them - else { - Out-LogFile "eDiscovery Log have been found." -Notice - Out-LogFile "Please review these eDiscoveryLogs.csv to validate the activity is legitimate." -Notice - # Go thru each even and prepare it to output to CSV - Foreach ($log in $eDiscoveryLogs){ - $log1 = $log.auditdata | ConvertFrom-Json - $report = $log1 | Select-Object -Property CreationTime, - Id, - Operation, - Workload, - UserID, - Case, - @{Name='CaseID';Expression={($_.ExtendedProperties | Where-Object {$_.Name -eq 'CaseId'}).value}}, - @{Name='Cmdlet';Expression={($_.Parameters | Where-Object {$_.Name -eq 'Cmdlet'}).value}} - - $report | Out-MultipleFileType -fileprefix "eDiscoveryLogs" -csv -append - } - - } - } diff --git a/Hawk/functions/Tenant/Get-HawkTenantEXOAdmins.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEXOAdmin.ps1 similarity index 89% rename from Hawk/functions/Tenant/Get-HawkTenantEXOAdmins.ps1 rename to Hawk/functions/Tenant/Get-HawkTenantEXOAdmin.ps1 index 9d35dfd..6cd04ca 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEXOAdmins.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEXOAdmin.ps1 @@ -1,4 +1,4 @@ -Function Get-HawkTenantEXOAdmins{ +Function Get-HawkTenantEXOAdmin{ <# .SYNOPSIS Exchange Online Administrator export. Must be connected to Exchange Online using the Connect-EXO cmdlet @@ -14,7 +14,7 @@ .NOTES #> BEGIN{ - Out-LogFile "Gathering Exchange Online Administrators" + Out-LogFile "Gathering Exchange Online Administrators" -Action Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -43,7 +43,7 @@ PROCESS{ } END{ - Out-Logfile "Completed exporting Exchange Online Admins" + Out-Logfile "Completed exporting Exchange Online Admins" -Information } }#End Function diff --git a/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 new file mode 100644 index 0000000..b1f32a8 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 @@ -0,0 +1,98 @@ +Function Get-HawkTenantEntraIDAdmin { + <# + .SYNOPSIS + Tenant Microsoft Entra ID Administrator export using Microsoft Graph. + .DESCRIPTION + Tenant Microsoft Entra ID Administrator export. Reviewing administrator access is key to knowing who can make changes + to the tenant and conduct other administrative actions to users and applications. + .EXAMPLE + Get-HawkTenantEntraIDAdmin + Gets all Entra ID Admins + .OUTPUTS + EntraIDAdministrators.csv + EntraIDAdministrators.json + .LINK + https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.directorymanagement/get-mgdirectoryrole + .NOTES + Requires Microsoft.Graph.Identity.DirectoryManagement module + #> + [CmdletBinding()] + param() + + BEGIN { + # Initializing Hawk Object if not present + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + Out-LogFile "Gathering Microsoft Entra ID Administrators" -Action + + # Verify Graph API connection + Test-GraphConnection + Send-AIEvent -Event "CmdRun" + } + + PROCESS { + try { + # Retrieve all directory roles from Microsoft Graph + $directoryRoles = Get-MgDirectoryRole -ErrorAction Stop + Out-LogFile "Retrieved $(($directoryRoles | Measure-Object).Count) directory roles" -Information + + # Process each role and its members + $roles = foreach ($role in $directoryRoles) { + # Get all members assigned to current role + $members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -ErrorAction Stop + + # Handle roles with no members + if (-not $members) { + [PSCustomObject]@{ + AdminGroupName = $role.DisplayName + Members = "No Members" + MemberType = "None" # Added member type for better analysis + ObjectId = $null + } + } + else { + # Process each member of the role + foreach ($member in $members) { + # Check if member is a user + if ($member.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.user") { + [PSCustomObject]@{ + AdminGroupName = $role.DisplayName + Members = $member.AdditionalProperties.userPrincipalName + MemberType = "User" + ObjectId = $member.Id + } + } + else { + # Handle groups and service principals + [PSCustomObject]@{ + AdminGroupName = $role.DisplayName + Members = $member.AdditionalProperties.displayName + MemberType = ($member.AdditionalProperties.'@odata.type' -replace '#microsoft.graph.', '') + ObjectId = $member.Id + } + } + } + } + } + + # Export results if any roles were found + if ($roles) { + $roles | Out-MultipleFileType -FilePrefix "EntraIDAdministrators" -csv -json + Out-LogFile "Successfully exported Microsoft Entra ID Administrators data" -Information + } + else { + Out-LogFile "No administrator roles found or accessible" -Information + } + } + catch { + # Handle and log any errors during execution + Out-LogFile "Error retrieving Microsoft Entra ID Administrators: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } + } + + END { + Out-LogFile "Completed exporting Microsoft Entra ID Admins" -Information + } + } \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 new file mode 100644 index 0000000..23694c9 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 @@ -0,0 +1,65 @@ +Function Get-HawkTenantEntraIDUser { + <# + .SYNOPSIS + This function will export all the Entra ID users (formerly Azure AD users). + .DESCRIPTION + This function exports all the Entra ID users to a .csv file, focusing on properties + relevant for digital forensics and incident response. Properties include user identity, + account status, and account dates. + + Note: SignInActivity requires additional AuditLog.Read.All permission and is currently commented out. + .EXAMPLE + PS C:\>Get-HawkTenantEntraIDUser + Exports all Entra ID users with DFIR-relevant properties to .csv and .json files. + .OUTPUTS + EntraIDUsers.csv, EntraIDUsers.json + .LINK + https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=powershell + .NOTES + Updated to use Microsoft Graph SDK instead of AzureAD module. + Properties selected for DFIR relevance. + #> + BEGIN { + # Initialize the Hawk environment if not already done + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + Out-LogFile "Gathering Entra ID Users" -Action + + # Ensure we have a valid Graph connection + Test-GraphConnection + } + PROCESS { + # Get all users with specific properties needed for DFIR + # -Property parameter optimizes API call to only retrieve needed fields + $users = Get-MgUser -All -Property UserPrincipalName, # Primary user identifier + DisplayName, # User's display name + Id, # Unique object ID + AccountEnabled, # Account status (active/disabled) + CreatedDateTime, # Account creation timestamp + DeletedDateTime, # Account deletion timestamp (if applicable) + LastPasswordChangeDateTime, # Last password modification + Mail | # Primary email address + Select-Object UserPrincipalName, + DisplayName, + Id, + AccountEnabled, + CreatedDateTime, + DeletedDateTime, + LastPasswordChangeDateTime, + Mail + + # Only process if users were found + if ($users) { + # Sort by UPN and export to both CSV and JSON formats + $users | Sort-Object -Property UserPrincipalName | + Out-MultipleFileType -FilePrefix "EntraIDUsers" -csv -json + } + else { + Out-LogFile "No users found" -Information + } + } + END { + Out-Logfile "Completed exporting Entra ID users" -Information + } + } \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantInboxRule.ps1 b/Hawk/functions/Tenant/Get-HawkTenantInboxRule.ps1 new file mode 100644 index 0000000..b9ff844 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantInboxRule.ps1 @@ -0,0 +1,126 @@ +Function Get-HawkTenantInboxRule { + <# + .SYNOPSIS + Retrieves the currently active inbox rules and forwarding settings from all (or specified) mailboxes. + + .DESCRIPTION + This function directly queries each mailbox in the organization to list its currently configured + inbox rules and email forwarding settings. It provides a real-time snapshot of what rules are + active right now, as opposed to historical audit data. + + Key points: + - Directly collects the current state of each mailbox’s rules using Get-HawkUserInboxRule. + - Also gathers forwarding settings from Get-HawkUserEmailForwarding. + - Does not rely on audit logs; instead, uses live mailbox data. + + For historical records of when rules were created and past suspicious activity, use Get-HawkTenantInboxRuleHistory. + + .PARAMETER CSVPath + A CSV file specifying a list of users to query. + Expected columns: DisplayName, PrimarySMTPAddress (minimum). + + .PARAMETER UserPrincipalName + The UPN of the admin or account used to authenticate against Exchange Online. + + .OUTPUTS + This function calls Get-HawkUserInboxRule and Get-HawkUserEmailForwarding. + For detailed information about the output, see their respective help documentation. + + File: Robust.log + Path: \ + Description: The log file generated by Start-RobustCloudCommand, which is used to retrieve + the rules and forwarding information from each mailbox. + + .EXAMPLE + Start-HawkTenantInboxRules -UserPrincipalName userx@tenantdomain.onmicrosoft.com + + Retrieves the current inbox rules and forwarding for all mailboxes in the organization. + + .EXAMPLE + Start-HawkTenantInboxRules -csvpath c:\temp\myusers.csv -UserPrincipalName admin@tenantdomain.onmicrosoft.com + + Retrieves the current inbox rules and forwarding for all mailboxes listed in myusers.csv. + + .LINK + https://gallery.technet.microsoft.com/office/Start-RobustCloudCommand-69fb349e + + .NOTES + - This function shows the current (live) rules and forwarding settings. + - For historical data on when rules were created, refer to Get-HawkTenantInboxRuleHistory. + #> + + + param ( + [string]$CSVPath, + [Parameter(Mandatory = $true)] + [string]$UserPrincipalName + ) + + Test-EXOConnection + Send-AIEvent -Event "CmdRun" + + # Prompt the user that this is going to take a long time to run + $title = "Long Running Command" + $message = "Running this search can take a very long time to complete (~1min per user). `nDo you wish to continue?" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue operation" + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Exit Cmdlet" + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + # If yes log and continue + # If no log error and exit + switch ($result) { + 0 { Out-LogFile "Starting full Tenant Search" -Action} + 1 { Write-Error -Message "User Stopped Cmdlet" -ErrorAction Stop } + } + + # Get the exo PS session + $exopssession = get-pssession | Where-Object { ($_.ConfigurationName -eq 'Microsoft.Exchange') -and ($_.State -eq 'Opened') } + + # Gather all of the mailboxes + Out-LogFile "Getting all Mailboxes" -Action + + # If we don't have a value for csvpath then gather all users in the tenant + if ([string]::IsNullOrEmpty($CSVPath)) { + $AllMailboxes = Invoke-Command -Session $exopssession -ScriptBlock { Get-Recipient -RecipientTypeDetails UserMailbox -ResultSize Unlimited | Select-Object -Property DisplayName, PrimarySMTPAddress } + $Allmailboxes | Out-MultipleFileType -FilePrefix "All_Mailboxes" -csv -json + } + # If we do read that in + else { + # Import the csv with error checking + $error.clear() + $AllMailboxes = Import-Csv $CSVPath + if ($error.Count -gt 0) { + Write-Error "Problem importing csv file aborting" -ErrorAction Stop + } + } + + # Report how many mailboxes we are going to operate on + Out-LogFile ("Found " + $AllMailboxes.count + " Mailboxes") -Information + + # Path for robust log file + # $RobustLog = Join-path $Hawk.FilePath "Robust.log" + + # Build the command we are going to need to run with Start-RobustCloudCommand + # $cmd = "Start-RobustCloudCommand -UserPrincipalName " + $UserPrincipalName + " -logfile `$RobustLog -recipients `$AllMailboxes -scriptblock {Get-HawkUserInboxRule -UserPrincipalName `$input.PrimarySmtpAddress.tostring()}" + $AllMailboxes | ForEach-Object { + Start-RobustCloudCommand -UserPrincipalName $UserPrincipalName -LogFile $RobustLog -Recipients $_ -ScriptBlock { + Get-HawkUserInboxRule -UserPrincipalName $_.PrimarySmtpAddress + } + } + + + + # Invoke our Start-Robust command to get all of the inbox rules + Out-LogFile "===== Starting Robust Cloud Command to gather user inbox rules for all tenant users =====" -Action + # Out-LogFile $cmd + # Invoke-Expression $cmd + + # Build the command directly without using Invoke-Expression + $AllMailboxes | ForEach-Object { + Start-RobustCloudCommand -UserPrincipalName $UserPrincipalName -LogFile $RobustLog -Recipients $_ -ScriptBlock { + Get-HawkUserInboxRule -UserPrincipalName $_.PrimarySmtpAddress + } + } + + Out-LogFile "Process Complete" -Information +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantInboxRules.ps1 b/Hawk/functions/Tenant/Get-HawkTenantInboxRules.ps1 deleted file mode 100644 index c7e0b98..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantInboxRules.ps1 +++ /dev/null @@ -1,99 +0,0 @@ -Function Get-HawkTenantInboxRules { - <# -.SYNOPSIS - Gets inbox rules and forwarding directly from all mailboxes in the org. -.DESCRIPTION - Uses Start-RobustCloudCommand to gather data from each mailbox in the org. - Gathers inbox rules with Get-HawkUserInboxRule - Gathers forwarding with Get-HawkUserEmailForwarding -.PARAMETER CSVPath - Path to a CSV file with a list of users to run against. - CSV header should have DisplayName,PrimarySMTPAddress at minimum - -.PARAMETER UserPrincipalName - The UPN of the user that will authenticate against Exchange Online. - -.OUTPUTS - See Help for Get-HawkUserInboxRule for inbox rule output - See Help for Get-HawkUserEmailForwarding for email forwarding output - - File: Robust.log - Path: \ - Description: Logfile for Start-RobustCloudCommand - -.EXAMPLE - Start-HawkTenantInboxRules -UserPrincipalName userx@tenantdomain.onmicrosoft.com - - Runs Get-HawkUserInboxRule and Get-HawkUserEmailForwarding against all mailboxes in the org. The UserPrincipalName - is the Admin/User who is running the cmdlet. - -.EXAMPLE - Start-HawkTenantInboxRules -csvpath c:\temp\myusers.csv - - Runs Get-HawkUserInboxRule and Get-HawkUserEmailForwarding against all mailboxes listed in myusers.csv.The UserPrincipalName - is the Admin/User who is running the cmdlet. - -.LINK - https://gallery.technet.microsoft.com/office/Start-RobustCloudCommand-69fb349e -#> - - param ( - [string]$CSVPath, - [Parameter(Mandatory = $true)] - [string]$UserPrincipalName - ) - - Test-EXOConnection - Send-AIEvent -Event "CmdRun" - - # Prompt the user that this is going to take a long time to run - $title = "Long Running Command" - $message = "Running this search can take a very long time to complete (~1min per user). `nDo you wish to continue?" - $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue operation" - $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Exit Cmdlet" - $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) - $result = $host.ui.PromptForChoice($title, $message, $options, 0) - # If yes log and continue - # If no log error and exit - switch ($result) { - 0 { Out-LogFile "Starting full Tenant Search" } - 1 { Write-Error -Message "User Stopped Cmdlet" -ErrorAction Stop } - } - - # Get the exo PS session - $exopssession = get-pssession | Where-Object { ($_.ConfigurationName -eq 'Microsoft.Exchange') -and ($_.State -eq 'Opened') } - - # Gather all of the mailboxes - Out-LogFile "Getting all Mailboxes" - - # If we don't have a value for csvpath then gather all users in the tenant - if ([string]::IsNullOrEmpty($CSVPath)) { - $AllMailboxes = Invoke-Command -Session $exopssession -ScriptBlock { Get-Recipient -RecipientTypeDetails UserMailbox -ResultSize Unlimited | Select-Object -Property DisplayName, PrimarySMTPAddress } - $Allmailboxes | Out-MultipleFileType -FilePrefix "All_Mailboxes" -csv -json - } - # If we do read that in - else { - # Import the csv with error checking - $error.clear() - $AllMailboxes = Import-Csv $CSVPath - if ($error.Count -gt 0) { - Write-Error "Problem importing csv file aborting" -ErrorAction Stop - } - } - - # Report how many mailboxes we are going to operate on - Out-LogFile ("Found " + $AllMailboxes.count + " Mailboxes") - - # Path for robust log file - $RobustLog = Join-path $Hawk.FilePath "Robust.log" - - # Build the command we are going to need to run with Start-RobustCloudCommand - $cmd = "Start-RobustCloudCommand -UserPrincipalName " + $UserPrincipalName + " -logfile `$RobustLog -recipients `$AllMailboxes -scriptblock {Get-HawkUserInboxRule -UserPrincipalName `$input.PrimarySmtpAddress.tostring()}" - - # Invoke our Start-Robust command to get all of the inbox rules - Out-LogFile "===== Starting Robust Cloud Command to gather user inbox rules for all tenant users =====" - Out-LogFile $cmd - Invoke-Expression $cmd - - Out-LogFile "Process Complete" -} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantMailItemsAccessed.ps1 b/Hawk/functions/Tenant/Get-HawkTenantMailItemsAccessed.ps1 index 16ed012..4ce4025 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantMailItemsAccessed.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantMailItemsAccessed.ps1 @@ -33,7 +33,7 @@ ) BEGIN { - Out-LogFile "Starting Unified Audit Log (UAL) search for 'MailItemsAccessed'" + Out-LogFile "Starting Unified Audit Log (UAL) search for 'MailItemsAccessed'" -Action }#End Begin @@ -45,7 +45,7 @@ PROCESS{ END{ - Out-Logfile "Completed exporting MailItemsAccessed logs" + Out-Logfile "Completed exporting MailItemsAccessed logs" -Information }#End End diff --git a/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 new file mode 100644 index 0000000..5402280 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 @@ -0,0 +1,110 @@ +Function Get-HawkTenantRBACChange { + <# + .SYNOPSIS + Looks for any changes made to Role-Based Access Control (RBAC). + .DESCRIPTION + Searches the Unified Audit Logs for commands related to RBAC management including role, + role assignment, role entry, role group, and management scope changes. This helps track + administrative permission changes across the tenant. + + Uses Get-AllUnifiedAuditLogEntry to ensure complete retrieval of all audit records, + handling pagination automatically for large result sets. + + The function searches for operations including: + - Role management (New/Remove/Set-ManagementRole) + - Role assignments (New/Remove/Set-ManagementRoleAssignment) + - Management scopes (New/Remove/Set-ManagementScope) + - Role entries (New/Remove/Set-ManagementRoleEntry) + - Role groups (New/Remove/Set/Add/Remove-RoleGroup*) + + .OUTPUTS + File: Simple_RBAC_Changes.csv + Path: \Tenant + Description: Simplified view of RBAC changes in CSV format + + File: RBAC_Changes.csv + Path: \Tenant + Description: Detailed RBAC changes in CSV format + + File: RBAC_Changes.json + Path: \Tenant + Description: Raw audit data in JSON format for detailed analysis + + .EXAMPLE + Get-HawkTenantRBACChange + + Searches for and reports all RBAC changes in the tenant within the configured search window. + #> + [CmdletBinding()] + param() + + # Verify EXO connection and send telemetry + Test-EXOConnection + Send-AIEvent -Event "CmdRun" + + Out-LogFile "Gathering any changes to RBAC configuration" -action + + # Define the operations to search for + [array]$RBACOperations = @( + "New-ManagementRole", + "Remove-ManagementRole", + "New-ManagementRoleAssignment", + "Remove-ManagementRoleAssignment", + "Set-ManagementRoleAssignment", + "New-ManagementScope", + "Remove-ManagementScope", + "Set-ManagementScope", + "New-ManagementRoleEntry", + "Remove-ManagementRoleEntry", + "Set-ManagementRoleEntry", + "New-RoleGroup", + "Remove-RoleGroup", + "Set-RoleGroup", + "Add-RoleGroupMember", + "Remove-RoleGroupMember" + ) + + # Create tenant folder if it doesn't exist + $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $TenantPath)) { + New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null + } + + try { + # Build search command for Get-AllUnifiedAuditLogEntry + $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations " + + "'$($RBACOperations -join "','")'" + + Out-LogFile "Searching for RBAC changes using Unified Audit Log." -Action + + # Get all RBAC changes using Get-AllUnifiedAuditLogEntry + [array]$RBACChanges = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand + + # Process results if any found + if ($RBACChanges.Count -gt 0) { + Out-LogFile ("Found " + $RBACChanges.Count + " changes made to Roles-Based Access Control") -Information + + # Parse changes using Get-SimpleUnifiedAuditLog + $ParsedChanges = $RBACChanges | Get-SimpleUnifiedAuditLog + + # Output results if successfully parsed + if ($ParsedChanges) { + # Write simple format for easy analysis + $ParsedChanges | Out-MultipleFileType -FilePrefix "Simple_RBAC_Changes" -csv -json + + # Write full audit logs for complete record + $RBACChanges | Out-MultipleFileType -FilePrefix "RBAC_Changes" -csv -json + } + else { + Out-LogFile "Error: Failed to parse RBAC changes" -isError + } + } + else { + Out-LogFile "No RBAC changes found." -Information + } + } + catch { + Out-LogFile "Error searching for RBAC changes: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantRbacChanges.ps1 b/Hawk/functions/Tenant/Get-HawkTenantRbacChanges.ps1 deleted file mode 100644 index 908334a..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantRbacChanges.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -# Search for any changes made to RBAC in the search window and report them -Function Get-HawkTenantRBACChanges { -<# -.SYNOPSIS - Looks for any changes made to Roles Based Access Control -.DESCRIPTION - Searches the EXO Audit logs for the following commands being run. - New-ManagementRole - Remove-ManagementRole - New-ManagementRoleAssignment - Remove-ManagementRoleAssignment - Set-MangementRoleAssignment - New-ManagementScope - Remove-ManagementScope - Set-ManagementScope -.OUTPUTS - - File: Simple_RBAC_Changes.csv - Path: \ - Description: All RBAC cmdlets that were run in an easy to read format - - File: RBAC_Changes.csv - Path: \ - Description: All RBAC changes in Raw format - - File: RBAC_Changes.xml - Path: \XML - Description: All RBAC changes as a CLI XML -.EXAMPLE - Get-HawkTenantRBACChanges - - Looks for all RBAC changes in the tenant within the search window -#> - - Test-EXOConnection - Send-AIEvent -Event "CmdRun" - - Out-LogFile "Gathering any changes to RBAC configuration" -action - - # Search EXO audit logs for any RBAC changes - [array]$RBACChanges = Search-AdminAuditLog -Cmdlets New-ManagementRole, New-ManagementRoleAssignment, New-ManagementScope, Remove-ManagementRole, Remove-ManagementRoleAssignment, Set-MangementRoleAssignment, Remove-ManagementScope, Set-ManagementScope -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate - - # If there are any results push them to an output file - if ($RBACChanges.Count -gt 0) { - Out-LogFile ("Found " + $RBACChanges.Count + " Changes made to Roles Based Access Control") - $RBACChanges | Get-SimpleAdminAuditLog | Out-MultipleFileType -FilePrefix "Simple_RBAC_Changes" -csv -json - $RBACChanges | Out-MultipleFileType -FilePrefix "RBAC_Changes" -csv -xml -json - } - # Otherwise report no results found - else { - Out-LogFile "No RBAC Changes found." - } -} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 b/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 index 6a9ac3f..7625933 100644 --- a/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 +++ b/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 @@ -52,7 +52,7 @@ # Make sure we got only a single IP address if ($IpAddress -like "*,*") { - Out-LogFile "Please provide a single IP address to search." + Out-LogFile "Please provide a single IP address to search." -Information Write-Error -Message "Please provide a single IP address to search." -ErrorAction Stop } @@ -63,7 +63,7 @@ # If we didn't get anything back log it if ($null -eq $ipevents) { - Out-LogFile ("No IP logon events found for IP " + $IpAddress) + Out-LogFile ("No IP logon events found for IP " + $IpAddress) -Information } # If we did then process it @@ -71,12 +71,12 @@ # Expand out the Data and convert from JSON [array]$ipeventsexpanded = $ipevents | Select-object -ExpandProperty AuditData | ConvertFrom-Json - Out-LogFile ("Found " + $ipeventsexpanded.count + " related to provided IP" ) + Out-LogFile ("Found " + $ipeventsexpanded.count + " related to provided IP" ) -Information $ipeventsexpanded | Out-MultipleFileType -FilePrefix "All_Events" -csv -json -User $DirectoryName # Get the logon events that were a success [array]$successipevents = $ipeventsexpanded | Where-Object { $_.ResultStatus -eq "success" } - Out-LogFile ("Found " + $successipevents.Count + " Successful logons related to provided IP") + Out-LogFile ("Found " + $successipevents.Count + " Successful logons related to provided IP") -Information $successipevents | Out-MultipleFileType -FilePrefix "Success_Events" -csv -json -User $DirectoryName # Select all unique users accessed by this IP diff --git a/Hawk/functions/Tenant/Search-HawkTenantEXOAuditLog.ps1 b/Hawk/functions/Tenant/Search-HawkTenantEXOAuditLog.ps1 deleted file mode 100644 index c084318..0000000 --- a/Hawk/functions/Tenant/Search-HawkTenantEXOAuditLog.ps1 +++ /dev/null @@ -1,234 +0,0 @@ -Function Search-HawkTenantEXOAuditLog { -<# -.SYNOPSIS - Searches the admin audit logs for possible bad actor activities -.DESCRIPTION - Searches the Exchange admin audkit logs for a number of possible bad actor activies. - * New inbox rules - * Changes to user forwarding configurations - * Changes to user mailbox permissions - * Granting of impersonation rights -.OUTPUTS - - File: Simple_New_InboxRule.csv - Path: \ - Description: cmdlets to create any new inbox rules in a simple to read format - - File: New_InboxRules.xml - Path: \XML - Description: Search results for any new inbox rules in CLI XML format - - File: _Investigate_Simple_New_InboxRule.csv - Path: \ - Description: cmdlets to create inbox rules that forward or delete email in a simple format - - File: _Investigate_New_InboxRules.xml - Path: \XML - Description: Search results for newly created inbox rules that forward or delete email in CLI XML - - File: _Investigate_New_InboxRules.txt - Path: \ - Description: Search results of newly created inbox rules that forward or delete email - - File: Simple_Forwarding_Changes.csv - Path: \ - Description: cmdlets that change forwarding settings in a simple to read format - - File: Forwarding_Changes.xml - Path: \XML - Description: Search results for cmdlets that change forwarding settings in CLI XML - - File: Forwarding_Recipients.csv - Path: \ - Description: List of unique Email addresses that were setup to recieve email via forwarding - - File: Simple_Mailbox_Permissions.csv - Path: \ - Description: Cmdlets that add permissions to users in a simple to read format - - File: Mailbox_Permissions.xml - Path: \XML - Description: Search results for cmdlets that change permissions in CLI XML - - File: _Investigate_Impersonation_Roles.csv - Path: \ - Description: List all users with impersonation rights if we find more than the default of one - - File: _Investigate_Impersonation_Roles.csv - Path: \XML - Description: List all users with impersonation rights if we find more than the default of one as CLI XML - - File: Impersonation_Rights.csv - Path: \ - Description: List all users with impersonation rights if we only find the default one - - File: Impersonation_Rights.csv - Path: \XML - Description: List all users with impersonation rights if we only find the default one as CLI XML -.EXAMPLE - Search-HawkTenantEXOAuditLog - - Searches the tenant audit logs looking for changes that could have been made in the tenant. -#> - - Test-EXOConnection - Send-AIEvent -Event "CmdRun" - - Out-LogFile "Searching EXO Audit Logs" -Action - Out-LogFile "Searching Entire Admin Audit Log for Specific cmdlets" - - #Make sure our values are null - $TenantInboxRules = $Null - $TenantSetInboxRules = $Null - $TenantRemoveInboxRules = $Null - - - # Search for the creation of ANY inbox rules - Out-LogFile "Searching for ALL Inbox Rules Created in the Shell" -action - [array]$TenantInboxRules = Search-AdminAuditLog -Cmdlets New-InboxRule -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate - - # If we found anything report it and log it - if ($TenantInboxRules.count -gt 0) { - - Out-LogFile ("Found " + $TenantInboxRules.count + " Inbox Rule(s) created from PowerShell") - $TenantInboxRules | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "Simple_New_InboxRule" -csv -json - $TenantInboxRules | Out-MultipleFileType -fileprefix "New_InboxRules" -csv -json - } - - # Search for the Modification of ANY inbox rules - Out-LogFile "Searching for ALL Inbox Rules Modified in the Shell" -action - [array]$TenantSetInboxRules = Search-AdminAuditLog -Cmdlets Set-InboxRule -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate - - # If we found anything report it and log it - if ($TenantSetInboxRules.count -gt 0) { - - Out-LogFile ("Found " + $TenantSetInboxRules.count + " Inbox Rule(s) created from PowerShell") - $TenantSetInboxRules | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "Simple_Set_InboxRule" -csv -json - $TenantSetInboxRules | Out-MultipleFileType -fileprefix "Set_InboxRules" -csv -json - } - - # Search for the Modification of ANY inbox rules - Out-LogFile "Searching for ALL Inbox Rules Removed in the Shell" -action - [array]$TenantRemoveInboxRules = Search-AdminAuditLog -Cmdlets Remove-InboxRule -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate - - # If we found anything report it and log it - if ($TenantRemoveInboxRules.count -gt 0) { - - Out-LogFile ("Found " + $TenantRemoveInboxRules.count + " Inbox Rule(s) created from PowerShell") - $TenantRemoveInboxRules | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "Simple_Remove_InboxRule" -csv -json - $TenantRemoveInboxRules | Out-MultipleFileType -fileprefix "Remove_InboxRules" -csv -json - } - - # Searching for interesting inbox rules - Out-LogFile "Searching for Interesting Inbox Rules Created in the Shell" -action - [array]$InvestigateInboxRules = Search-AdminAuditLog -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate -cmdlets New-InboxRule -Parameters ForwardTo, ForwardAsAttachmentTo, RedirectTo, DeleteMessage - - # if we found a rule report it and output it to the _Investigate files - if ($InvestigateInboxRules.count -gt 0) { - Out-LogFile ("Found " + $InvestigateInboxRules.count + " Inbox Rules that should be investigated further.") -notice - $InvestigateInboxRules | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "_Investigate_Simple_New_InboxRule" -csv -json -Notice - $InvestigateInboxRules | Out-MultipleFileType -fileprefix "_Investigate_New_InboxRules" -xml -txt -Notice - } - - # Look for changes to user forwarding - Out-LogFile "Searching for user Forwarding Changes" -action - [array]$TenantForwardingChanges = Search-AdminAuditLog -Cmdlets Set-Mailbox -Parameters ForwardingAddress, ForwardingSMTPAddress -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate - - if ($TenantForwardingChanges.count -gt 0) { - Out-LogFile ("Found " + $TenantForwardingChanges.count + " Change(s) to user Email Forwarding") -notice - $TenantForwardingChanges | Get-SimpleAdminAuditLog | Out-MultipleFileType -FilePrefix "Simple_Forwarding_Changes" -csv -json -Notice - $TenantForwardingChanges | Out-MultipleFileType -FilePrefix "Forwarding_Changes" -xml -Notice - - # Make sure our output array is null - [array]$Output = $null - - # Checking if addresses were added or removed - # If added compile a list - Foreach ($Change in $TenantForwardingChanges) { - - # Get the user object modified - $user = ($Change.CmdletParameters | Where-Object ($_.name -eq "Identity")).value - - # Check the ForwardingSMTPAddresses first - if ([string]::IsNullOrEmpty(($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingSMTPAddress" }).value)) { } - # If not null then push the email address into $output - else { - [array]$Output = $Output + ($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingSMTPAddress" }) | Select-Object -Property @{Name = "UserModified"; Expression = { $user } }, @{Name = "TargetSMTPAddress"; Expression = { $_.value.split(":")[1] } } - } - - # Check ForwardingAddress - if ([string]::IsNullOrEmpty(($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingAddress" }).value)) { } - else { - # Here we get back a recipient object in EXO not an SMTP address - # So we need to go track down the recipient object - $recipient = Get-EXORecipient (($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingAddress" }).value) -ErrorAction SilentlyContinue - - # If we can't resolve the recipient we need to log that - if ($null -eq $recipient) { - Out-LogFile ("Unable to resolve forwarding Target Recipient " + ($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingAddress" })) -notice - } - # If we can resolve it then we need to push the address the mail was being set to into $output - else { - # Determine the type of recipient and handle as needed to get out the SMTP address - Switch ($recipient.RecipientType) { - # For mailcontact we needed the external email address - MailContact { - [array]$Output += $recipient | Select-Object -Property @{Name = "UserModified"; Expression = { $user } }; @{Name = "TargetSMTPAddress"; Expression = { $_.ExternalEmailAddress.split(":")[1] } } - } - # For all others I believe primary will work - Default { - [array]$Output += $recipient | Select-Object -Property @{Name = "UserModified"; Expression = { $user } }; @{Name = "TargetSMTPAddress"; Expression = { $_.PrimarySmtpAddress } } - } - } - } - } - } - - # Output our email address user modified pairs - Out-logfile ("Found " + $Output.count + " email addresses set to be forwarded mail") -notice - $Output | Out-MultipleFileType -FilePrefix "Forwarding_Recipients" -csv -json -Notice - - } - - # Look for changes to mailbox permissions - Out-LogFile "Searching for Mailbox Permissions Changes" -Action - [array]$TenantMailboxPermissionChanges = Search-AdminAuditLog -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate -cmdlets Add-MailboxPermission - - if ($TenantMailboxPermissionChanges.count -gt 0) { - Out-LogFile ("Found " + $TenantMailboxPermissionChanges.count + " changes to mailbox permissions") - $TenantMailboxPermissionChanges | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "Simple_Mailbox_Permissions" -csv -json - $TenantMailboxPermissionChanges | Out-MultipleFileType -fileprefix "Mailbox_Permissions" -xml - - ## TODO: Possibly check who was added with permissions and see how old their accounts are - } - - # Look for change to impersonation access - Out-LogFile "Searching Impersonation Access" -action - [array]$TenantImpersonatingRoles = Get-ManagementRoleEntry "*\Impersonate-ExchangeUser" - if ($TenantImpersonatingRoles.count -gt 1) { - Out-LogFile ("Found " + $TenantImpersonatingRoles.count + " Impersonation Roles. Default is 1") -notice - $TenantImpersonatingRoles | Out-MultipleFileType -fileprefix "_Investigate_Impersonation_Roles" -csv -json -xml -Notice - } - elseif ($TenantImpersonatingRoles.count -eq 0) { } - else { - $TenantImpersonatingRoles | Out-MultipleFileType -fileprefix "Impersonation_Roles" -csv -json -xml - } - - $Output = $null - # Search all impersonation roles for users that have access - foreach ($Role in $TenantImpersonatingRoles) { - [array]$Output += Get-ManagementRoleAssignment -Role $Role.role -GetEffectiveUsers -Delegating:$false - } - - if ($Output.count -gt 1) { - Out-LogFile ("Found " + $Output.count + " Users/Groups with Impersonation rights. Default is 1") -notice - $Output | Out-MultipleFileType -fileprefix "Impersonation_Rights" -csv -json -xml - $Output | Out-MultipleFileType -fileprefix "_Investigate_Impersonation_Rights" -csv -json -xml -Notice - } - elseif ($Output.count -eq 1) { - Out-LogFile ("Found default number of Impersonation users") - $Output | Out-MultipleFileType -fileprefix "Impersonation_Rights" -csv -json -xml - } - else { } - -} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 index cb01a1c..4b9bdbe 100644 --- a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 +++ b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 @@ -1,24 +1,40 @@ Function Start-HawkTenantInvestigation { -<# + <# .SYNOPSIS - Gathers common data about a tenant. + Gathers common data about a tenant. .DESCRIPTION - Runs all Hawk Basic tenant related cmdlets and gathers the data. - - Cmdlet Information Gathered - ------------------------- ------------------------- - Get-HawkTenantConfigurationn Basic Tenant information - Get-HawkTenantEDiscoveryConfiguration Looks for changes to ediscovery configuration - Search-HawkTenantEXOAuditLog Searches the EXO audit log for activity - Get-HawkTenantRBACChanges Looks for changes to Roles Based Access Control -.OUTPUTS - See help from individual cmdlets for output list. - All outputs are placed in the $Hawk.FilePath directory + Runs all Hawk Basic tenant related cmdlets and gathers data about the tenant's configuration, + security settings, and audit logs. This comprehensive investigation helps identify potential + security issues and configuration changes. + +.PARAMETER Confirm + Prompts for confirmation before running operations that could modify system state. + +.PARAMETER WhatIf + Shows what would happen if the command runs. The command is not run. + +.EXAMPLE + PS C:\> Start-HawkTenantInvestigation + Runs a complete tenant investigation, gathering all available data. + +.EXAMPLE + PS C:\> Start-HawkTenantInvestigation -WhatIf + Shows what data gathering operations would be performed without executing them. + .EXAMPLE - Start-HawkTenantInvestigation + PS C:\> Start-HawkTenantInvestigation -Confirm + Prompts for confirmation before running each data gathering operation. -R uns all of the tenant investigation cmdlets. +.OUTPUTS + Various CSV and files containing investigation results. + See help from individual cmdlets for specific output details. + All outputs are placed in the $Hawk.FilePath directory. #> + [CmdletBinding(SupportsShouldProcess)] + param() + + Write-HawkBanner + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { Initialize-HawkGlobalObject } @@ -26,39 +42,85 @@ R uns all of the tenant investigation cmdlets. Out-LogFile "Starting Tenant Sweep" -action Send-AIEvent -Event "CmdRun" - Out-LogFile "Running Get-HawkTenantConfiguration" -action - Get-HawkTenantConfiguration + # Wrap operations in ShouldProcess checks + if ($PSCmdlet.ShouldProcess("Tenant Configuration", "Get configuration data")) { + Out-LogFile "Running Get-HawkTenantConfiguration" -action + Get-HawkTenantConfiguration + } - Out-LogFile "Running Get-HawkTenantEDiscoveryConfiguration" -action - Get-HawkTenantEDiscoveryConfiguration + if ($PSCmdlet.ShouldProcess("EDiscovery Configuration", "Get eDiscovery configuration")) { + Out-LogFile "Running Get-HawkTenantEDiscoveryConfiguration" -action + Get-HawkTenantEDiscoveryConfiguration + } - Out-LogFile "Running Search-HawkTenantEXOAuditLog" -action - Search-HawkTenantEXOAuditLog + if ($PSCmdlet.ShouldProcess("Admin Inbox Rule Creation Audit Log", "Search Admin Inbox Rule Creation")) { + Out-LogFile "Running Get-HawkTenantAdminInboxRuleCreation" -action + Get-HawkTenantAdminInboxRuleCreation + } - Out-LogFile "Running Get-HawkTenantEDiscoveryLogs" - Get-HawkTenantEDiscoveryLogs -action + if ($PSCmdlet.ShouldProcess("Admin Inbox Rule Modification Audit Log", "Search Admin Inbox Rule Modification")) { + Out-LogFile "Running Get-HawkTenantInboxRuleModification" -action + Get-HawkTenantAdminInboxRuleModification + } - Out-LogFile "Running Get-HawkTenantDomainActivity" -action - Get-HawkTenantDomainActivity + if ($PSCmdlet.ShouldProcess("Admin Inbox Rule Removal Audit Log", "Search Admin Inbox Rule Removal")) { + Out-LogFile "Running Get-HawkTenantAdminInboxRuleRemoval" -action + Get-HawkTenantAdminInboxRuleRemoval + } - Out-LogFile "Running Get-HawkTenantRBACChanges" -action - Get-HawkTenantRBACChanges + if ($PSCmdlet.ShouldProcess("Admin Inbox Rule Permission Change Audit Log", "Search Admin Inbox Permission Changes")) { + Out-LogFile "Running Get-HawkTenantAdminMailboxPermissionChange" -action + Get-HawkTenantAdminMailboxPermissionChange + } + + if ($PSCmdlet.ShouldProcess("Admin Email Forwarding Change Change Audit Log", "Search Admin Email Forwarding Changes")) { + Out-LogFile "Running Get-HawkTenantAdminEmailForwardingChange" -action + Get-HawkTenantAdminEmailForwardingChange + } - Out-LogFile "Running Get-HawkTenantAzureAppAuditLog" -action - Get-HawkTenantAzureAppAuditLog - Out-LogFile "Running Get-HawkTenantEXOAdmins" -action - Get-HawkTenantEXOAdmins + if ($PSCmdlet.ShouldProcess("EDiscovery Logs", "Get eDiscovery logs")) { + Out-LogFile "Running Get-HawkTenantEDiscoveryLog" -action + Get-HawkTenantEDiscoveryLog + } + + if ($PSCmdlet.ShouldProcess("Domain Activity", "Get domain activity")) { + Out-LogFile "Running Get-HawkTenantDomainActivity" -action + Get-HawkTenantDomainActivity + } - Out-LogFile "Running Get-HawkTenantConsentGrants" -action - Get-HawkTenantConsentGrants + if ($PSCmdlet.ShouldProcess("RBAC Changes", "Get RBAC changes")) { + Out-LogFile "Running Get-HawkTenantRBACChange" -action + Get-HawkTenantRBACChange + } - Out-LogFile "Running Get-HawkTenantAZAdmins" -action - Get-HawkTenantAZAdmins + if ($PSCmdlet.ShouldProcess("Azure App Audit Log", "Get app audit logs")) { + Out-LogFile "Running Get-HawkTenantAzureAppAuditLog" -action + Get-HawkTenantAzureAppAuditLog + } - Out-LogFile "Running Get-HawkTenantAppAndSPNCredentialDetails" -action - Get-HawkTenantAppAndSPNCredentialDetails + if ($PSCmdlet.ShouldProcess("Exchange Admins", "Get Exchange admin list")) { + Out-LogFile "Running Get-HawkTenantEXOAdmin" -action + Get-HawkTenantEXOAdmin + } - Out-Logfile "Running Get-HawkTenantAzureADUsers" -action - Get-HawkTenantAzureADUsers + if ($PSCmdlet.ShouldProcess("Consent Grants", "Get consent grants")) { + Out-LogFile "Running Get-HawkTenantConsentGrant" -action + Get-HawkTenantConsentGrant + } + + if ($PSCmdlet.ShouldProcess("Entra ID Admins", "Get Entra ID admin list")) { + Out-LogFile "Running Get-HawkTenantEntraIDAdmin" -action + Get-HawkTenantEntraIDAdmin + } + + if ($PSCmdlet.ShouldProcess("App and SPN Credentials", "Get credential details")) { + Out-LogFile "Running Get-HawkTenantAppAndSPNCredentialDetail" -action + Get-HawkTenantAppAndSPNCredentialDetail + } + + if ($PSCmdlet.ShouldProcess("Entra ID Users", "Get Entra ID user list")) { + Out-LogFile "Running Get-HawkTenantEntraIDUser" -action + Get-HawkTenantEntraIDUser + } } \ No newline at end of file diff --git a/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 b/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 index 27aa490..401f595 100644 --- a/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 +++ b/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 @@ -1,25 +1,37 @@ Function Get-HawkUserAdminAudit { -<# -.SYNOPSIS - Searches the EXO Audit logs for any commands that were run against the provided user object. -.DESCRIPTION - Searches the EXO Audit logs for any commands that were run against the provided user object. - Limited by the provided search period. -.PARAMETER UserPrincipalName - UserPrincipalName of the user you're investigating -.OUTPUTS - - File: Simple_User_Changes.csv - Path: \ - Description: All cmdlets that were run against the user in a simple format. -.EXAMPLE - Get-HawkUserAdminAudit -UserPrincipalName user@company.com - - Gets all changes made to user@company.com and ouputs them to the csv and xml files. -#> - - param - ( + <# + .SYNOPSIS + Searches the Unified Audit logs for any commands that were run against the provided user object. + .DESCRIPTION + Searches the Unified Audit logs for any commands that were run against the provided user object. + Uses Get-AllUnifiedAuditLogEntry to ensure complete retrieval of all audit records within the + specified search period, handling pagination and large result sets automatically. + + .PARAMETER UserPrincipalName + UserPrincipalName of the user you're investigating. Can be a single UPN, comma-separated list, + or array of objects containing UPNs. + + .OUTPUTS + File: Simple_User_Changes.csv + Path: \ + Description: All cmdlets that were run against the user in a simple format. + + File: User_Changes.csv + Path: \ + Description: Raw data of all changes made to the user. + + .EXAMPLE + Get-HawkUserAdminAudit -UserPrincipalName user@company.com + + Gets all changes made to user@company.com and outputs them to the csv and json files. + + .EXAMPLE + Get-HawkUserAdminAudit -UserPrincipalName (Get-Mailbox -Filter {CustomAttribute1 -eq "VIP"}) + + Gets admin audit data for all mailboxes with CustomAttribute1 set to "VIP". + #> + [CmdletBinding()] + param ( [Parameter(Mandatory = $true)] [array]$UserPrincipalName ) @@ -30,28 +42,51 @@ # Verify our UPN input [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName - Foreach ($Object in $UserArray) { + foreach ($Object in $UserArray) { [string]$User = $Object.UserPrincipalName # Get the mailbox name since that is what we store in the admin audit log - $MailboxName = (Get-Mailbox -identity $User).name + $MailboxName = (Get-Mailbox -Identity $User).Name Out-LogFile ("Searching for changes made to: " + $MailboxName) -action - # Get all changes to this user from the admin audit logs - [array]$UserChanges = Search-AdminAuditLog -ObjectIDs $MailboxName -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate + try { + # Build search command for Get-AllUnifiedAuditLogEntry + $searchCommand = "Search-UnifiedAuditLog -UserIds $User -RecordType ExchangeAdmin -Operations '*'" + # Get all changes for this user using Get-AllUnifiedAuditLogEntry + [array]$UserChanges = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand - # If there are any results push them to an output file - if ($UserChanges.Count -gt 0) { - Out-LogFile ("Found " + $UserChanges.Count + " changes made to this user") - $UserChanges | Get-SimpleAdminAuditLog | Out-MultipleFileType -FilePrefix "Simple_User_Changes" -csv -json -user $User - $UserChanges | Out-MultipleFileType -FilePrefix "User_Changes" -csv -json -user $User + # If there are any results process and output them + if ($UserChanges.Count -gt 0) { + Out-LogFile ("Found " + $UserChanges.Count + " changes made to this user") -Information + + # Get the user's output folder path + $UserFolder = Join-Path -Path $Hawk.FilePath -ChildPath $User + + # Ensure user folder exists + if (-not (Test-Path -Path $UserFolder)) { + New-Item -Path $UserFolder -ItemType Directory -Force | Out-Null + } + + # Parse and format the changes using Get-SimpleUnifiedAuditLog + $ParsedChanges = $UserChanges | Get-SimpleUnifiedAuditLog + + # Output the processed results + if ($ParsedChanges) { + $ParsedChanges | Out-MultipleFileType -FilePrefix "Simple_User_Changes" -csv -json -User $User + } + + # Output the raw changes + $UserChanges | Out-MultipleFileType -FilePrefix "User_Changes" -csv -json -User $User + } + else { + Out-LogFile "No User Changes found." -Information + } } - # Otherwise report no results found - else { - Out-LogFile "No User Changes found." + catch { + Out-LogFile "Error processing audit logs for $User : $_" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue } - } -} +} \ No newline at end of file diff --git a/Hawk/functions/User/Get-HawkUserAuthHistory.ps1 b/Hawk/functions/User/Get-HawkUserAuthHistory.ps1 index ea94cfb..8996f10 100644 --- a/Hawk/functions/User/Get-HawkUserAuthHistory.ps1 +++ b/Hawk/functions/User/Get-HawkUserAuthHistory.ps1 @@ -56,18 +56,18 @@ # Get back the account logon logs for the user foreach ($Type in $RecordTypes) { - Out-LogFile ("Searching Unified Audit log for Records of type: " + $Type) + Out-LogFile ("Searching Unified Audit log for Records of type: " + $Type) -action $UserLogonLogs += Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -UserIds " + $User + " -RecordType " + $Type) } # Make sure we have results if ($null -eq $UserLogonLogs) { - Out-LogFile "[ERROR] - No results found when searching UAL for AzureActiveDirectoryAccountLogon events" + Out-LogFile "No results found when searching UAL for AzureActiveDirectoryAccountLogon events" -isError } else { # Expand out the AuditData and convert from JSON - Out-LogFile "Converting AuditData" + Out-LogFile "Converting AuditData" -action $ExpandedUserLogonLogs = $null $ExpandedUserLogonLogs = New-Object System.Collections.ArrayList $FailedConversions = $null @@ -85,14 +85,18 @@ } } - if ($FailedConversions -le 0){} + if ($FailedConversions.Count -le 0) { + # Do nothing or handle the zero-case + } else { - Out-LogFile ("[ERROR] - " + $FailedConversions.Count + " Entries failed JSON Conversion") - $FailedConversions | Out-MultipleFileType -fileprefix "Failed_Conversion_Authentication_Logs" -user $User -csv -json + Out-LogFile ("$($FailedConversions.Count) Entries failed JSON Conversion") -isError + $FailedConversions | Out-MultipleFileType -FilePrefix "Failed_Conversion_Authentication_Logs" -User $User -Csv -Json } + # Add IP Geo Location information to the data if ($ResolveIPLocations) { + Out-File "Resolving IP Locations" # Setup our counter $i = 0 @@ -113,7 +117,7 @@ } # Combine the connection object and the location object so that we have a single output ready - $ExpandedUserLogonLogs.item($i) = ($ExpandedUserLogonLogs.item($i) | Select-Object -Property *, @{Name = "CountryName"; Expression = { $Location.CountryName } }, @{Name = "RegionCode"; Expression = { $Location.RegionCode } }, @{Name = "RegionName"; Expression = { $Location.RegionName } }, @{Name = "City"; Expression = { $Location.City } }, @{Name = "ZipCode"; Expression = { $Location.ZipCode } }, @{Name = "KnownMicrosoftIP"; Expression = { $Location.KnownMicrosoftIP } }) + $ExpandedUserLogonLogs.item($i) = ($ExpandedUserLogonLogs.item($i) | Select-Object -Property *, @{Name = "CountryName"; Expression = { $Location.CountryName } }, @{Name = "RegionCode"; Expression = { $Location.RegionCode } }, @{Name = "RegionName"; Expression = { $Location.RegionName } }, @{Name = "City"; Expression = { $Location.City } }, @{Name = "KnownMicrosoftIP"; Expression = { $Location.KnownMicrosoftIP } }) # increment our counter for the progress bar $i++ @@ -122,12 +126,12 @@ Write-Progress -Completed -Activity "Looking Up Ip Address Locations" -Status " " } else { - Out-LogFile "ResolveIPLocations not specified" + Out-LogFile "ResolveIPLocations not specified" -Information } # Convert to human readable and export - Out-LogFile "Converting to Human Readable" - (Import-AzureAuthenticationLogs -JsonConvertedLogs $ExpandedUserLogonLogs) | Out-MultipleFileType -fileprefix "Converted_Authentication_Logs" -User $User -csv -json + Out-LogFile "Converting to Human Readable" -action + (Import-AzureAuthenticationLog -JsonConvertedLogs $ExpandedUserLogonLogs) | Out-MultipleFileType -fileprefix "Converted_Authentication_Logs" -User $User -csv -json # Export RAW data $UserLogonLogs | Out-MultipleFileType -fileprefix "Raw_Authentication_Logs" -user $User -csv -json diff --git a/Hawk/functions/User/Get-HawkUserAutoReply.ps1 b/Hawk/functions/User/Get-HawkUserAutoReply.ps1 index daff674..be81c5d 100644 --- a/Hawk/functions/User/Get-HawkUserAutoReply.ps1 +++ b/Hawk/functions/User/Get-HawkUserAutoReply.ps1 @@ -41,13 +41,13 @@ [string]$User = $Object.UserPrincipalName # Get Autoreply Configuration - Out-LogFile ("Retrieving Autoreply Configuration: " + $User) -action + Out-LogFile ("Retrieving AutoReply Configuration: " + $User) -action $AutoReply = Get-MailboxAutoReplyConfiguration -Identity $User # Check if the Autoreply is Disabled if ($AutoReply.AutoReplyState -eq 'Disabled') { - Out-LogFile "AutoReply is not enabled or not configured." + Out-LogFile "AutoReply is not enabled or not configured." -Information } # Output Enabled AutoReplyConfiguration to a generic txt else { diff --git a/Hawk/functions/User/Get-HawkUserConfiguration.ps1 b/Hawk/functions/User/Get-HawkUserConfiguration.ps1 index c44e5ba..f7df349 100644 --- a/Hawk/functions/User/Get-HawkUserConfiguration.ps1 +++ b/Hawk/functions/User/Get-HawkUserConfiguration.ps1 @@ -1,5 +1,5 @@ -Function Get-HawkUserConfiguration { -<# +Function Get-HawkUserConfiguration { + <# .SYNOPSIS Gathers baseline information about the provided user. .DESCRIPTION @@ -38,29 +38,29 @@ Function Get-HawkUserConfiguration { Gathers the user configuration for all users who have "C-Level" set in CustomAttribute1 #> - param - ( - [Parameter(Mandatory = $true)] - [array]$UserPrincipalName - ) + param + ( + [Parameter(Mandatory = $true)] + [array]$UserPrincipalName + ) - Test-EXOConnection - Send-AIEvent -Event "CmdRun" + Test-EXOConnection + Send-AIEvent -Event "CmdRun" - # Verify our UPN input - [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName + # Verify our UPN input + [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName - foreach ($Object in $UserArray) { - [string]$User = $Object.UserPrincipalName + foreach ($Object in $UserArray) { + [string]$User = $Object.UserPrincipalName - Out-LogFile ("Gathering information about " + $User) -action + Out-LogFile ("Gathering information about " + $User) -action - #Gather mailbox information - Out-LogFile "Gathering Mailbox Information" - $mbx = Get-EXOMailbox -Identity $user + #Gather mailbox information + Out-LogFile "Gathering Mailbox Information" -action + $mbx = Get-EXOMailbox -Identity $user # Test to see if we have an archive and include that info as well - if (!($null -eq $mbx.archivedatabase)){ + if (!($null -eq $mbx.archivedatabase)) { Get-EXOMailboxStatistics -identity $user -Archive | Out-MultipleFileType -FilePrefix "Mailbox_Archive_Statistics" -user $user -txt } @@ -68,8 +68,8 @@ Function Get-HawkUserConfiguration { Get-EXOMailboxStatistics -Identity $user | Out-MultipleFileType -FilePrefix "Mailbox_Statistics" -User $User -txt Get-EXOMailboxFolderStatistics -identity $user | Out-MultipleFileType -FilePrefix "Mailbox_Folder_Statistics" -User $User -txt - # Gather cas mailbox sessions - Out-LogFile "Gathering CAS Mailbox Information" + # Gather cas mailbox sessions + Out-LogFile "Gathering CAS Mailbox Information" -action Get-EXOCasMailbox -identity $user | Out-MultipleFileType -FilePrefix "CAS_Mailbox_Info" -User $User -txt } } diff --git a/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 b/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 index c0328ae..ef2320a 100644 --- a/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 +++ b/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 @@ -55,7 +55,7 @@ # Check if forwarding is configured by user or admin if ([string]::IsNullOrEmpty($mbx.ForwardingSMTPAddress) -and [string]::IsNullOrEmpty($mbx.ForwardingAddress)) { - Out-LogFile "No forwarding configuration found" + Out-LogFile "No forwarding configuration found" -Information } # If populated report it and add to a CSV file of positive finds else { diff --git a/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 b/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 index 3d62bb6..11952fc 100644 --- a/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 +++ b/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 @@ -40,12 +40,10 @@ Looks for hidden inbox rules for all users who have "C-Level" set in CustomAttribute1 #> - param - ( + param ( [Parameter(Mandatory = $true)] [array]$UserPrincipalName, [System.Management.Automation.PSCredential]$EWSCredential - ) Test-EXOConnection @@ -54,122 +52,86 @@ # Verify our UPN input [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName - # Process thru each object recieved + # Process each object received foreach ($Object in $UserArray) { # Push the UPN into $user for ease of use - $user = $object.UserPrincipalName + $user = $Object.UserPrincipalName # Determine if the email address is null or empty - # If it is write a warning and skip the rest of the script - [string]$EmailAddress = (Get-EXOMailbox $user).primarysmtpaddress + [string]$EmailAddress = (Get-EXOMailbox $user).PrimarySmtpAddress if ([string]::IsNullOrEmpty($EmailAddress)) { - Write-Warning "No SMTP Address found Skipping" - Return $null + Write-Warning "No SMTP Address found. Skipping." + return $null } - # If we don't have a credential object then ask for creds and push them into the global scope + # If we don't have a credential object, ask for credentials if ($null -eq $EWSCredential) { - Out-LogFile "Please provide credentials that have impersonation rights to the mailbox you are looking to check" + Out-LogFile "Please provide credentials that have impersonation rights to the mailbox you are looking to check" -Information $EWSCredential = Get-Credential } # Import the EWS Managed API if (Test-Path 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll') { - Out-LogFile "Ews Managed API Found" - } - else { + Out-LogFile "EWS Managed API Found" -Information + } else { Write-Error "Please install EWS Managed API 2.2 `nhttp://www.microsoft.com/en-us/download/details.aspx?id=42951" -ErrorAction Stop } - # Import the EWS managed API dll + # Import the EWS Managed API DLL Import-Module 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll' # Set up the EWS Connection - Write-Host ("Setting up connection for " + $emailaddress) -ForegroundColor Green + Write-Information ("Setting up connection for " + $EmailAddress) $exchService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList ([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_Sp1) - $exchService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($EWSCredential.username, $EWSCredential.GetNetworkCredential().password); + $exchService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($EWSCredential.Username, $EWSCredential.GetNetworkCredential().Password) - # If we have the global URL for EWS then we just use it since it should all be the same in this case - # Otherwise we need to get it via autodiscover + # Autodiscover or use global EWS URL if ($null -eq $EWSUrl) { - $exchService.AutodiscoverUrl($emailAddress, { $true }) - $exchService.url | Set-Variable -name EWSUrl -Scope Global - } - else { - $exchService.url = $EWSUrl + $exchService.AutodiscoverUrl($EmailAddress, { $true }) + $exchService.Url | Set-Variable -Name EWSUrl -Scope Global + } else { + $exchService.Url = $EWSUrl } - # Set the connection up for impersonation so that we log into the mailbox we want not the one we have creds for - $exchService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $emailAddress); + # Set impersonation + $exchService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress) - # Add the Anchor mailbox to the http header + # Add the Anchor mailbox to the HTTP header $exchService.HttpHeaders.Add("X-AnchorMailbox", [string]$EmailAddress) - # Using the exchService object connect and retrieve all inbox rules - # This DID NOT work since it didn't pull back the hidden rule - # $rules = $exchService.GetInboxRules($EmailAddress) - - # Bind to the inbox folder - try { - $inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox) - } - catch { - # If we don't have rights to impersonate throw in the log file and provide a better error - if ($_.Exception.innerexception -like "*permission to impersonate*") { - Out-LogFile ("[ERROR] - Account does not have Impersonation Rights on Mailbox: " + $EmailAddress) - Out-LogFile "https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-configure-impersonation" - Write-Error $_ -ErrorAction Stop - } - # If it isn't an impersonation error throw it and stop - else { - Write-Error $_ -ErrorAction Stop - } - } - - # Setup the search - $SearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass, "IPM.Rule.Version2.Message") - $Itemview = new-object Microsoft.Exchange.WebServices.Data.ItemView(500) - $ItemView.Traversal = [Microsoft.Exchange.Webservices.Data.ItemTraversal]::Associated + # Search for hidden rules + $SearchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass, "IPM.Rule.Version2.Message") + $ItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(500) + $ItemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Associated # Create our property set to view $PR_RULE_MSG_NAME = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x65EC, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String) $PR_RULE_MSG_PROVIDER = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x65EB, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String) $PR_PRIORITY = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0026, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer) - $psPropset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IDOnly, $PR_RULE_MSG_NAME, $PR_RULE_MSG_PROVIDER, $PR_PRIORITY) + $psPropset = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IDOnly, $PR_RULE_MSG_NAME, $PR_RULE_MSG_PROVIDER, $PR_PRIORITY) # Add the property set to the item view $ItemView.PropertySet = $psPropset # Do the search and return the items - $ruleResults = $inbox.finditems($SearchFilter, $Itemview) + $ruleResults = $inbox.FindItems($SearchFilter, $ItemView) - # Null our return arry and populate it - [array]$ruleArray = $null - $ruleResults | ForEach-Object { [array]$ruleArray += $_ } - - # Set our found flag to false + # Check each rule directly from $ruleResults $FoundHidden = $false - - # Check each rule - Foreach ($rule in $ruleArray) { - - # If either Rule Name or Rule Provider are null then we need to flag it and return the priority of the rule - if ([string]::IsNullOrEmpty($rule.ExtendedProperties[0].value) -or [string]::IsNullOrEmpty($rule.ExtendedProperties[1].value)) { - $priority = ($rule.ExtendedProperties | Where-Object { $_.propertydefinition.tag -eq 38 }).value - Out-LogFile ("Possible Hidden Rule found in mailbox: " + $EmailAddress + " -- Rule Priority: " + $priority) -notice - $RuleOutput = $rule | Select-Object -Property ID, @{Name = "Priority"; Expression = { ($rule.ExtendedProperties | Where-Object { $_.propertydefinition -like "*38*" }).value } } - $RuleOutput | Out-MultipleFileType -FilePrefix "EWS_Inbox_rule" -txt -user $user -append + foreach ($rule in $ruleResults) { + if ([string]::IsNullOrEmpty($rule.ExtendedProperties[0].Value) -or [string]::IsNullOrEmpty($rule.ExtendedProperties[1].Value)) { + $priority = ($rule.ExtendedProperties | Where-Object { $_.PropertyDefinition.Tag -eq 38 }).Value + Out-LogFile ("Possible Hidden Rule found in mailbox: " + $EmailAddress + " -- Rule Priority: " + $priority) -Notice + $RuleOutput = $rule | Select-Object -Property ID, @{ Name = "Priority"; Expression = { ($rule.ExtendedProperties | Where-Object { $_.PropertyDefinition -like "*38*" }).Value } } + $RuleOutput | Out-MultipleFileType -FilePrefix "EWS_Inbox_rule" -Txt -User $user -Append $FoundHidden = $true } - } - # If the flag wasn't set then we need to log that we didn't find any hidden rules for th euser + # Log if no hidden rules are found if ($FoundHidden -eq $false) { - Out-LogFile ("No Hidden rules found for mailbox: " + $EmailAddress) + Out-LogFile ("No Hidden rules found for mailbox: " + $EmailAddress) -Information } - - # return $ruleArray } -} \ No newline at end of file +} diff --git a/Hawk/functions/User/Get-HawkUserInboxRule.ps1 b/Hawk/functions/User/Get-HawkUserInboxRule.ps1 index 32e00ab..2509230 100644 --- a/Hawk/functions/User/Get-HawkUserInboxRule.ps1 +++ b/Hawk/functions/User/Get-HawkUserInboxRule.ps1 @@ -54,7 +54,7 @@ Function Get-HawkUserInboxRule { Out-LogFile ("Gathering Inbox Rules: " + $User) -action $InboxRules = Get-InboxRule -mailbox $User - if ($null -eq $InboxRules) { Out-LogFile "No Inbox Rules found" } + if ($null -eq $InboxRules) { Out-LogFile "No Inbox Rules found" -Information } else { # If the rules contains one of a number of known suspecious properties flag them foreach ($Rule in $InboxRules) { @@ -98,7 +98,7 @@ Function Get-HawkUserInboxRule { Out-LogFile ("Gathering Sweep Rules: " + $User) -action $SweepRules = Get-SweepRule -Mailbox $User - if ($null -eq $SweepRules) { Out-LogFile "No Sweep Rules found" } + if ($null -eq $SweepRules) { Out-LogFile "No Sweep Rules found" -Information} else { # Output all rules to a user CSV diff --git a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 index 672b48b..e56d7f9 100644 --- a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 +++ b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 @@ -1,74 +1,82 @@ function Get-HawkUserMailboxAuditing { -<# -.SYNOPSIS - Gathers Mailbox Audit data if enabled for the user. -.DESCRIPTION - Check if mailbox auditing is enabled for the user. - If it is pulls the mailbox audit logs from the time period specified for the investigation. - - Will pull from the Unified Audit Log and the Mailbox Audit Log -.PARAMETER UserPrincipalName - Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs. -.OUTPUTS - - File: Exchange_UAL_Audit.csv - Path: \ - Description: All Exchange related audit events found in the Unified Audit Log. - - File: Exchange_Mailbox_Audit.csv - Path: \ - Description: All Exchange related audit events found in the Mailbox Audit Log. - .EXAMPLE - - Get-HawkUserMailboxAuditing -UserPrincipalName user@contoso.com + <# + .SYNOPSIS + Gathers Mailbox Audit data if enabled for the user. + + .DESCRIPTION + Retrieves mailbox audit logs from Microsoft 365 Unified Audit Log, focusing on mailbox + content access and operations. This function replaces the deprecated Search-MailboxAuditLog + cmdlet with modern UAL-based auditing. + + Migration Changes: + - Old: Used Search-MailboxAuditLog for direct mailbox audit log access + - New: Uses Search-UnifiedAuditLog with separate collection of: + * ExchangeItem records (item-level operations) + * ExchangeItemGroup records (access patterns) + + The new implementation provides: + - Improved visibility into mailbox item access patterns + - More consistent data collection across Exchange Online + - Automatic pagination for large result sets + - Integration with Microsoft 365 compliance center + - Separated output files for better data analysis + + Note: Administrative actions on mailboxes (like granting permissions) are tracked by + Get-HawkUserAdminAudit instead of this function. + + .PARAMETER UserPrincipalName + Single UPN of a user, comma-separated list of UPNs, or array of objects that contain UPNs. + + .OUTPUTS + ExchangeItem Records: + File: ExchangeItem_Simple_{User}.csv/.json + Path: \ + Description: Flattened item-level operations data in CSV and JSON formats + + File: ExchangeItem_Logs_{User}.csv/.json + Path: \ + Description: Raw item-level operations data in CSV and JSON formats + + ExchangeItemGroup Records: + File: ExchangeItemGroup_Simple_{User}.csv/.json + Path: \ + Description: Flattened access pattern data in CSV and JSON formats + + File: ExchangeItemGroup_Logs_{User}.csv/.json + Path: \ + Description: Raw access pattern data in CSV and JSON formats - Search for all Mailbox Audit logs from user@contoso.com .EXAMPLE + Get-HawkUserMailboxAuditing -UserPrincipalName user@contoso.com - Get-HawkUserMailboxAuditing -UserPrincipalName (get-mailbox -Filter {Customattribute1 -eq "C-level"}) + Search for all Mailbox Audit logs from user@contoso.com, creating separate files for + item operations and access patterns, each with both raw and processed formats. - Search for all Mailbox Audit logs for all users who have "C-Level" set in CustomAttribute1 -#> - - param - ( + .EXAMPLE + Get-HawkUserMailboxAuditing -UserPrincipalName (Get-Mailbox -Filter {CustomAttribute1 -eq "C-level"}) + + Search for all Mailbox Audit logs for all users who have "C-Level" set in CustomAttribute1, + creating separate output files for each user's item operations and access patterns. + + .NOTES + In older versions of Exchange Online, Search-MailboxAuditLog provided direct access to + mailbox audit data. This has been replaced by the Unified Audit Log which provides a + more comprehensive and consistent view of mailbox activities through separate record types: + - ExchangeItem: Tracks specific operations on items + - ExchangeItemGroup: Tracks access patterns and aggregated activity + + Each record type is processed separately and output in multiple formats to support + different analysis needs: + - Simple (flattened) formats for easy analysis + - Raw formats for detailed investigation + - JSON dumps for programmatic processing + #> + [CmdletBinding()] + param ( [Parameter(Mandatory = $true)] [array]$UserPrincipalName ) - Function Get-MailboxAuditLogsFiveDaysAtATime { - param( - [Parameter(Mandatory = $true)] - [datetime]$StartDate, - [Parameter(Mandatory = $true)] - [datetime]$EndDate, - [Parameter(Mandatory = $true)] - $User - ) - - - # Setup the initial start date - [datetime]$RangeStart = $StartDate - - do { - # Get the end of the Range we are going to gather data for - [datetime] $RangeEnd = ($RangeStart.AddDays(5)) - # Do the actual search - Out-LogFile ("Searching Range " + [string]$RangeStart + " To " + [string]$RangeEnd) - [array]$Results += Search-MailboxAuditLog -StartDate $RangeStart -EndDate $RangeEnd -identity $User -ShowDetails -ResultSize 250000 - - # Set the RangeStart = to the RangeEnd so we do the next range - $RangeStart = $RangeEnd - } - # While the start range is less than the end date we need to keep pulling in 5 day increments - while ($RangeStart -le $EndDate) - - # Return the results object - Return $Results - - } - - ### MAIN ### Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -78,34 +86,74 @@ foreach ($Object in $UserArray) { [string]$User = $Object.UserPrincipalName - Out-LogFile ("Attempting to Gather Mailbox Audit logs " + $User) -action + Out-LogFile ("Attempting to Gather Mailbox Audit logs for: " + $User) -action # Test if mailbox auditing is enabled - $mbx = Get-Mailbox -identity $User + $mbx = Get-Mailbox -Identity $User if ($mbx.AuditEnabled -eq $true) { - # if enabled pull the mailbox auditing from the unified audit logs - Out-LogFile "Mailbox Auditing is enabled." - Out-LogFile "Searching Unified Audit Log for Exchange Related Events" - - $UnifiedAuditLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -UserIDs " + $User + " -RecordType ExchangeItem") | select-object -Expandproperty AuditData | convertfrom-json - Out-LogFile ("Found " + $UnifiedAuditLogs.Count + " Exchange audit records.") - - # Output the data we found - $UnifiedAuditLogs | Out-MultipleFileType -FilePrefix "Exchange_UAL_Audit" -User $User -csv -json - - # Search the MailboxAuditLogs as well since they may have different/more information - Out-LogFile "Searching Exchange Mailbox Audit Logs (this can take some time)" - - $MailboxAuditLogs = Get-MailboxAuditLogsFiveDaysAtATime -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate -User $User - Out-LogFile ("Found " + $MailboxAuditLogs.Count + " Exchange Mailbox audit records.") - - # Output the data we found - $MailboxAuditLogs | Out-MultipleFileType -FilePrefix "Exchange_Mailbox_Audit" -User $User -csv -json - + Out-LogFile "Mailbox Auditing is enabled." -Information + + try { + # Get the user's folder path + $UserFolder = Join-Path -Path $Hawk.FilePath -ChildPath $User + if (-not (Test-Path -Path $UserFolder)) { + New-Item -Path $UserFolder -ItemType Directory -Force | Out-Null + } + + # Process ExchangeItem records + Out-LogFile "Searching Unified Audit Log for ExchangeItem events." -action + $searchCommand = "Search-UnifiedAuditLog -UserIds $User -RecordType ExchangeItem" + $itemLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand + + if ($itemLogs.Count -gt 0) { + Out-LogFile ("Found " + $itemLogs.Count + " ExchangeItem events.") -Information + + # Process and output flattened data + $ParsedItemLogs = $itemLogs | Get-SimpleUnifiedAuditLog + if ($ParsedItemLogs) { + $ParsedItemLogs | Out-MultipleFileType -FilePrefix "ExchangeItem_Simple" -csv -json -User $User + } + + # Output raw data + $itemLogs | Out-MultipleFileType -FilePrefix "ExchangeItem_Logs" -csv -json -User $User + } + else { + Out-LogFile "No ExchangeItem events found." -Information + } + + # Process ExchangeItemGroup records + Out-LogFile "Searching Unified Audit Log for ExchangeItemGroup events." -action + $searchCommand = "Search-UnifiedAuditLog -UserIds $User -RecordType ExchangeItemGroup" + $groupLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand + + if ($groupLogs.Count -gt 0) { + Out-LogFile ("Found " + $groupLogs.Count + " ExchangeItemGroup events.") -Information + + # Process and output flattened data + $ParsedGroupLogs = $groupLogs | Get-SimpleUnifiedAuditLog + if ($ParsedGroupLogs) { + $ParsedGroupLogs | Out-MultipleFileType -FilePrefix "ExchangeItemGroup_Simple" -csv -json -User $User + } + + # Output raw data + $groupLogs | Out-MultipleFileType -FilePrefix "ExchangeItemGroup_Logs" -csv -json -User $User + } + else { + Out-LogFile "No ExchangeItemGroup events found." -Information + } + + # Summary logging + $totalEvents = ($itemLogs.Count + $groupLogs.Count) + Out-LogFile "Completed processing $totalEvents total events." -Information + } + catch { + Out-LogFile "Error retrieving audit logs: $($_.Exception.Message)" -isError + Write-Error -ErrorRecord $_ -ErrorAction Continue + } } - # If auditing is not enabled log it and move on else { - Out-LogFile ("Auditing not enabled for " + $User) + Out-LogFile ("Auditing not enabled for " + $User) -Information + Out-LogFile "Enable auditing to track mailbox access patterns." -Information } } -} +} \ No newline at end of file diff --git a/Hawk/functions/User/Get-HawkUserMessageTrace.ps1 b/Hawk/functions/User/Get-HawkUserMessageTrace.ps1 index 4dcef97..feea71b 100644 --- a/Hawk/functions/User/Get-HawkUserMessageTrace.ps1 +++ b/Hawk/functions/User/Get-HawkUserMessageTrace.ps1 @@ -42,7 +42,7 @@ Single UPN of a user, commans seperated list of UPNs, or array of objects that c [string]$PrimarySMTP = (Get-Mailbox -identity $User).primarysmtpaddress if ([string]::IsNullOrEmpty($PrimarySMTP)) { - Out-LogFile ("[ERROR] - Failed to find Primary SMTP Address for user: " + $User) + Out-LogFile ("Failed to find Primary SMTP Address for user: " + $User) -isError Write-Error ("Failed to find Primary SMTP Address for user: " + $User) } else { diff --git a/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 b/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 index c1154a0..20ef3b9 100644 --- a/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 +++ b/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 @@ -44,14 +44,14 @@ [string]$User = $Object.UserPrincipalName # Get all mobile devices - Out-Logfile ("Gathering Mobile Devices for: " + $User) + Out-Logfile ("Gathering Mobile Devices for: " + $User) -Action [array]$MobileDevices = Get-MobileDevice -mailbox $User if ($Null -eq $MobileDevices) { - Out-Logfile ("No devices found for user: " + $User) + Out-Logfile ("No devices found for user: " + $User) -Information } else { - Out-Logfile ("Found " + $MobileDevices.count + " Devices") + Out-Logfile ("Found " + $MobileDevices.count + " Devices") -Information # Check each device to see if it was NEW # If so flag it for investigation diff --git a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 index 8e2e05d..fb465c2 100644 --- a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 +++ b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 @@ -1,79 +1,92 @@ Function Get-HawkUserPWNCheck { -<# -.SYNOPSIS - Checks an email address against haveibeenpwned.com -.DESCRIPTION - Checks a single email address against HaveIBeenPwned. An API key is required and can be obtained from https://haveibeenpwned.com/API/Key for $3.50 a month. - This script will prompt for the key if $hibpkey is not set as a variable. -.PARAMETER Email - Accepts since EMail address or array of Email address strings. - DOES NOT Accept an array of objects (it will end up checked the UPN and not the email address) -.OUTPUTS - File: Have_I_Been_Pwned.txt - Path: \ - Description: Information returned from the pwned database -.EXAMPLE - Start-HawkUserPWNCheck -Email user@company.com - - Returns the pwn state of the email address provided -#> - - param([array]$Email) - - # if there is no value of hibpkey then we need to get it from the user - if ($null -eq $hibpkey) { - - Write-Host -ForegroundColor Green " - - HaveIBeenPwned.com now requires an API access key to gather Stats with from their API. - - Please purchase an API key for $3.50 a month from get a Free access key from https://haveibeenpwned.com/API/Key and provide it below. - - " - - # get the access key from the user - $hibpkey = Read-Host "haveibeenpwned.com apikey" - } - - # Verify our UPN input - [array]$UserArray = Test-UserObject -ToTest $Email - $headers=@{'hibp-api-key' = $hibpkey} + <# + .SYNOPSIS + Checks an email address against haveibeenpwned.com + .DESCRIPTION + Checks a single email address against HaveIBeenPwned. An API key is required and can be obtained from https://haveibeenpwned.com/API/Key for $3.50 a month. + This script will prompt for the key if $hibpkey is not set as a variable. + .PARAMETER EmailAddress + Accepts since EMail address or array of Email address strings. + DOES NOT Accept an array of objects (it will end up checked the UPN and not the email address) + .OUTPUTS + File: Have_I_Been_Pwned.txt + Path: \ + Description: Information returned from the pwned database + .EXAMPLE + Get-HawkUserPWNCheck -EmailAddress user@company.com + + Returns the pwn state of the email address provided + #> + + param( + [string[]]$EmailAddress + ) + + # if there is no value of hibpkey then we need to get it from the user + BEGIN {if ($null -eq $hibpkey) { + + Write-Host -ForegroundColor Green " + + HaveIBeenPwned.com now requires an API access key to gather Stats with from their API. + + Please purchase an API key for `$3.95 a month from get a Free access key from https://haveibeenpwned.com/API/Key and provide it below. + + " + + # get the access key from the user + $hibpkey = Read-Host "haveibeenpwned.com apikey" + } + }#End of BEGIN block - foreach ($Object in $UserArray) { + # Verify our UPN input + PROCESS {[array]$UserArray = Test-UserObject -ToTest $EmailAddress + $headers=@{'hibp-api-key' = $hibpkey} - $[string]$User = $Object.UserPrincipalName + foreach ($Object in $UserArray) { - # Convert the email to URL encoding - $uriEncodeEmail = [uri]::EscapeDataString($($user)) + [string]$User = $Object.UserPrincipalName - # Build and invoke the URL - $InvokeURL = 'https://haveibeenpwned.com/api/v3/breachedaccount/' + $uriEncodeEmail + '?truncateResponse=false' - $Error.clear() + # Convert the email to URL encoding + $uriEncodeEmail = [uri]::EscapeDataString($($user)) - try { - $Result = Invoke-WebRequest $InvokeURL -Headers $headers -userAgent 'Hawk' -ErrorAction Stop - } - catch { - switch ($Error[0].exception.response.statuscode) { - NotFound { - write-host "Email Not Found to be Pwned" - return - } - Default { - write-host "[ERROR] - Failure to retrieve pwned status" - write-host $Error - return + # Build and invoke the URL + $InvokeURL = 'https://haveibeenpwned.com/api/v3/breachedaccount/' + $uriEncodeEmail + '?truncateResponse=false' + $Error.clear() + #Will catch the error if the email is not found. 404 error means that the email is not found in the database. + #https://haveibeenpwned.com/API/v3#ResponseCodes contains the response codes for the API + try { + $Result = Invoke-WebRequest -Uri $InvokeURL -Headers $headers -userAgent 'Hawk' -ErrorAction Stop + } + catch { + $StatusCode = $_.Exception.Response.StatusCode + $ErrorMessage = $_.Exception.Message + switch ($StatusCode) { + NotFound{ + write-host "Email Provided Not Found in Pwned Database" + return + } + Unauthorized{ + write-host "Unauthorised Access - API key provided is not valid or has expired" + return + } + Default { + write-host $ErrorMessage + return + } } } - } - # Convert the result into a PS object - $Pwned = $Result.content | ConvertFrom-Json + # Convert the result into a PS custgom object + $Pwned = $Result.content | ConvertFrom-Json + + # Output the value + Out-LogFile ("Email Address found in " + $pwned.count) + $Pwned | Out-MultipleFileType -FilePreFix "Have_I_Been_Pwned" -user $user -txt - # Output the value - Out-LogFile ("Email Address found in " + $pwned.count) - $Pwned | Out-MultipleFileType -FilePreFix "Have_I_Been_Pwned" -user $user -txt - Start-Sleep -Milliseconds 1500 - } -} + } + }#End of PROCESS block + END { + Start-Sleep -Milliseconds 1500 + }#End of END block +}#End of Function Get-HawkUserPWNCheck \ No newline at end of file diff --git a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 index b962924..35054fe 100644 --- a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 +++ b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 @@ -1,81 +1,120 @@ -# String together the hawk user functions to pull data for a single user -Function Start-HawkUserInvestigation { -<# -.SYNOPSIS - Gathers common data about a provided user. -.DESCRIPTION - Runs all Hawk users related cmdlets against the specified user and gathers the data. - - Cmdlet Information Gathered - ------------------------- ------------------------- - Get-HawkTenantConfigurationn Basic Tenant information - Get-HawkUserConfiguration Basic User information - Get-HawkUserInboxRule Searches the user for Inbox Rules - Get-HawkUserEmailForwarding Looks for email forwarding configured on the user - Get-HawkUserAutoReply Looks for enabled AutoReplyConfiguration - Get-HawkuserAuthHistory Searches the unified audit log for users logons - Get-HawkUserMailboxAuditing Searches the unified audit log for mailbox auditing information - Get-HawkUserAdminAudit Searches the EXO Audit logs for any commands that were run against the provided user object. - Get-HawkUserMessageTrace Pulls the email sent by the user in the last 7 days. -.PARAMETER UserPrincipalName - Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs. -.OUTPUTS - See help from individual cmdlets for output list. - All outputs are placed in the $Hawk.FilePath directory -.EXAMPLE - Start-HawkUserInvestigation -UserPrincipalName bsmith@contoso.com - - Runs all Get-HawkUser* cmdlets against the user with UPN bsmith@contoso.com -.EXAMPLE - - Start-HawkUserInvestigation -UserPrincipalName (get-mailbox -Filter {Customattribute1 -eq "C-level"}) - - Runs all Get-HawkUser* cmdlets against all users who have "C-Level" set in CustomAttribute1 -#> - - param - ( - [Parameter(Mandatory = $true)] - [array]$UserPrincipalName - ) - - Out-LogFile "Investigating Users" - Send-AIEvent -Event "CmdRun" - - # Pull the tenent configuration - Get-HawkTenantConfiguration - - # Verify our UPN input - [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName - - foreach ($Object in $UserArray) { - [string]$User = $Object.UserPrincipalName - - Out-LogFile "Running Get-HawkUserConfiguration" -action - Get-HawkUserConfiguration -User $User - - Out-LogFile "Running Get-HawkUserInboxRule" -action - Get-HawkUserInboxRule -User $User - - Out-LogFile "Running Get-HawkUserEmailForwarding" -action - Get-HawkUserEmailForwarding -User $User - - Out-LogFile "Running Get-HawkUserAutoReply" -action - Get-HawkUserAutoReply -User $User - - Out-LogFile "Running Get-HawkUserAuthHistory" -action - Get-HawkUserAuthHistory -User $user -ResolveIPLocations - - Out-LogFile "Running Get-HawkUserMailboxAuditing" -action - Get-HawkUserMailboxAuditing -User $User - - Out-LogFile "Running Get-HawkUserAdminAudit" -action - Get-HawkUserAdminAudit -User $User - - Out-LogFile "Running Get-HawkUserMessageTrace" -action - Get-HawkUserMessageTrace -user $User - - Out-LogFile "Running Get-HawkUserMobileDevice" -action - Get-HawkUserMobileDevice -user $User - } -} \ No newline at end of file +Function Start-HawkUserInvestigation { + <# + .SYNOPSIS + Gathers common data about a provided user. + + .DESCRIPTION + Runs all Hawk user-related cmdlets against the specified user and gathers the data. + + Cmdlet Information Gathered + ------------------------- ------------------------- + Get-HawkTenantConfiguration Basic Tenant information + Get-HawkUserConfiguration Basic User information + Get-HawkUserInboxRule Searches the user for Inbox Rules + Get-HawkUserEmailForwarding Looks for email forwarding configured on the user + Get-HawkUserAutoReply Looks for enabled AutoReplyConfiguration + Get-HawkUserAuthHistory Searches the unified audit log for user logons + Get-HawkUserMailboxAuditing Searches the unified audit log for mailbox auditing information + Get-HawkUserAdminAudit Searches the EXO Audit logs for commands run against the provided user + Get-HawkUserMessageTrace Pulls emails sent by the user in the last 7 days + + .PARAMETER UserPrincipalName + Single UPN of a user, comma-separated list of UPNs, or an array of objects that contain UPNs. + + .PARAMETER Confirm + Prompts for confirmation before running operations that could modify system state. + + .PARAMETER WhatIf + Shows what would happen if the command runs. The command is not actually run. + + .OUTPUTS + See help from individual cmdlets for output list. + All outputs are placed in the $Hawk.FilePath directory. + + .EXAMPLE + Start-HawkUserInvestigation -UserPrincipalName bsmith@contoso.com + + Runs all Get-HawkUser* cmdlets against the user with UPN bsmith@contoso.com. + + .EXAMPLE + Start-HawkUserInvestigation -UserPrincipalName (Get-Mailbox -Filter {CustomAttribute1 -eq "C-level"}) + + Runs all Get-HawkUser* cmdlets against all users who have "C-Level" set in CustomAttribute1. + + .NOTES + Ensure the Hawk global object is initialized with a valid logging file path before running this function. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [array]$UserPrincipalName + ) + + Write-HawkBanner + + # Check if the logging filepath is set + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + + if ($PSCmdlet.ShouldProcess("Investigating Users")) { + Out-LogFile "Investigating Users" -Action + Send-AIEvent -Event "CmdRun" + + # Pull the tenant configuration + Get-HawkTenantConfiguration + + # Verify the UPN input + [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName + + foreach ($Object in $UserArray) { + [string]$User = $Object.UserPrincipalName + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserConfiguration for $User")) { + Out-LogFile "Running Get-HawkUserConfiguration" -Action + Get-HawkUserConfiguration -User $User + } + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserInboxRule for $User")) { + Out-LogFile "Running Get-HawkUserInboxRule" -Action + Get-HawkUserInboxRule -User $User + } + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserEmailForwarding for $User")) { + Out-LogFile "Running Get-HawkUserEmailForwarding" -Action + Get-HawkUserEmailForwarding -User $User + } + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserAutoReply for $User")) { + Out-LogFile "Running Get-HawkUserAutoReply" -Action + Get-HawkUserAutoReply -User $User + } + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserAuthHistory for $User")) { + Out-LogFile "Running Get-HawkUserAuthHistory" -Action + Get-HawkUserAuthHistory -User $User -ResolveIPLocations + } + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserMailboxAuditing for $User")) { + Out-LogFile "Running Get-HawkUserMailboxAuditing" -Action + Get-HawkUserMailboxAuditing -User $User + } + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserAdminAudit for $User")) { + Out-LogFile "Running Get-HawkUserAdminAudit" -Action + Get-HawkUserAdminAudit -User $User + } + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserMessageTrace for $User")) { + Out-LogFile "Running Get-HawkUserMessageTrace" -Action + Get-HawkUserMessageTrace -User $User + } + + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserMobileDevice for $User")) { + Out-LogFile "Running Get-HawkUserMobileDevice" -Action + Get-HawkUserMobileDevice -User $User + } + } + } + } + \ No newline at end of file diff --git a/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 b/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..867f14b --- /dev/null +++ b/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,12 @@ +@{ + # Rules to be excluded from analysis + ExcludeRules = @( + # These are both excluded because they were hardcoded in the Hawk PSScriptAnalyzer.Tests originally. + # It is assumed this was done with good reason. + 'PSAvoidTrailingWhitespace' + 'PSShouldProcess' + # Exclude this as old test rules use Global Vars, will need to fix old tests and re-include this rule + 'PSAvoidGlobalVars' + 'PSUseDeclaredVarsMoreThanAssignments' + ) +} \ No newline at end of file diff --git a/Hawk/internal/configurations/configuration.ps1 b/Hawk/internal/configurations/configuration.ps1 index f21799d..91aa66f 100644 --- a/Hawk/internal/configurations/configuration.ps1 +++ b/Hawk/internal/configurations/configuration.ps1 @@ -14,7 +14,7 @@ Set-PSFConfig -Module 'Hawk' -Name 'Example.Setting' -Value 10 -Initialize -Vali Set-PSFConfig -Module 'Hawk' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'Hawk' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." -Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value 90 -Initialize -Validation integerpositive -Description 'How long into the past will the project look' +#Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value 90 -Initialize -Validation integerpositive -Description 'How long into the past will the project look' $handler = { $paramSetPSFLoggingProvider = @{ @@ -29,4 +29,4 @@ $handler = { Set-PSFLoggingProvider @paramSetPSFLoggingProvider } -Set-PSFConfig -Module 'Hawk' -Name "FilePath" -Value '' -Initialize -Validation string -Handler $handler -Description 'Path where the module maintains logs and export data' \ No newline at end of file +Set-PSFConfig -Module 'Hawk' -Name "FilePath" -Value '' -Initialize -Validation string -Handler $handler -Description 'Path where the module maintains logs and exports data' \ No newline at end of file diff --git a/Hawk/internal/functions/Add-HawkAppData.ps1 b/Hawk/internal/functions/Add-HawkAppData.ps1 index baebbef..2b259b7 100644 --- a/Hawk/internal/functions/Add-HawkAppData.ps1 +++ b/Hawk/internal/functions/Add-HawkAppData.ps1 @@ -24,7 +24,7 @@ Function Add-HawkAppData { [string]$Value ) - Out-LogFile ("Adding " + $value + " to " + $Name + " in HawkAppData") + Out-LogFile ("Adding " + $value + " to " + $Name + " in HawkAppData") -Action # Test if our HawkAppData variable exists if ([bool](get-variable HawkAppData -ErrorAction SilentlyContinue)) { diff --git a/Hawk/internal/functions/Compress-HawkData.ps1 b/Hawk/internal/functions/Compress-HawkData.ps1 index b626a1b..c6ef66d 100644 --- a/Hawk/internal/functions/Compress-HawkData.ps1 +++ b/Hawk/internal/functions/Compress-HawkData.ps1 @@ -29,7 +29,7 @@ Function Compress-HawkData { # Make sure we didn't throw an error when we tried to remove them if ($Error.Count -gt 0) { Out-LogFile "Unable to remove existing zip files from " + $Hawk.filepath + " please remove them manually" - Write-Error -Message "Unable to remove existing zip files from " + $Hawk.filepath + " please remove them manually" -ErrorAction Stop + Write-Error -Message ("Unable to remove existing zip files from " + $Hawk.filepath + " please remove them manually") -ErrorAction Stop } else { } } diff --git a/Hawk/internal/functions/Get-AllUnifiedAuditLogEntry.ps1 b/Hawk/internal/functions/Get-AllUnifiedAuditLogEntry.ps1 index 10d5e03..3626efe 100644 --- a/Hawk/internal/functions/Get-AllUnifiedAuditLogEntry.ps1 +++ b/Hawk/internal/functions/Get-AllUnifiedAuditLogEntry.ps1 @@ -1,82 +1,85 @@ Function Get-AllUnifiedAuditLogEntry { -<# -.SYNOPSIS - Make sure we get back all of the unified audit log results for the search we are doing -.DESCRIPTION - Make sure we get back all of the unified audit log results for the search we are doing -.PARAMETER UnifiedSearch - The search parameters -.PARAMETER StartDate - The start date provided by user during Hawk Object Initialization -.PARAMETER EndDate - The end date provide by the user during Hawk Object Initialization -.EXAMPLE - Get-AllUnifiedAuditLogEntry - Gets all unified auditlog entries -.NOTES - General notes -#> - param - ( - [Parameter(Mandatory = $true)] - [string]$UnifiedSearch, - [datetime]$StartDate = $Hawk.StartDate, - [datetime]$EndDate = $Hawk.EndDate - ) - - # Validate the incoming search command - if (($UnifiedSearch -match "-StartDate") -or ($UnifiedSearch -match "-EndDate") -or ($UnifiedSearch -match "-SessionCommand") -or ($UnifiedSearch -match "-ResultSize") -or ($UnifiedSearch -match "-SessionId")) { - Out-LogFile "Do not include any of the following in the Search Command" - Out-LogFile "-StartDate, -EndDate, -SessionCommand, -ResultSize, -SessionID" - Write-Error -Message "Unable to process search command, switch in UnifiedSearch that is handled by this cmdlet specified" -ErrorAction Stop - } - - # Make sure key variables are null - [string]$cmd = $null - - # build our search command to execute - $cmd = $UnifiedSearch + " -StartDate `'" + (get-date ($StartDate) -UFormat %m/%d/%Y) + "`' -EndDate `'" + (get-date ($endDate) -UFormat %m/%d/%Y) + "`' -SessionCommand ReturnLargeSet -resultsize 5000 -sessionid " + (Get-Date -UFormat %H%M%S) - Out-LogFile ("Running Unified Audit Log Search") - Out-Logfile $cmd - - # Run the initial command - $Output = $null - # $Output = New-Object System.Collections.ArrayList - - # Setup our run variable - $Run = $true - - # Since we have more than 1k results we need to keep returning results until we have them all - while ($Run) { - $Output += (Invoke-Expression $cmd) - - # Check for null results if so warn and stop - if ($null -eq $Output) { - Out-LogFile ("[WARNING] - Unified Audit log returned no results.") - $Run = $false + <# + .SYNOPSIS + Make sure we get back all of the unified audit log results for the search we are doing + .DESCRIPTION + Make sure we get back all of the unified audit log results for the search we are doing + .PARAMETER UnifiedSearch + The search parameters + .PARAMETER StartDate + The start date provided by user during Hawk Object Initialization + .PARAMETER EndDate + The end date provide by the user during Hawk Object Initialization + .EXAMPLE + Get-AllUnifiedAuditLogEntry + Gets all unified auditlog entries + .NOTES + General notes + #> + param + ( + [Parameter(Mandatory = $true)] + [string]$UnifiedSearch, + [datetime]$StartDate = $Hawk.StartDate, + [datetime]$EndDate = $Hawk.EndDate + ) + + # Validate the incoming search command + if (($UnifiedSearch -match "-StartDate") -or ($UnifiedSearch -match "-EndDate") -or ($UnifiedSearch -match "-SessionCommand") -or ($UnifiedSearch -match "-ResultSize") -or ($UnifiedSearch -match "-SessionId")) { + Out-LogFile "Do not include any of the following in the Search Command" -isError + Out-LogFile "-StartDate, -EndDate, -SessionCommand, -ResultSize, -SessionID" -isError + Write-Error -Message "Unable to process search command, switch in UnifiedSearch that is handled by this cmdlet specified" -ErrorAction Stop } - # Else continue - else { - # Sort our result set to make sure the higest number is in the last position - $Output = $Output | Sort-Object -Property ResultIndex - - # if total result count returned is 0 then we should warn and stop - if ($Output[-1].ResultCount -eq 0) { - Out-LogFile ("[WARNING] - Returned Result count was 0") + + # Make sure key variables are null + [string]$cmd = $null + + # build our search command to execute + $cmd = $UnifiedSearch + " -StartDate `'" + (get-date ($StartDate) -UFormat %m/%d/%Y) + "`' -EndDate `'" + (get-date ($endDate) -UFormat %m/%d/%Y) + "`' -SessionCommand ReturnLargeSet -resultsize 5000 -sessionid " + (Get-Date -UFormat %H%M%S) + Out-LogFile ("Running Unified Audit Log Search") -Action + Out-Logfile $cmd -NoDisplay + + # Run the initial command + $Output = $null + # $Output = New-Object System.Collections.ArrayList + + # Setup our run variable + $Run = $true + + # Convert the command string into a scriptblock to avoid Invoke-Expression + $searchScript = [ScriptBlock]::Create($cmd) + + # Since we have more than 1k results we need to keep returning results until we have them all + while ($Run) { + $Output += & $searchScript + + # Check for null results if so warn and stop + if ($null -eq $Output) { + Out-LogFile ("Unified Audit log returned no results.") -Information $Run = $false } - # if our resultindex = our resultcount then we have everything and should stop - elseif ($Output[-1].Resultindex -ge $Output[-1].ResultCount) { - Out-LogFile ("Retrieved all results.") - $Run = $false + # Else continue + else { + # Sort our result set to make sure the higest number is in the last position + $Output = $Output | Sort-Object -Property ResultIndex + + # if total result count returned is 0 then we should warn and stop + if ($Output[-1].ResultCount -eq 0) { + Out-LogFile ("Returned Result count was 0") -Information + $Run = $false + } + # if our resultindex = our resultcount then we have everything and should stop + elseif ($Output[-1].Resultindex -ge $Output[-1].ResultCount) { + Out-LogFile ("Retrieved all results.") -Information + $Run = $false + } + + # Output the current progress + Out-LogFile ("Retrieved:" + $Output[-1].ResultIndex.tostring().PadRight(5, " ") + " Total: " + $Output[-1].ResultCount) -Information } - - # Output the current progress - Out-LogFile ("Retrieved:" + $Output[-1].ResultIndex.tostring().PadRight(5, " ") + " Total: " + $Output[-1].ResultCount) } - } - - # Convert our list to an array and return it - [array]$Output = $Output - return $Output -} \ No newline at end of file + + # Convert our list to an array and return it + [array]$Output = $Output + return $Output + } \ No newline at end of file diff --git a/Hawk/internal/functions/Get-AzureADPSPermission.ps1 b/Hawk/internal/functions/Get-AzureADPSPermission.ps1 new file mode 100644 index 0000000..08ddd08 --- /dev/null +++ b/Hawk/internal/functions/Get-AzureADPSPermission.ps1 @@ -0,0 +1,211 @@ +Function Get-AzureADPSPermission { + <# + .SYNOPSIS + Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). + + .DESCRIPTION + Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments) + using Microsoft Graph API. This function retrieves and formats permission information for analysis + of application and delegated permissions in your tenant. + + .PARAMETER DelegatedPermissions + If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions + switch is set, both application and delegated permissions will be returned. + + .PARAMETER ApplicationPermissions + If set, will return application permissions. If neither this switch nor the DelegatedPermissions + switch is set, both application and delegated permissions will be returned. + + .PARAMETER UserProperties + The list of properties of user objects to include in the output. Defaults to DisplayName only. + + .PARAMETER ServicePrincipalProperties + The list of properties of service principals (i.e. apps) to include in the output. + Defaults to DisplayName only. + + .PARAMETER ShowProgress + Whether or not to display a progress bar when retrieving application permissions (which could take some time). + + .PARAMETER PrecacheSize + The number of users to pre-load into a cache. For tenants with over a thousand users, + increasing this may improve performance of the script. + + .EXAMPLE + PS C:\> Get-AzureADPSPermission | Export-Csv -Path "permissions.csv" -NoTypeInformation + Generates a CSV report of all permissions granted to all apps. + + .EXAMPLE + PS C:\> Get-AzureADPSPermission -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" } + Get all apps which have application permissions for Directory.Read.All. + + .EXAMPLE + PS C:\> Get-AzureADPSPermission -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId") + Gets all permissions granted to all apps and includes additional properties for users and service principals. + + .NOTES + This function requires Microsoft.Graph PowerShell module and appropriate permissions: + - Application.Read.All + - Directory.Read.All + #> + [CmdletBinding()] + param( + [switch] $DelegatedPermissions, + [switch] $ApplicationPermissions, + [string[]] $UserProperties = @("DisplayName"), + [string[]] $ServicePrincipalProperties = @("DisplayName"), + [switch] $ShowProgress, + [System.Int32] $PrecacheSize = 999 + ) + + # Verify Graph connection + try { + $tenant_details = Get-MgOrganization + } + catch { + throw "You must call Connect-MgGraph before running this script." + } + Write-Verbose ("TenantId: {0}" -f $tenant_details.Id) + + # Cache objects + $script:ObjectByObjectId = @{} + $script:ObjectByObjectType = @{ + 'ServicePrincipal' = @{} + 'User' = @{} + } + + function CacheObject ($Object, $Type) { + if ($Object) { + $script:ObjectByObjectType[$Type][$Object.Id] = $Object + $script:ObjectByObjectId[$Object.Id] = $Object + } + } + + function GetObjectByObjectId ($ObjectId) { + if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) { + Write-Verbose ("Querying Graph API for object '{0}'" -f $ObjectId) + try { + $object = Get-MgDirectoryObject -DirectoryObjectId $ObjectId + # Determine type from OdataType + $type = $object.AdditionalProperties.'@odata.type'.Split('.')[-1] + CacheObject -Object $object -Type $type + } + catch { + Write-Verbose "Object not found." + } + } + return $script:ObjectByObjectId[$ObjectId] + } + + # Cache all service principals + Write-Verbose "Retrieving all ServicePrincipal objects..." + $servicePrincipals = Get-MgServicePrincipal -All + foreach($sp in $servicePrincipals) { + CacheObject -Object $sp -Type 'ServicePrincipal' + } + $servicePrincipalCount = $servicePrincipals.Count + + # Cache users + Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize) + $users = Get-MgUser -Top $PrecacheSize + foreach($user in $users) { + CacheObject -Object $user -Type 'User' + } + + if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { + Write-Verbose "Retrieving OAuth2PermissionGrants..." + $oauth2Grants = Get-MgOAuth2PermissionGrant -All + + foreach ($grant in $oauth2Grants) { + if ($grant.Scope) { + $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object { + $scope = $_ + + $grantDetails = [ordered]@{ + "PermissionType" = "Delegated" + "ClientObjectId" = $grant.ClientId + "ResourceObjectId" = $grant.ResourceId + "Permission" = $scope + "ConsentType" = $grant.ConsentType + "PrincipalObjectId" = $grant.PrincipalId + } + + # Add service principal properties + if ($ServicePrincipalProperties.Count -gt 0) { + $client = $script:ObjectByObjectId[$grant.ClientId] + $resource = $script:ObjectByObjectId[$grant.ResourceId] + + $insertAtClient = 2 + $insertAtResource = 3 + foreach ($propertyName in $ServicePrincipalProperties) { + $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) + $insertAtResource++ + $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) + $insertAtResource++ + } + } + + # Add user properties + if ($UserProperties.Count -gt 0) { + $principal = if ($grant.PrincipalId) { + $script:ObjectByObjectId[$grant.PrincipalId] + } else { @{} } + + foreach ($propertyName in $UserProperties) { + $grantDetails["Principal$propertyName"] = $principal.$propertyName + } + } + + New-Object PSObject -Property $grantDetails + } + } + } + } + + if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { + Write-Verbose "Retrieving AppRoleAssignments..." + + $i = 0 + foreach ($sp in $servicePrincipals) { + if ($ShowProgress) { + Write-Progress -Activity "Retrieving application permissions..." ` + -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` + -PercentComplete (($i / $servicePrincipalCount) * 100) + } + + $appRoleAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -All + + foreach ($assignment in $appRoleAssignments) { + if ($assignment.PrincipalType -eq "ServicePrincipal") { + $resource = $script:ObjectByObjectId[$assignment.ResourceId] + $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.AppRoleId } + + $grantDetails = [ordered]@{ + "PermissionType" = "Application" + "ClientObjectId" = $assignment.PrincipalId + "ResourceObjectId" = $assignment.ResourceId + "Permission" = $appRole.Value + } + + if ($ServicePrincipalProperties.Count -gt 0) { + $client = $script:ObjectByObjectId[$assignment.PrincipalId] + + $insertAtClient = 2 + $insertAtResource = 3 + foreach ($propertyName in $ServicePrincipalProperties) { + $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) + $insertAtResource++ + $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) + $insertAtResource++ + } + } + + New-Object PSObject -Property $grantDetails + } + } + } + + if ($ShowProgress) { + Write-Progress -Completed -Activity "Retrieving application permissions..." + } + } + } \ No newline at end of file diff --git a/Hawk/internal/functions/Get-AzureADPSPermissions.ps1 b/Hawk/internal/functions/Get-AzureADPSPermissions.ps1 deleted file mode 100644 index 4e12b4f..0000000 --- a/Hawk/internal/functions/Get-AzureADPSPermissions.ps1 +++ /dev/null @@ -1,249 +0,0 @@ -Function Get-AzureADPSPermissions { - - <# - .SYNOPSIS - Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). - .DESCRIPTION - ists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). - .PARAMETER DelegatedPermissions - If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions switch is set, - both application and delegated permissions will be returned. - .PARAMETER ApplicationPermissions - If set, will return application permissions. If neither this switch nor the DelegatedPermissions switch is set, - both application and delegated permissions will be returned. - .PARAMETER UserProperties - The list of properties of user objects to include in the output. Defaults to DisplayName only. - .PARAMETER ServicePrincipalProperties - The list of properties of service principals (i.e. apps) to include in the output. Defaults to DisplayName only. - .PARAMETER ShowProgress - Whether or not to display a progress bar when retrieving application permissions (which could take some time). - .PARAMETER PrecacheSize - The number of users to pre-load into a cache. For tenants with over a thousand users, - increasing this may improve performance of the script. - .EXAMPLE - PS C:\> .\Get-AzureADPSPermissions.ps1 | Export-Csv -Path "permissions.csv" -NoTypeInformation - Generates a CSV report of all permissions granted to all apps. - .EXAMPLE - PS C:\> .\Get-AzureADPSPermissions.ps1 -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" } - Get all apps which have application permissions for Directory.Read.All. - .EXAMPLE - PS C:\> .\Get-AzureADPSPermissions.ps1 -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId") - Gets all permissions granted to all apps and includes additional properties for users and service principals. - - .LINK - https://gist.github.com/psignoret/9d73b00b377002456b24fcb808265c23 - - #> - - [CmdletBinding()] - param( - [switch] $DelegatedPermissions, - - [switch] $ApplicationPermissions, - - [string[]] $UserProperties = @("DisplayName"), - - [string[]] $ServicePrincipalProperties = @("DisplayName"), - - [switch] $ShowProgress, - - [int] $PrecacheSize = 999 - ) - - # Get tenant details to test that Connect-AzureAD has been called - try { - $tenant_details = Get-AzureADTenantDetail - } catch { - throw "You must call Connect-AzureAD before running this script." - } - Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f ` - $tenant_details.ObjectId, ` - ($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name) - - # An in-memory cache of objects by {object ID} andy by {object class, object ID} - $script:ObjectByObjectId = @{} - $script:ObjectByObjectClassId = @{} - - # Function to add an object to the cache - function CacheObject ($Object) { - if ($Object) { - if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) { - $script:ObjectByObjectClassId[$Object.ObjectType] = @{} - } - $script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object - $script:ObjectByObjectId[$Object.ObjectId] = $Object - } - } - - # Function to retrieve an object from the cache (if it's there), or from Azure AD (if not). - function GetObjectByObjectId ($ObjectId) { - if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) { - Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId) - try { - $object = Get-AzureADObjectByObjectId -ObjectId $ObjectId - CacheObject -Object $object - } catch { - Write-Verbose "Object not found." - } - } - return $script:ObjectByObjectId[$ObjectId] - } - - # Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode) - # or by iterating over all ServicePrincipal objects. The latter is required if there are more than - # 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD. - function GetOAuth2PermissionGrants ([switch]$FastMode) { - if ($FastMode) { - Get-AzureADOAuth2PermissionGrant -All $true - } else { - $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } { - if ($ShowProgress) { - Write-Progress -Activity "Retrieving delegated permissions..." ` - -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` - -PercentComplete (($i / $servicePrincipalCount) * 100) - } - - $client = $_.Value - Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId - } - } - } - - $empty = @{} # Used later to avoid null checks - - # Get all ServicePrincipal objects and add to the cache - Write-Verbose "Retrieving all ServicePrincipal objects..." - Get-AzureADServicePrincipal -All $true | ForEach-Object { - CacheObject -Object $_ - } - $servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count - - if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { - - # Get one page of User objects and add to the cache - Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize) - Get-AzureADUser -Top $PrecacheSize | Where-Object { - CacheObject -Object $_ - } - - Write-Verbose "Testing for OAuth2PermissionGrants bug before querying..." - $fastQueryMode = $false - try { - # There's a bug in Azure AD Graph which does not allow for directly listing - # oauth2PermissionGrants if there are more than 999 of them. The following line will - # trigger this bug (if it still exists) and throw an exception. - $null = Get-AzureADOAuth2PermissionGrant -Top 999 - $fastQueryMode = $true - } catch { - if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) { - Write-Verbose ("Fast query for delegated permissions failed, using slow method...") - } else { - throw $_ - } - } - - # Get all existing OAuth2 permission grants, get the client, resource and scope details - Write-Verbose "Retrieving OAuth2PermissionGrants..." - GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object { - $grant = $_ - if ($grant.Scope) { - $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object { - - $scope = $_ - - $grantDetails = [ordered]@{ - "PermissionType" = "Delegated" - "ClientObjectId" = $grant.ClientId - "ResourceObjectId" = $grant.ResourceId - "Permission" = $scope - "ConsentType" = $grant.ConsentType - "PrincipalObjectId" = $grant.PrincipalId - } - - # Add properties for client and resource service principals - if ($ServicePrincipalProperties.Count -gt 0) { - - $client = GetObjectByObjectId -ObjectId $grant.ClientId - $resource = GetObjectByObjectId -ObjectId $grant.ResourceId - - $insertAtClient = 2 - $insertAtResource = 3 - foreach ($propertyName in $ServicePrincipalProperties) { - $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) - $insertAtResource++ - $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) - $insertAtResource ++ - } - } - - # Add properties for principal (will all be null if there's no principal) - if ($UserProperties.Count -gt 0) { - - $principal = $empty - if ($grant.PrincipalId) { - $principal = GetObjectByObjectId -ObjectId $grant.PrincipalId - } - - foreach ($propertyName in $UserProperties) { - $grantDetails["Principal$propertyName"] = $principal.$propertyName - } - } - - Return New-Object PSObject -Property $grantDetails - } - } - } - } - - if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { - - # Iterate over all ServicePrincipal objects and get app permissions - Write-Verbose "Retrieving AppRoleAssignments..." - $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } { - - if ($ShowProgress) { - Write-Progress -Activity "Retrieving application permissions..." ` - -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` - -PercentComplete (($i / $servicePrincipalCount) * 100) - - if ($i -eq $servicePrincipalCount){ - Write-Progress -Completed -Activity "Retrieving application permissions..." ` - } - } - - $sp = $_.Value - - Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true ` - | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object { - $assignment = $_ - - $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId - $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id } - - $grantDetails = [ordered]@{ - "PermissionType" = "Application" - "ClientObjectId" = $assignment.PrincipalId - "ResourceObjectId" = $assignment.ResourceId - "Permission" = $appRole.Value - } - - # Add properties for client and resource service principals - if ($ServicePrincipalProperties.Count -gt 0) { - - $client = GetObjectByObjectId -ObjectId $assignment.PrincipalId - - $insertAtClient = 2 - $insertAtResource = 3 - foreach ($propertyName in $ServicePrincipalProperties) { - $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) - $insertAtResource++ - $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) - $insertAtResource ++ - } - } - - Return New-Object PSObject -Property $grantDetails - } - } - } - } \ No newline at end of file diff --git a/Hawk/internal/functions/Get-HawkUserPath.ps1 b/Hawk/internal/functions/Get-HawkUserPath.ps1 new file mode 100644 index 0000000..b0dd608 --- /dev/null +++ b/Hawk/internal/functions/Get-HawkUserPath.ps1 @@ -0,0 +1,41 @@ +Function Get-HawkUserPath { + <# + .SYNOPSIS + Gets the output folder path for a specific user in Hawk + .DESCRIPTION + Creates and returns the full path to a user's output folder within the Hawk + file structure. Creates the folder if it doesn't exist. + .PARAMETER User + The UserPrincipalName of the user to create/get path for + .EXAMPLE + Get-HawkUserPath -User "user@contoso.com" + + Returns the full path to the user's output folder and creates it if it doesn't exist + .OUTPUTS + System.String + Returns the full path to the user's output folder + .NOTES + Internal function used by Hawk cmdlets to manage user-specific output folders + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$User + ) + + # Check if Hawk global object exists + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + + # Join the Hawk filepath with the user's UPN for the output folder + $userPath = Join-Path -Path $Hawk.FilePath -ChildPath $User + + # Create directory if it doesn't exist + if (-not (Test-Path -Path $userPath)) { + Out-LogFile "Making output directory for user $userPath" + New-Item -Path $userPath -ItemType Directory -Force | Out-Null + } + + return $userPath +} \ No newline at end of file diff --git a/Hawk/internal/functions/Get-IPGeolocation.ps1 b/Hawk/internal/functions/Get-IPGeolocation.ps1 index 82774d6..dbf1ff5 100644 --- a/Hawk/internal/functions/Get-IPGeolocation.ps1 +++ b/Hawk/internal/functions/Get-IPGeolocation.ps1 @@ -28,10 +28,7 @@ Function Get-IPGeolocation { # if there is no value of access_key then we need to get it from the user if ($null -eq $HawkAppData.access_key) { - Write-Host -ForegroundColor Green " - IpStack.com now requires an API access key to gather GeoIP information from their API. - Please get a Free access key from https://ipstack.com/ and provide it below. - " + Out-LogFile "IpStack.com now requires an API access key to gather GeoIP information from their API.`nPlease get a Free access key from https://ipstack.com/ and provide it below." -Information # get the access key from the user $Accesskey = Read-Host "ipstack.com accesskey" @@ -53,7 +50,8 @@ Function Get-IPGeolocation { $hash = @{ IP = $IPAddress CountryName = "NULL IP" - Continent = "Unknown" + RegionName = "Unknown" + RegionCode = "Unknown" ContinentName = "Unknown" City = "Unknown" KnownMicrosoftIP = "Unknown" @@ -69,11 +67,12 @@ Function Get-IPGeolocation { $geoip = Invoke-RestMethod -Method Get -URI $resource -ErrorAction SilentlyContinue if (($Error.Count -gt 0) -or ($null -eq $geoip.type)) { - Out-LogFile ("Failed to retreive location for IP " + $IPAddress) + Out-LogFile ("Failed to retreive location for IP " + $IPAddress) -isError $hash = @{ IP = $IPAddress CountryName = "Failed to Resolve" - Continent = "Unknown" + RegionName = "Unknown" + RegionCode = "Unknown" ContinentName = "Unknown" City = "Unknown" KnownMicrosoftIP = "Unknown" @@ -81,16 +80,19 @@ Function Get-IPGeolocation { } else { # Determine if this IP is known to be owned by Microsoft - [string]$isMSFTIP = Test-MicrosoftIP -IP $IPAddress -type $geoip.type - + [string]$isMSFTIP = Test-MicrosoftIP -IPToTest $IPAddress -type $geoip.type + if ($isMSFTIP){ + $MSFTIP = $isMSFTIP + } # Push return into a response object $hash = @{ IP = $geoip.ip CountryName = $geoip.country_name - Continent = $geoip.continent_code ContinentName = $geoip.continent_name + RegionName = $geoip.region_name + RegionCode = $geoip.region_code City = $geoip.City - KnownMicrosoftIP = $isMSFTIP + KnownMicrosoftIP = $MSFTIP } $result = New-Object PSObject -Property $hash } diff --git a/Hawk/internal/functions/Get-SimpleAdminAuditLog.ps1 b/Hawk/internal/functions/Get-SimpleAdminAuditLog.ps1 index 30d27cd..3a02679 100644 --- a/Hawk/internal/functions/Get-SimpleAdminAuditLog.ps1 +++ b/Hawk/internal/functions/Get-SimpleAdminAuditLog.ps1 @@ -1,21 +1,21 @@ -<# -.SYNOPSIS - Convert output from search-adminauditlog to be more human readable -.DESCRIPTION - Convert output from search-adminauditlog to be more human readable -.PARAMETER SearchResults - Results from query -.EXAMPLE - PS C:\> - Explanation of what the example does -.INPUTS - Inputs (if any) -.OUTPUTS - Output (if any) -.NOTES - General notes -#> -Function Get-SimpleAdminAuditLog { +Function Get-SimpleAdminAuditLog { + <# + .SYNOPSIS + Convert output from search-adminauditlog to be more human readable + .DESCRIPTION + Convert output from search-adminauditlog to be more human readable + .PARAMETER SearchResults + Results from query + .EXAMPLE + PS C:\> + Explanation of what the example does + .INPUTS + Inputs (if any) + .OUTPUTS + Output (if any) + .NOTES + General notes + #> Param ( [Parameter( Position = 0, @@ -139,4 +139,4 @@ Function Get-SimpleAdminAuditLog { # Return the array set Return $ResultSet } -} +} \ No newline at end of file diff --git a/Hawk/internal/functions/Get-SimpleUnifiedAuditLog.ps1 b/Hawk/internal/functions/Get-SimpleUnifiedAuditLog.ps1 new file mode 100644 index 0000000..c60efde --- /dev/null +++ b/Hawk/internal/functions/Get-SimpleUnifiedAuditLog.ps1 @@ -0,0 +1,287 @@ +function Get-SimpleUnifiedAuditLog { + <# + .SYNOPSIS + Flattens nested Microsoft 365 Unified Audit Log records into a simplified format. + + .DESCRIPTION + This function processes Microsoft 365 Unified Audit Log records by converting nested JSON data + (stored in the AuditData property) into a flat structure suitable for analysis and export. + It handles complex nested objects, arrays, and special cases like parameter collections. + + The function: + - Preserves base record properties + - Flattens nested JSON structures + - Provides special handling for Parameters collections + - Creates human-readable command reconstructions + - Supports type preservation for data analysis + + .PARAMETER Record + A PowerShell object representing a unified audit log record. Typically, this is the output + from Search-UnifiedAuditLog and should contain both base properties and an AuditData + property containing a JSON string of additional audit information. + + .PARAMETER PreserveTypes + When specified, maintains the original data types of values instead of converting them + to strings. This is useful when the output will be used for further PowerShell processing + rather than export to CSV/JSON. + + .EXAMPLE + $auditLogs = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -RecordType ExchangeAdmin + $auditLogs | Get-SimpleUnifiedAuditLog | Export-Csv -Path "AuditLogs.csv" -NoTypeInformation + + Processes Exchange admin audit logs and exports them to CSV with all nested properties flattened. + + .EXAMPLE + $userChanges = Search-UnifiedAuditLog -UserIds user@domain.com -Operations "Add-*" + $userChanges | Get-SimpleUnifiedAuditLog -PreserveTypes | + Where-Object { $_.ResultStatus -eq $true } | + Select-Object CreationTime, Operation, FullCommand + + Gets all "Add" operations for a specific user, preserves data types, filters for successful operations, + and selects specific columns. + + .OUTPUTS + Collection of PSCustomObjects with flattened properties from both the base record and AuditData. + Properties include: + - All base record properties (RecordType, CreationDate, etc.) + - Flattened nested objects with property names using dot notation + - Individual parameters as Param_* properties + - ParameterString containing all parameters in a readable format + - FullCommand showing reconstructed PowerShell command (when applicable) + + .NOTES + Author: Jonathan Butler + Version: 2.0 + Development Date: December 2024 + + The function is designed to handle any RecordType from the Unified Audit Log and will + automatically adapt to changes in the audit log schema. Special handling is implemented + for common patterns like Parameters collections while maintaining flexibility for + other nested structures. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [PSObject]$Record, + + [Parameter(Mandatory = $false)] + [switch]$PreserveTypes + ) + + begin { + # Collection to store processed results + $Results = @() + + function ConvertTo-FlatObject { + <# + .SYNOPSIS + Recursively flattens nested objects into a single-level hashtable. + + .DESCRIPTION + Internal helper function that converts complex nested objects into a flat structure + using dot notation for property names. Handles special cases like Parameters arrays + and preserves type information when requested. + #> + param ( + [Parameter(Mandatory = $true)] + [PSObject]$InputObject, + + [Parameter(Mandatory = $false)] + [string]$Prefix = "", + + [Parameter(Mandatory = $false)] + [switch]$PreserveTypes + ) + + # Initialize hashtable for flattened properties + $flatProperties = @{} + + # Process each property of the input object + foreach ($prop in $InputObject.PSObject.Properties) { + # Build the property key name, incorporating prefix if provided + $key = if ($Prefix) { "${Prefix}_$($prop.Name)" } else { $prop.Name } + + # Special handling for Parameters array - common in UAL records + if ($prop.Name -eq 'Parameters' -and $prop.Value -is [Array]) { + # Create human-readable parameter string + $paramStrings = foreach ($param in $prop.Value) { + "$($param.Name)=$($param.Value)" + } + $flatProperties['ParameterString'] = $paramStrings -join ' | ' + + # Create individual parameter properties + foreach ($param in $prop.Value) { + $paramKey = "Param_$($param.Name)" + $flatProperties[$paramKey] = $param.Value + } + + # Reconstruct full command if Operation property exists + if ($InputObject.Operation) { + $paramStrings = foreach ($param in $prop.Value) { + # Format parameter values based on content + $value = switch -Regex ($param.Value) { + '\s' { "'$($param.Value)'" } # Quote values containing spaces + '^True$|^False$' { "`$$($param.Value.ToLower())" } # Format booleans + ';' { "'$($param.Value)'" } # Quote values containing semicolons + default { $param.Value } + } + "-$($param.Name) $value" + } + $flatProperties['FullCommand'] = "$($InputObject.Operation) $($paramStrings -join ' ')" + } + continue + } + + # Handle different value types + switch ($prop.Value) { + # Recursively process nested hashtables + { $_ -is [System.Collections.IDictionary] } { + $nestedObject = ConvertTo-FlatObject -InputObject $_ -Prefix $key -PreserveTypes:$PreserveTypes + $flatProperties += $nestedObject + } + # Process arrays (excluding Parameters which was handled above) + { $_ -is [System.Collections.IList] -and $prop.Name -ne 'Parameters' } { + if ($_.Count -gt 0) { + if ($_[0] -is [PSObject]) { + # Handle array of objects + for ($i = 0; $i -lt $_.Count; $i++) { + $nestedObject = ConvertTo-FlatObject -InputObject $_[$i] -Prefix "${key}_${i}" -PreserveTypes:$PreserveTypes + $flatProperties += $nestedObject + } + } + else { + # Handle array of simple values + $flatProperties[$key] = $_ -join "|" + } + } + else { + # Handle empty arrays + $flatProperties[$key] = [string]::Empty + } + } + # Recursively process nested objects + { $_ -is [PSObject] } { + $nestedObject = ConvertTo-FlatObject -InputObject $_ -Prefix $key -PreserveTypes:$PreserveTypes + $flatProperties += $nestedObject + } + # Handle simple values + default { + if ($PreserveTypes) { + # Keep original type if PreserveTypes is specified + $flatProperties[$key] = $_ + } + else { + # Convert values to appropriate types + $flatProperties[$key] = switch ($_) { + { $_ -is [datetime] } { $_ } + { $_ -is [bool] } { $_ } + { $_ -is [int] } { $_ } + { $_ -is [long] } { $_ } + { $_ -is [decimal] } { $_ } + { $_ -is [double] } { $_ } + default { [string]$_ } + } + } + } + } + } + + return $flatProperties + } + } + + process { + try { + # Extract base properties excluding AuditData + $baseProperties = $Record | Select-Object * -ExcludeProperty AuditData + + # Process AuditData if present + $auditData = $Record.AuditData | ConvertFrom-Json + if ($auditData) { + # Flatten the audit data + $flatAuditData = ConvertTo-FlatObject -InputObject $auditData -PreserveTypes:$PreserveTypes + + # Combine base properties with flattened audit data + $combinedProperties = @{} + $baseProperties.PSObject.Properties | ForEach-Object { $combinedProperties[$_.Name] = $_.Value } + $flatAuditData.GetEnumerator() | ForEach-Object { $combinedProperties[$_.Key] = $_.Value } + + # Create and store the result + $Results += [PSCustomObject]$combinedProperties + } + } + catch { + # Handle and log any processing errors + Write-Warning "Error processing record: $_" + $errorProperties = @{ + RecordType = $Record.RecordType + CreationDate = Get-Date + Error = $_.Exception.Message + Record = $Record + } + $Results += [PSCustomObject]$errorProperties + } + } + + end { + # Define the ordered common schema properties + $orderedProperties = @( + 'CreationTime', + 'Workload', + 'RecordType', + 'Operation', + 'ResultStatus', + 'ClientIP', + 'UserId', + 'Id', + 'OrganizationId', + 'UserType', + 'UserKey', + 'ObjectId', + 'Scope', + 'AppAccessContext' + ) + + # Process each result to ensure proper property ordering + $orderedResults = $Results | ForEach-Object { + $orderedObject = [ordered]@{} + + # Add ordered common schema properties first + foreach ($prop in $orderedProperties) { + if ($_.PSObject.Properties.Name -contains $prop) { + $orderedObject[$prop] = $_.$prop + } + } + + # Add ParameterString if it exists + if ($_.PSObject.Properties.Name -contains 'ParameterString') { + $orderedObject['ParameterString'] = $_.ParameterString + + # Add all Param_* properties immediately after ParameterString + $_.PSObject.Properties | + Where-Object { $_.Name -like 'Param_*' } | + Sort-Object Name | + ForEach-Object { + $orderedObject[$_.Name] = $_.Value + } + } + + # Add all remaining properties that aren't already added + $_.PSObject.Properties | + Where-Object { + $_.Name -notin $orderedProperties -and + $_.Name -ne 'ParameterString' -and + $_.Name -notlike 'Param_*' + } | + ForEach-Object { + $orderedObject[$_.Name] = $_.Value + } + + # Return the ordered object + [PSCustomObject]$orderedObject + } + + # Return all processed results with ordered properties + $orderedResults + } +} \ No newline at end of file diff --git a/Hawk/internal/functions/Import-AzureAuthenticationLog.ps1 b/Hawk/internal/functions/Import-AzureAuthenticationLog.ps1 new file mode 100644 index 0000000..98290a4 --- /dev/null +++ b/Hawk/internal/functions/Import-AzureAuthenticationLog.ps1 @@ -0,0 +1,182 @@ + +Function Import-AzureAuthenticationLog { + <# + .SYNOPSIS + Takes in a set of azure Authentication logs and combines them into a unified output + .DESCRIPTION + Takes in a set of azure Authentication logs and combines them into a unified output + .PARAMETER JsonConvertedLogs + Logs that are converted + .EXAMPLE + Import-AzureAuthenticationLog + Imprts Azure Auth logs + .NOTES + General notes + #> + Param([array]$JsonConvertedLogs) + + # Null out the output object + $Listoutput = $null + $baseproperties = $null + $i = 0 + + # Create the output list array + $ListOutput = New-Object System.Collections.ArrayList + $baseproperties = New-Object System.Collections.ArrayList + + # Process each entry in the array + foreach ($entry in $JsonConvertedLogs) { + + if ([bool]($i % 25)) { } + Else { + Write-Progress -Activity "Converting Json Entries" -CurrentOperation ("Entry " + $i) -PercentComplete (($i / $JsonConvertedLogs.count) * 100) -Status ("Processing") + } + + # null out a temp object and create it as a new custom ps object + $processedentry = $null + $processedentry = New-Object -TypeName PSobject + + # Look at each member of the entry ... we want to process each in turn and add them to a new object + foreach ($member in ($entry | get-member -MemberType NoteProperty)) { + + # Identity unique properties and add to property list of base object if not present + if ($baseproperties -contains $member.name) { } + else { + $baseproperties.add($member.name) | Out-Null + } + + # Switch statement to deal with known "special" properties + switch ($member.name) { + # Extended properties can contain addtional values so we need to expand those + ExtendedProperties { + # Null check + if ($null -eq $entry.ExtendedProperties) { } + else { + # expand out each entry and add it to the base properties and to the property of our exported object + Foreach ($Object in $entry.ExtendedProperties) { + # Identity unique properties and add to property list of base object if not present + if ($baseproperties -contains $object.name) { } + else { + $baseproperties.add($object.name) | out-null + } + + # For some entries a property can appear in ExtendedProperties and as a normal property + # We need to deal with this situation + try { + # Now add the entry from extendedproperties to the overall properties list + $processedentry | Add-Member -MemberType NoteProperty -Name $object.name -Value $object.value -ErrorAction SilentlyContinue + } + catch { + if ((($error[0].FullyQualifiedErrorId).split(",")[0]) -eq "MemberAlreadyExists") { } + } + } + + # Convert our extended properties into a string and add that just for fidelity + # null the output string + [string]$epstring = $null + + # Convert into a string that is , seperated but with : seperating name and value + foreach ($ep in $entry.extendedproperties) { + [string]$epstring += $ep.name + ":" + $ep.v + "," + } + + # We also still want to add extendedproperties in as is just for fidelity + $processedentry | Add-Member -MemberType NoteProperty -Name ExtendedProperties -Value ($epstring.TrimEnd(",")) + } + } + # Need to convert this from a system object into a string + # This is an initial pass at this might be a better way to do it + Actor { + if ($null -eq $entry.actor) { } + else { + # null the output string + [string]$actorstring = $null + + # Convert into a string that is , seperated but with : seperating ID and type + foreach ($actor in $entry.actor) { + [string]$actorstring += $actor.id + ":" + $actor.type + "," + } + + # Add the string to the output + $processedentry | Add-Member -MemberType NoteProperty -Name "Actor" -Value ($actorstring.TrimEnd(",")) + } + } + Target { + if ($null -eq $entry.target) { } + else { + # null the output string + [string]$targetstring = $null + + # Convert into a string that is , seperated but with : seperating ID and type + foreach ($target in $entry.target) { + [string]$targetstring += $target.id + ":" + $target.type + "," + } + + # Add the string to the output + $processedentry | Add-Member -MemberType NoteProperty -Name "Target" -Value ($targetstring.TrimEnd(",")) + } + } + Creationtime { + $processedentry | Add-Member -MemberType NoteProperty -Name CreationTime -value (get-date $entry.Creationtime -format g) + } + Default { + # For some entries a property can appear in ExtendedProperties and as a normal property + # We need to deal with this situation + try { + # Now add the entry from extendedproperties to the overall properties list + $processedentry | Add-Member -MemberType NoteProperty -Name $member.name -Value $entry.($member.name) -ErrorAction SilentlyContinue + } + catch { + if ((($error[0].FullyQualifiedErrorId).split(",")[0]) -eq "MemberAlreadyExists") { } + } + } + } + } + + # Increment our counter + $i++ + + # Add to output object + $Listoutput.add($processedentry) | Out-Null + } + + Write-Progress -Completed -Activity "Converting Json Entries" -Status " " + + # Build a base object using all unique property names + $baseobject = $null + $baseobject = New-Object -TypeName PSobject + foreach ($propertyname in $baseproperties) { + switch ($propertyname) { + CreationTime { $baseobject | Add-Member -MemberType NoteProperty -Name $propertyname -Value (get-date 01/01/1900 -format g) } + Default { $baseobject | Add-Member -MemberType NoteProperty -Name $propertyname -Value "Base" } + } + } + + # Add that object to the output + $Listoutput.add($baseobject) | Out-Null + + # Base object HAS to be the first entry in the output so that when it is written to CSV it includes all properties + [array]$sortedoutput = $Listoutput | Sort-Object -Property creationtime + $sortedoutput = $sortedoutput | Where-Object { $_.ClientIP -ne 'Base' } + + # Build an ordered arry to use to order the output coloums + # Key coloums that we want ordered at the begining of the output + [array]$baseorder = "CreationTime", "UserId", "Workload", "ClientIP", "CountryName", "KnownMicrosoftIP" + + foreach ($coloumheader in $baseorder) { + # If the coloum header exists as one of our base properties then add to to coloumorder array and remove from baseproperties list + if ($baseproperties -contains $coloumheader) { + [array]$coloumorder += $coloumheader + $baseproperties.remove($coloumheader) + } + else { } + } + + # Add all of the remaining base properties to the sort order array + [array]$coloumorder += $baseproperties + + $sortedoutput = $sortedoutput | Select-Object $coloumorder + + # write-host $baseproperties + return $sortedoutput + } \ No newline at end of file diff --git a/Hawk/internal/functions/Import-AzureAuthenticationLogs.ps1 b/Hawk/internal/functions/Import-AzureAuthenticationLogs.ps1 deleted file mode 100644 index 3c03f85..0000000 --- a/Hawk/internal/functions/Import-AzureAuthenticationLogs.ps1 +++ /dev/null @@ -1,182 +0,0 @@ - -Function Import-AzureAuthenticationLogs { -<# -.SYNOPSIS - Takes in a set of azure Authentication logs and combines them into a unified output -.DESCRIPTION - Takes in a set of azure Authentication logs and combines them into a unified output -.PARAMETER JsonConvertedLogs - Logs that are converted -.EXAMPLE - Import-AzureAuthenticationLogs - Imprts Azure Auth logs -.NOTES - General notes -#> - Param([array]$JsonConvertedLogs) - - # Null out the output object - $Listoutput = $null - $baseproperties = $null - $i = 0 - - # Create the output list array - $ListOutput = New-Object System.Collections.ArrayList - $baseproperties = New-Object System.Collections.ArrayList - - # Process each entry in the array - foreach ($entry in $JsonConvertedLogs) { - - if ([bool]($i % 25)) { } - Else { - Write-Progress -Activity "Converting Json Entries" -CurrentOperation ("Entry " + $i) -PercentComplete (($i / $JsonConvertedLogs.count) * 100) -Status ("Processing") - } - - # null out a temp object and create it as a new custom ps object - $processedentry = $null - $processedentry = New-Object -TypeName PSobject - - # Look at each member of the entry ... we want to process each in turn and add them to a new object - foreach ($member in ($entry | get-member -MemberType NoteProperty)) { - - # Identity unique properties and add to property list of base object if not present - if ($baseproperties -contains $member.name) { } - else { - $baseproperties.add($member.name) | Out-Null - } - - # Switch statement to deal with known "special" properties - switch ($member.name) { - # Extended properties can contain addtional values so we need to expand those - ExtendedProperties { - # Null check - if ($null -eq $entry.ExtendedProperties) { } - else { - # expand out each entry and add it to the base properties and to the property of our exported object - Foreach ($Object in $entry.ExtendedProperties) { - # Identity unique properties and add to property list of base object if not present - if ($baseproperties -contains $object.name) { } - else { - $baseproperties.add($object.name) | out-null - } - - # For some entries a property can appear in ExtendedProperties and as a normal property - # We need to deal with this situation - try { - # Now add the entry from extendedproperties to the overall properties list - $processedentry | Add-Member -MemberType NoteProperty -Name $object.name -Value $object.value -ErrorAction SilentlyContinue - } - catch { - if ((($error[0].FullyQualifiedErrorId).split(",")[0]) -eq "MemberAlreadyExists") { } - } - } - - # Convert our extended properties into a string and add that just for fidelity - # null the output string - [string]$epstring = $null - - # Convert into a string that is , seperated but with : seperating name and value - foreach ($ep in $entry.extendedproperties) { - [string]$epstring += $ep.name + ":" + $ep.v + "," - } - - # We also still want to add extendedproperties in as is just for fidelity - $processedentry | Add-Member -MemberType NoteProperty -Name ExtendedProperties -Value ($epstring.TrimEnd(",")) - } - } - # Need to convert this from a system object into a string - # This is an initial pass at this might be a better way to do it - Actor { - if ($null -eq $entry.actor) { } - else { - # null the output string - [string]$actorstring = $null - - # Convert into a string that is , seperated but with : seperating ID and type - foreach ($actor in $entry.actor) { - [string]$actorstring += $actor.id + ":" + $actor.type + "," - } - - # Add the string to the output - $processedentry | Add-Member -MemberType NoteProperty -Name "Actor" -Value ($actorstring.TrimEnd(",")) - } - } - Target { - if ($null -eq $entry.target) { } - else { - # null the output string - [string]$targetstring = $null - - # Convert into a string that is , seperated but with : seperating ID and type - foreach ($target in $entry.target) { - [string]$targetstring += $target.id + ":" + $target.type + "," - } - - # Add the string to the output - $processedentry | Add-Member -MemberType NoteProperty -Name "Target" -Value ($targetstring.TrimEnd(",")) - } - } - Creationtime { - $processedentry | Add-Member -MemberType NoteProperty -Name CreationTime -value (get-date $entry.Creationtime -format g) - } - Default { - # For some entries a property can appear in ExtendedProperties and as a normal property - # We need to deal with this situation - try { - # Now add the entry from extendedproperties to the overall properties list - $processedentry | Add-Member -MemberType NoteProperty -Name $member.name -Value $entry.($member.name) -ErrorAction SilentlyContinue - } - catch { - if ((($error[0].FullyQualifiedErrorId).split(",")[0]) -eq "MemberAlreadyExists") { } - } - } - } - } - - # Increment our counter - $i++ - - # Add to output object - $Listoutput.add($processedentry) | Out-Null - } - - Write-Progress -Completed -Activity "Converting Json Entries" -Status " " - - # Build a base object using all unique property names - $baseobject = $null - $baseobject = New-Object -TypeName PSobject - foreach ($propertyname in $baseproperties) { - switch ($propertyname) { - CreationTime { $baseobject | Add-Member -MemberType NoteProperty -Name $propertyname -Value (get-date 01/01/1900 -format g) } - Default { $baseobject | Add-Member -MemberType NoteProperty -Name $propertyname -Value "Base" } - } - } - - # Add that object to the output - $Listoutput.add($baseobject) | Out-Null - - # Base object HAS to be the first entry in the output so that when it is written to CSV it includes all properties - [array]$sortedoutput = $Listoutput | Sort-Object -Property creationtime - $sortedoutput = $sortedoutput | Where-Object { $_.ClientIP -ne 'Base' } - - # Build an ordered arry to use to order the output coloums - # Key coloums that we want ordered at the begining of the output - [array]$baseorder = "CreationTime", "UserId", "Workload", "ClientIP", "CountryName", "KnownMicrosoftIP" - - foreach ($coloumheader in $baseorder) { - # If the coloum header exists as one of our base properties then add to to coloumorder array and remove from baseproperties list - if ($baseproperties -contains $coloumheader) { - [array]$coloumorder += $coloumheader - $baseproperties.remove($coloumheader) - } - else { } - } - - # Add all of the remaining base properties to the sort order array - [array]$coloumorder += $baseproperties - - $sortedoutput = $sortedoutput | Select-Object $coloumorder - - # write-host $baseproperties - return $sortedoutput -} \ No newline at end of file diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index c9e4b23..82d35ba 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -1,5 +1,5 @@ Function Initialize-HawkGlobalObject { -<# + <# .SYNOPSIS Create global variable $Hawk for use by all Hawk cmdlets. .DESCRIPTION @@ -10,8 +10,6 @@ * Records target start and end dates for searches .PARAMETER Force Switch to force the function to run and allow the variable to be recreated -.PARAMETER IAgreeToTheEula - Agrees to the EULA on the command line to skip the prompt. .PARAMETER SkipUpdate Skips checking for the latest version of the Hawk Module .PARAMETER DaysToLookBack @@ -33,19 +31,16 @@ StartDate Calculated start date for searches based on DaysToLookBack EndDate One day in the future WhenCreated Date and time that the variable was created - EULA If you have agreed to the EULA or not .EXAMPLE Initialize-HawkGlobalObject -Force This Command will force the creation of a new $Hawk variable even if one already exists. #> - [CmdletBinding()] + [CmdletBinding()] param ( [switch]$Force, - [switch]$IAgreeToTheEula, [switch]$SkipUpdate, - [int]$DaysToLookBack, [DateTime]$StartDate, [DateTime]$EndDate, [string]$FilePath @@ -75,10 +70,11 @@ } Function New-LoggingFolder { + [CmdletBinding(SupportsShouldProcess)] param([string]$RootPath) # Create a folder ID based on date - [string]$TenantName = (Get-MGDomain | Where-Object {$_.isDefault}).ID + [string]$TenantName = (Get-MGDomain | Where-Object { $_.isDefault }).ID [string]$FolderID = "Hawk_" + $TenantName.Substring(0, $TenantName.IndexOf('.')) + "_" + (Get-Date -UFormat %Y%m%d_%H%M).tostring() # Add that ID to the given path @@ -98,6 +94,7 @@ } Function Set-LoggingPath { + [CmdletBinding(SupportsShouldProcess)] param ([string]$Path) # If no value of Path is provided prompt and gather from the user @@ -139,62 +136,13 @@ Return $Folder } - Function Get-Eula { - - if ([string]::IsNullOrEmpty($Hawk.EULA)) { - Write-Information (' - - DISCLAIMER: - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - ') - - # Prompt the user to agree with EULA - $title = "Disclaimer" - $message = "Do you agree with the above disclaimer?" - $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Logs agreement and continues use of the Hawk Functions." - $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Stops execution of Hawk Functions" - $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) - $result = $host.ui.PromptForChoice($title, $message, $options, 0) - # If yes log and continue - # If no log error and exit - switch ($result) { - 0 { - Write-Information "`n" - Return ("Agreed " + (Get-Date)).ToString() - } - 1 { - Write-Information "Aborting Cmdlet" - Write-Error -Message "Failure to agree with EULA" -ErrorAction Stop - break - } - } - } - else { Return $Hawk.EULA } - - } - Function New-ApplicationInsight { + [CmdletBinding(SupportsShouldProcess)] + param() # Initialize Application Insights client $insightkey = "b69ffd8b-4569-497c-8ee7-b71b8257390e" if ($Null -eq $Client) { - Write-Information "Initializing Application Insights" + Write-Output "Initializing Application Insights" $Client = New-AIClient -key $insightkey } } @@ -218,23 +166,12 @@ } # Test if we have a connection to Microsoft Graph - $notification = New-Object -ComObject Wscript.Shell - $Output =$notification.Popup("Hawk has been updated to support MGGraph due to MSONLINE deprecation. Please click OK to continue", 0, "Hawk Update", 0x00000040) Write-Information "Testing Graph Connection" Test-GraphConnection - + # If the global variable Hawk doesn't exist or we have -force then set the variable up Write-Information "Setting Up initial Hawk environment variable" - ### Validating EULA ### - if ($IAgreeToTheEula) { - # Customer has accepted the EULA on the command line - [string]$Eula = ("Agreed " + (Get-Date)) - } - else { - [string]$Eula = Get-Eula - } - #### Checking log path and setting up subdirectory ### # If we have a path passed in then we need to check that otherwise ask if ([string]::IsNullOrEmpty($FilePath)) { @@ -248,7 +185,7 @@ if ($null -eq $StartDate) { # Read in our # of days back or the actual start date - $StartRead = Read-Host "`nFirst Day of Search Window (1-90, Date, Default 90)" + $StartRead = Read-Host "`nPlease Enter First Day of Search Window (1-90, Date, Default 90)" # Determine if the input was a date time # True means it was NOT a datetime @@ -264,9 +201,8 @@ } # Calculate our startdate setting it to midnight - Write-Information ("Calculating Start Date from current date minus " + $StartRead + " days.") [DateTime]$StartDate = ((Get-Date).AddDays(-$StartRead)).Date - Write-Information ("Setting StartDate by Calculation to " + $StartDate + "`n") + Write-Information ("Start Date: " + $StartDate + "") } elseif (!($null -eq ($StartRead -as [DateTime]))) { #### DATE TIME Provided #### @@ -283,8 +219,6 @@ Write-Information ("Setting date to default of Today - 90 days.") [DateTime]$StartDate = ((Get-Date).AddDays(-90)).Date } - - Write-Information ("Setting StartDate by Date to " + $StartDate + "`n") } else { Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop @@ -293,7 +227,7 @@ if ($null -eq $EndDate) { # Read in the end date - $EndRead = Read-Host "`nLast Day of search Window (1-90, date, Default Today)" + $EndRead = Read-Host "`nPlease Enter Last Day of Search Window (1-90, date, Default Today)" # Determine if the input was a date time # True means it was NOT a datetime @@ -302,12 +236,11 @@ # if we have a null entry (just hit enter) then set startread to the default of 90 if ([string]::IsNullOrEmpty($EndRead)) { - Write-Information ("Setting End Date to Today") [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date } else { # Calculate our startdate setting it to midnight - Write-Information ("Calculating End Date from current date minus " + $EndRead + " days.") + Write-Information ("End Date: " + $EndRead + " days.") # Subtract 1 from the EndRead entry so that we get one day less for the purpose of how searching works with times [DateTime]$EndDate = ((Get-Date).AddDays( - ($EndRead - 1))).Date } @@ -317,7 +250,7 @@ Write-Error "StartDate Cannot be More Recent than EndDate" -ErrorAction Stop } else { - Write-Information ("Setting EndDate by Calculation to " + $EndDate + "`n") + Write-Information ("End Date: " + $EndDate + "`n") } } elseif (!($null -eq ($EndRead -as [DateTime]))) { @@ -332,7 +265,7 @@ Write-Information "Setting EndDate to today." [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date } - elseif ($EndDate -gt (get-Date).AddDays(2)){ + elseif ($EndDate -gt (get-Date).AddDays(2)) { Write-Information "EndDate to Far in the furture." Write-Information "Setting EndDate to Today." [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date @@ -359,35 +292,43 @@ [bool]$AdvancedAzureLicense = $false } - # Configuration Example, currently not used - #TODO: Implement Configuration system across entire project - Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value $Days -PassThru | Register-PSFConfig - if ($OutputPath) { - Set-PSFConfig -Module 'Hawk' -Name 'FilePath' -Value $OutputPath -PassThru | Register-PSFConfig - } + # Configuration Example, currently not used + #TODO: Implement Configuration system across entire project + Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value $Days -PassThru | Register-PSFConfig + if ($OutputPath) { + Set-PSFConfig -Module 'Hawk' -Name 'FilePath' -Value $OutputPath -PassThru | Register-PSFConfig + } - #TODO: Discard below once migration to configuration is completed + #TODO: Discard below once migration to configuration is completed $Output = [PSCustomObject]@{ - FilePath = $OutputPath - DaysToLookBack = $Days - StartDate = $StartDate - EndDate = $EndDate - AdvancedAzureLicense = $AdvancedAzureLicense - WhenCreated = (Get-Date -Format g) - EULA = $Eula - } + FilePath = $OutputPath + DaysToLookBack = $Days + StartDate = $StartDate + EndDate = $EndDate + AdvancedAzureLicense = $AdvancedAzureLicense + WhenCreated = (Get-Date -Format g) + } # Create the script hawk variable Write-Information "Setting up Script Hawk environment variable`n" New-Variable -Name Hawk -Scope Script -value $Output -Force - Out-LogFile "Script Variable Configured" - Out-LogFile ("*** Version " + (Get-Module Hawk).version + " ***") - Out-LogFile $Hawk - + Out-LogFile "Script Variable Configured" -Information + Out-LogFile ("Hawk Version: " + (Get-Module Hawk).version) -Information + # Print each property of $Hawk on its own line + foreach ($prop in $Hawk.PSObject.Properties) { + # If the property value is $null or an empty string, display "N/A" + $value = if ($null -eq $prop.Value -or [string]::IsNullOrEmpty($prop.Value.ToString())) { + "N/A" + } else { + $prop.Value + } + + Out-LogFile ("{0} = {1}" -f $prop.Name, $value) -Information + } #### End of IF } else { Write-Information "Valid Hawk Object already exists no actions will be taken." } -} +} \ No newline at end of file diff --git a/Hawk/internal/functions/Out-HawkAppData.ps1 b/Hawk/internal/functions/Out-HawkAppData.ps1 index 721f23e..99efbd9 100644 --- a/Hawk/internal/functions/Out-HawkAppData.ps1 +++ b/Hawk/internal/functions/Out-HawkAppData.ps1 @@ -24,6 +24,6 @@ Function Out-HawkAppData { $null = New-Item -ItemType Directory -Path $HawkAppdataFolder } - Out-LogFile ("Recording HawkAppData to file " + $HawkAppdataPath) + Out-LogFile ("Recording HawkAppData to file " + $HawkAppdataPath) -Action $global:HawkAppData | ConvertTo-Json | Out-File -FilePath $HawkAppdataPath -Force } \ No newline at end of file diff --git a/Hawk/internal/functions/Out-LogFile.ps1 b/Hawk/internal/functions/Out-LogFile.ps1 index 11fc834..9ba305a 100644 --- a/Hawk/internal/functions/Out-LogFile.ps1 +++ b/Hawk/internal/functions/Out-LogFile.ps1 @@ -1,32 +1,100 @@ -<# -.SYNOPSIS - Writes output to a log file with a time date stamp -.DESCRIPTION - Writes output to a log file with a time date stamp -.PARAMETER string - Log Message -.PARAMETER action - What is happening -.PARAMETER notice - Verbose notification -.PARAMETER silentnotice - Silent notification -.EXAMPLE - Out-LogFile - Sends messages to the log file -.NOTES - This is will depracted soon. -#> -Function Out-LogFile { +Function Out-LogFile { + <# + .SYNOPSIS + Writes output to a log file with a time date stamp. + .DESCRIPTION + Writes output to a log file with a time date stamp and appropriate prefixes + based on the type of message. By default, messages are also displayed on the screen + unless the -NoDisplay switch is used. + + Message types: + - Action: Represent ongoing operations or procedures. + - Error: Represent failures, exceptions, or error conditions that prevented successful execution. + - Investigate (notice, silentnotice): Represent events that require attention or hold + investigative value. + - Information: Represent successful completion or informational status updates + that do not require action or investigation. + + .PARAMETER string + The log message to be written. + + .PARAMETER action + Switch indicating the log entry is describing an action being performed. + + .PARAMETER isError + Switch indicating the log entry represents an error condition or failure. + The output is prefixed with [ERROR] in the log file. + + .PARAMETER notice + Switch indicating the log entry requires investigation or special attention. + + .PARAMETER silentnotice + Switch indicating additional investigative information that should not be + displayed on the screen. This is logged to the file but suppressed in console output. + + .PARAMETER NoDisplay + Switch indicating the message should only be written to the log file, + not displayed in the console. + + .PARAMETER Information + Switch indicating the log entry provides informational status or completion messages, + for example: "Retrieved all results" or "Completed data export successfully." + + .EXAMPLE + Out-LogFile "Routine scan completed." + + Writes a simple log message with a timestamp to the log file and displays it on the screen. + + .EXAMPLE + Out-LogFile "Starting mailbox export operation" -action + + Writes a log message indicating an action is being performed. + The output is prefixed with [ACTION] in the log file. + + .EXAMPLE + Out-LogFile "Failed to connect to Exchange Online" -isError + + Writes a log message indicating an error condition. + The output is prefixed with [ERROR] in the log file. + + .EXAMPLE + Out-LogFile "Detected suspicious login attempt from external IP" -notice + + Writes a log message indicating a situation requiring investigation. + The output is prefixed with [INVESTIGATE] and also recorded in a separate _Investigate.txt file. + + .EXAMPLE + Out-LogFile "User mailbox configuration details" -silentnotice + + Writes investigative detail to the log and _Investigate.txt file without printing to the console. + This is useful for adding detail to a previously logged [INVESTIGATE] event without cluttering the console. + + .EXAMPLE + Out-LogFile "Retrieved all results successfully" -Information + + Writes a log message indicating a successful or informational event. + The output is prefixed with [INFO], suitable for status updates or completion notices. + + .EXAMPLE + Out-LogFile "Executing periodic health check" -NoDisplay + + Writes a log message to the file without displaying it on the console, + useful for routine logging that doesn't need immediate user visibility. + #> + [CmdletBinding()] Param ( + [Parameter(Mandatory = $true)] [string]$string, [switch]$action, [switch]$notice, - [switch]$silentnotice - ) + [switch]$silentnotice, + [switch]$isError, + [switch]$NoDisplay, + [switch]$Information + ) - Write-PSFMessage -Message $string -ModuleName Hawk -FunctionName (Get-PSCallstack)[1].FunctionName + Write-PSFMessage -Message $string -ModuleName Hawk -FunctionName (Get-PSCallstack)[1].FunctionName # Make sure we have the Hawk Global Object if ([string]::IsNullOrEmpty($Hawk.FilePath)) { @@ -35,52 +103,52 @@ Function Out-LogFile { # Get our log file path $LogFile = Join-path $Hawk.FilePath "Hawk.log" - $ScreenOutput = $true + $ScreenOutput = -not $NoDisplay $LogOutput = $true # Get the current date - [string]$date = Get-Date -Format G + [string]$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + [string]$logstring = "" - # Deal with each switch and what log string it should put out and if any special output - - # Action indicates that we are starting to do something + # Build the log string based on the type of message if ($action) { - [string]$logstring = ( "[" + $date + "] - [ACTION] - " + $string) - + $logstring = "[$timestamp] - [ACTION] - $string" + } + elseif ($isError) { + $logstring = "[$timestamp] - [ERROR] - $string" } - # If notice is true the we should write this to interesting.txt as well elseif ($notice) { - [string]$logstring = ( "[" + $date + "] - ## INVESTIGATE ## - " + $string) + $logstring = "[$timestamp] - [INVESTIGATE] - $string" - # Build the file name for Investigate stuff log + # Write to the investigation file [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt" $logstring | Out-File -FilePath $InvestigateFile -Append } - # For silent we need to supress the screen output elseif ($silentnotice) { - [string]$logstring = ( "Addtional Information: " + $string) - # Build the file name for Investigate stuff log + $logstring = "[$timestamp] - [INVESTIGATE] - Additional Information: $string" + + # Write to the investigation file [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt" $logstring | Out-File -FilePath $InvestigateFile -Append - # Supress screen and normal log output + # Suppress regular output for silentnotice $ScreenOutput = $false $LogOutput = $false - } - # Normal output + elseif ($Information) { + $logstring = "[$timestamp] - [INFO] - $string" + } else { - [string]$logstring = ( "[" + $date + "] - " + $string) + $logstring = "[$timestamp] - $string" } - # Write everything to our log file + # Write to log file if enabled if ($LogOutput) { $logstring | Out-File -FilePath $LogFile -Append } - # Output to the screen + # Write to screen if enabled if ($ScreenOutput) { Write-Information -MessageData $logstring -InformationAction Continue } - } \ No newline at end of file diff --git a/Hawk/internal/functions/Out-MultipleFileType.ps1 b/Hawk/internal/functions/Out-MultipleFileType.ps1 index 5fae6b2..7110618 100644 --- a/Hawk/internal/functions/Out-MultipleFileType.ps1 +++ b/Hawk/internal/functions/Out-MultipleFileType.ps1 @@ -17,6 +17,8 @@ csv file format .PARAMETER txt txt file format +.PARAMETER json + Export data in JSON format. The data will be converted using ConvertTo-Json with a depth of 100 to preserve object structure. .PARAMETER Notice Notification that data retrieved meets the investigation criteria .EXAMPLE @@ -46,7 +48,7 @@ Function Out-MultipleFileType { # If no file types were specified then we need to error out here if (($xml -eq $false) -and ($csv -eq $false) -and ($txt -eq $false) -and ($json -eq $false)) { - Out-LogFile "[ERROR] - No output type specified on object" + Out-LogFile "No output type specified on object" -isError Write-Error -Message "No output type specified on object" -ErrorAction Stop } @@ -59,7 +61,7 @@ Function Out-MultipleFileType { # Test the path if it is there do nothing otherwise create it if (test-path $path) { } else { - Out-LogFile ("Making output directory for Tenant " + $Path) + Out-LogFile ("Making output directory for Tenant " + $Path) -Action $Null = New-Item $Path -ItemType Directory } } @@ -74,7 +76,7 @@ Function Out-MultipleFileType { # Test the path if it is there do nothing otherwise create it if (test-path $path) { } else { - Out-LogFile ("Making output directory for user " + $Path) + Out-LogFile ("Making output directory for user " + $Path) -Action $Null = New-Item $Path -ItemType Directory } } @@ -89,7 +91,7 @@ Function Out-MultipleFileType { end { if ($null -eq $AllObject) { - Out-LogFile "No Data Found" + Out-LogFile "No Data Found" -Information } else { @@ -100,7 +102,7 @@ Function Out-MultipleFileType { $xmlpath = Join-path $Path XML if (Test-path $xmlPath) { } else { - Out-LogFile ("Making output directory for xml files " + $xmlPath) + Out-LogFile ("Making output directory for xml files " + $xmlPath) -Action $null = New-Item $xmlPath -ItemType Directory } @@ -111,7 +113,7 @@ Function Out-MultipleFileType { else { $filename = Join-Path $xmlPath ($FilePrefix + ".xml") } - Out-LogFile ("Writing Data to " + $filename) + Out-LogFile ("Writing Data to " + $filename) -Action # Output our objects to clixml $AllObject | Export-Clixml $filename @@ -133,7 +135,7 @@ Function Out-MultipleFileType { # If we have -append then append the data if ($append) { - Out-LogFile ("Appending Data to " + $filename) + Out-LogFile ("Appending Data to " + $filename) -NoDisplay # Write it out to csv making sture to append $AllObject | Export-Csv $filename -NoTypeInformation -Append -Encoding UTF8 @@ -141,7 +143,7 @@ Function Out-MultipleFileType { # Otherwise overwrite else { - Out-LogFile ("Writing Data to " + $filename) + Out-LogFile ("Writing Data to " + $filename) -Action $AllObject | Export-Csv $filename -NoTypeInformation -Encoding UTF8 } @@ -161,13 +163,13 @@ Function Out-MultipleFileType { # If we have -append then append the data if ($Append) { - Out-LogFile ("Appending Data to " + $filename) + Out-LogFile ("Appending Data to " + $filename) -NoDisplay $AllObject | Format-List * | Out-File $filename -Append } # Otherwise overwrite else { - Out-LogFile ("Writing Data to " + $filename) + Out-LogFile ("Writing Data to " + $filename) -Action $AllObject | Format-List * | Out-File $filename } @@ -188,7 +190,7 @@ Function Out-MultipleFileType { # If we have -append then append the data if ($append) { - Out-LogFile ("Appending Data to " + $filename) + Out-LogFile ("Appending Data to " + $filename) -NoDisplay # Write it out to json making sture to append $AllObject | ConvertTo-Json -Depth 100 | Out-File -FilePath $filename -Append @@ -196,7 +198,7 @@ Function Out-MultipleFileType { # Otherwise overwrite else { - Out-LogFile ("Writing Data to " + $filename) + Out-LogFile ("Writing Data to " + $filename) -Action $AllObject | ConvertTo-Json -Depth 100 | Out-File -FilePath $filename } diff --git a/Hawk/internal/functions/Out-Report.ps1 b/Hawk/internal/functions/Out-Report.ps1 index b667d13..e29a4a3 100644 --- a/Hawk/internal/functions/Out-Report.ps1 +++ b/Hawk/internal/functions/Out-Report.ps1 @@ -1,212 +1,212 @@ Function Out-Report { -<# -.SYNOPSIS - Adds the data to an XML report -.DESCRIPTION - Adds the data to an XML report -.PARAMETER Identity - User Identity for the report -.PARAMETER Property - xml property -.PARAMETER Value - Value of xml element -.PARAMETER Description - Description of element -.PARAMETER State - Color mapping -.PARAMETER Link - Element link -.EXAMPLE - Out-Report - Add the data to an XML report -.NOTES - General notes -#> - Param - ( - [Parameter(Mandatory = $true)] - [string]$Identity, - [Parameter(Mandatory = $true)] - [string]$Property, - [Parameter(Mandatory = $true)] - [string]$Value, - [string]$Description, - [string]$State, - [string]$Link - ) - - # Force the case on all our critical values - #$Property = $Property.tolower() - #$Identity = $Identity.tolower() - - # Set our output path - # Single report file for all outputs user/tenant/etc. - # This might change in the future??? - $reportpath = Join-path $hawk.filepath report.xml - - # Switch statement to handle the state to color mapping - switch ($State) { - Warning { $highlighcolor = "#FF8000" } - Success { $highlighcolor = "Green" } - Error { $highlighcolor = "#8A0808" } - default { $highlighcolor = "Light Grey" } - } - - # Check if we have our XSL file in the output directory - $xslpath = Join-path $hawk.filepath Report.xsl - - if (Test-Path $xslpath ) { } - else { - # Copy the XSL file into the current output path - $sourcepath = join-path (split-path (Get-Module Hawk).path) report.xsl - if (test-path $sourcepath) { - Copy-Item -Path $sourcepath -Destination $hawk.filepath + <# + .SYNOPSIS + Adds the data to an XML report + .DESCRIPTION + Adds the data to an XML report + .PARAMETER Identity + User Identity for the report + .PARAMETER Property + xml property + .PARAMETER Value + Value of xml element + .PARAMETER Description + Description of element + .PARAMETER State + Color mapping + .PARAMETER Link + Element link + .EXAMPLE + Out-Report + Add the data to an XML report + .NOTES + General notes + #> + Param + ( + [Parameter(Mandatory = $true)] + [string]$Identity, + [Parameter(Mandatory = $true)] + [string]$Property, + [Parameter(Mandatory = $true)] + [string]$Value, + [string]$Description, + [string]$State, + [string]$Link + ) + + # Force the case on all our critical values + #$Property = $Property.tolower() + #$Identity = $Identity.tolower() + + # Set our output path + # Single report file for all outputs user/tenant/etc. + # This might change in the future??? + $reportpath = Join-path $hawk.filepath report.xml + + # Switch statement to handle the state to color mapping + switch ($State) { + Warning { $highlighcolor = "#FF8000" } + Success { $highlighcolor = "Green" } + Error { $highlighcolor = "#8A0808" } + default { $highlighcolor = "Light Grey" } } - # If we couldn't find it throw and error and stop + + # Check if we have our XSL file in the output directory + $xslpath = Join-path $hawk.filepath Report.xsl + + if (Test-Path $xslpath ) { } else { - Write-Error ("Unable to find transform file " + $sourcepath) -ErrorAction Stop + # Copy the XSL file into the current output path + $sourcepath = join-path (split-path (Get-Module Hawk).path) report.xsl + if (test-path $sourcepath) { + Copy-Item -Path $sourcepath -Destination $hawk.filepath + } + # If we couldn't find it throw and error and stop + else { + Write-Error ("Unable to find transform file " + $sourcepath) -ErrorAction Stop + } } - } - - # See if we have already created a report file - # If so we need to import it - if (Test-path $reportpath) { - $reportxml = $null - [xml]$reportxml = get-content $reportpath - } - # Since we have NOTHING we will create a new XML and just add / save / and exit - else { - Out-LogFile ("Creating new Report file" + $reportpath) - # Create the report xml object - $reportxml = New-Object xml - - # Create the xml declaraiton and stylesheet - $reportxml.AppendChild($reportxml.CreateXmlDeclaration("1.0", $null, $null)) | Out-Null - # $xmlstyle = "type=`"text/xsl`" href=`"https://csshawk.azurewebsites.net/report.xsl`"" - # $reportxml.AppendChild($reportxml.CreateProcessingInstruction("xml-stylesheet",$xmlstyle)) | Out-Null - - # Create all of the needed elements - $newreport = $reportxml.CreateElement("report") - $newentity = $reportxml.CreateElement("entity") - $newentityidentity = $reportxml.CreateElement("identity") - $newentityproperty = $reportxml.CreateElement("property") - $newentitypropertyname = $reportxml.CreateElement("name") - $newentitypropertyvalue = $reportxml.CreateElement("value") - $newentitypropertycolor = $reportxml.CreateElement("color") - $newentitypropertydescription = $reportxml.CreateElement("description") - $newentitypropertylink = $reportxml.CreateElement("link") - - ### Build the XML from the bottom up ### - # Add the property values to the entity object - $newentityproperty.AppendChild($newentitypropertyname) | Out-Null - $newentityproperty.AppendChild($newentitypropertyvalue) | Out-Null - $newentityproperty.AppendChild($newentitypropertycolor) | Out-Null - $newentityproperty.AppendChild($newentitypropertydescription) | Out-Null - $newentityproperty.AppendChild($newentitypropertylink) | Out-Null - - # Set the values for the leaf nodes we just added - $newentityproperty.name = $Property - $newentityproperty.value = $Value - $newentityproperty.color = $highlighcolor - $newentityproperty.description = $Description - $newentityproperty.link = $Link - - # Add the identity element to the entity and set its value - $newentity.AppendChild($newentityidentity) | Out-Null - $newentity.identity = $Identity - - # Add the property to the entity - $newentity.AppendChild($newentityproperty) | Out-Null - - # Add the entity to the report - $newreport.AppendChild($newentity) | Out-Null - - # Add the whole thing to the xml root - $reportxml.AppendChild($newreport) | Out-Null - - # save the xml - $reportxml.save($reportpath) - } - - # We need to check if an entity with the ID $identity already exists - if ($reportxml.report.entity.identity.contains($Identity)) { } - # Didn't find and entity so we are going to create the whole thing and once - else { - # Create all of the needed elements - $newentity = $reportxml.CreateElement("entity") - $newentityidentity = $reportxml.CreateElement("identity") - $newentityproperty = $reportxml.CreateElement("property") - $newentitypropertyname = $reportxml.CreateElement("name") - $newentitypropertyvalue = $reportxml.CreateElement("value") - $newentitypropertycolor = $reportxml.CreateElement("color") - $newentitypropertydescription = $reportxml.CreateElement("description") - $newentitypropertylink = $reportxml.CreateElement("link") - - ### Build the XML from the bottom up ### - # Add the property values to the entity object - $newentityproperty.AppendChild($newentitypropertyname) | Out-Null - $newentityproperty.AppendChild($newentitypropertyvalue) | Out-Null - $newentityproperty.AppendChild($newentitypropertycolor) | Out-Null - $newentityproperty.AppendChild($newentitypropertydescription) | Out-Null - $newentityproperty.AppendChild($newentitypropertylink) | Out-Null - - # Set the values for the leaf nodes we just added - $newentityproperty.name = $Property - $newentityproperty.value = $Value - $newentityproperty.color = $highlighcolor - $newentityproperty.description = $Description - $newentityproperty.link = $Link - - # Add them together and set values - $newentity.AppendChild($newentityidentity) | Out-Null - $newentity.identity = $Identity - $newentity.AppendChild($newentityproperty) | Out-Null - - # Add the new entity stub back to the XML - $reportxml.report.AppendChild($newentity) | Out-Null - } - - # Now we need to check for the property we are looking to add - # The property exists so we need to update it - if (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property.name.contains($Property)) { - ### Update existing property ### - (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).value = $Value - (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).color = $highlighcolor - (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).description = $Description - (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).link = $Link - } - # We need to add the property to the entity - else { - ### Add new property to existing Entity ### - # Create the elements that we are going to need - $newproperty = $reportxml.CreateElement("property") - $newname = $reportxml.CreateElement("name") - $newvalue = $reportxml.CreateElement("value") - $newcolor = $reportxml.CreateElement("color") - $newdescription = $reportxml.CreateElement("description") - $newlink = $reportxml.CreateElement("link") - - # Add on all of the elements - $newproperty.AppendChild($newname) | Out-Null - $newproperty.AppendChild($newvalue) | Out-Null - $newproperty.AppendChild($newcolor) | Out-Null - $newproperty.AppendChild($newdescription) | Out-Null - $newproperty.AppendChild($newlink) | Out-Null - - # Set the values - $newproperty.name = $Property - $newproperty.value = $Value - $newproperty.color = $highlighcolor - $newproperty.description = $Description - $newproperty.link = $Link - - # Add the newly created property to the entity - ($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).AppendChild($newproperty) | Out-Null - } - - # Make sure we save our changes - $reportxml.Save($reportpath) - - # Convert it to HTML and Save - Convert-ReportToHTML -Xml $reportpath -Xsl $xslpath -} \ No newline at end of file + + # See if we have already created a report file + # If so we need to import it + if (Test-path $reportpath) { + $reportxml = $null + [xml]$reportxml = get-content $reportpath + } + # Since we have NOTHING we will create a new XML and just add / save / and exit + else { + Out-LogFile ("Creating new Report file" + $reportpath) + # Create the report xml object + $reportxml = New-Object xml + + # Create the xml declaraiton and stylesheet + $reportxml.AppendChild($reportxml.CreateXmlDeclaration("1.0", $null, $null)) | Out-Null + # $xmlstyle = "type=`"text/xsl`" href=`"https://csshawk.azurewebsites.net/report.xsl`"" + # $reportxml.AppendChild($reportxml.CreateProcessingInstruction("xml-stylesheet",$xmlstyle)) | Out-Null + + # Create all of the needed elements + $newreport = $reportxml.CreateElement("report") + $newentity = $reportxml.CreateElement("entity") + $newentityidentity = $reportxml.CreateElement("identity") + $newentityproperty = $reportxml.CreateElement("property") + $newentitypropertyname = $reportxml.CreateElement("name") + $newentitypropertyvalue = $reportxml.CreateElement("value") + $newentitypropertycolor = $reportxml.CreateElement("color") + $newentitypropertydescription = $reportxml.CreateElement("description") + $newentitypropertylink = $reportxml.CreateElement("link") + + ### Build the XML from the bottom up ### + # Add the property values to the entity object + $newentityproperty.AppendChild($newentitypropertyname) | Out-Null + $newentityproperty.AppendChild($newentitypropertyvalue) | Out-Null + $newentityproperty.AppendChild($newentitypropertycolor) | Out-Null + $newentityproperty.AppendChild($newentitypropertydescription) | Out-Null + $newentityproperty.AppendChild($newentitypropertylink) | Out-Null + + # Set the values for the leaf nodes we just added + $newentityproperty.name = $Property + $newentityproperty.value = $Value + $newentityproperty.color = $highlighcolor + $newentityproperty.description = $Description + $newentityproperty.link = $Link + + # Add the identity element to the entity and set its value + $newentity.AppendChild($newentityidentity) | Out-Null + $newentity.identity = $Identity + + # Add the property to the entity + $newentity.AppendChild($newentityproperty) | Out-Null + + # Add the entity to the report + $newreport.AppendChild($newentity) | Out-Null + + # Add the whole thing to the xml root + $reportxml.AppendChild($newreport) | Out-Null + + # save the xml + $reportxml.save($reportpath) + } + + # We need to check if an entity with the ID $identity already exists + if ($reportxml.report.entity.identity.contains($Identity)) { } + # Didn't find and entity so we are going to create the whole thing and once + else { + # Create all of the needed elements + $newentity = $reportxml.CreateElement("entity") + $newentityidentity = $reportxml.CreateElement("identity") + $newentityproperty = $reportxml.CreateElement("property") + $newentitypropertyname = $reportxml.CreateElement("name") + $newentitypropertyvalue = $reportxml.CreateElement("value") + $newentitypropertycolor = $reportxml.CreateElement("color") + $newentitypropertydescription = $reportxml.CreateElement("description") + $newentitypropertylink = $reportxml.CreateElement("link") + + ### Build the XML from the bottom up ### + # Add the property values to the entity object + $newentityproperty.AppendChild($newentitypropertyname) | Out-Null + $newentityproperty.AppendChild($newentitypropertyvalue) | Out-Null + $newentityproperty.AppendChild($newentitypropertycolor) | Out-Null + $newentityproperty.AppendChild($newentitypropertydescription) | Out-Null + $newentityproperty.AppendChild($newentitypropertylink) | Out-Null + + # Set the values for the leaf nodes we just added + $newentityproperty.name = $Property + $newentityproperty.value = $Value + $newentityproperty.color = $highlighcolor + $newentityproperty.description = $Description + $newentityproperty.link = $Link + + # Add them together and set values + $newentity.AppendChild($newentityidentity) | Out-Null + $newentity.identity = $Identity + $newentity.AppendChild($newentityproperty) | Out-Null + + # Add the new entity stub back to the XML + $reportxml.report.AppendChild($newentity) | Out-Null + } + + # Now we need to check for the property we are looking to add + # The property exists so we need to update it + if (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property.name.contains($Property)) { + ### Update existing property ### + (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).value = $Value + (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).color = $highlighcolor + (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).description = $Description + (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).link = $Link + } + # We need to add the property to the entity + else { + ### Add new property to existing Entity ### + # Create the elements that we are going to need + $newproperty = $reportxml.CreateElement("property") + $newname = $reportxml.CreateElement("name") + $newvalue = $reportxml.CreateElement("value") + $newcolor = $reportxml.CreateElement("color") + $newdescription = $reportxml.CreateElement("description") + $newlink = $reportxml.CreateElement("link") + + # Add on all of the elements + $newproperty.AppendChild($newname) | Out-Null + $newproperty.AppendChild($newvalue) | Out-Null + $newproperty.AppendChild($newcolor) | Out-Null + $newproperty.AppendChild($newdescription) | Out-Null + $newproperty.AppendChild($newlink) | Out-Null + + # Set the values + $newproperty.name = $Property + $newproperty.value = $Value + $newproperty.color = $highlighcolor + $newproperty.description = $Description + $newproperty.link = $Link + + # Add the newly created property to the entity + ($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).AppendChild($newproperty) | Out-Null + } + + # Make sure we save our changes + $reportxml.Save($reportpath) + + # Convert it to HTML and Save + Convert-ReportToHTML -Xml $reportpath -Xsl $xslpath + } \ No newline at end of file diff --git a/Hawk/internal/functions/Read-HawkAppData.ps1 b/Hawk/internal/functions/Read-HawkAppData.ps1 index 08a7692..288626f 100644 --- a/Hawk/internal/functions/Read-HawkAppData.ps1 +++ b/Hawk/internal/functions/Read-HawkAppData.ps1 @@ -18,11 +18,11 @@ Function Read-HawkAppData { # check to see if our xml file is there if (test-path $HawkAppdataPath) { - Out-LogFile ("Reading file " + $HawkAppdataPath) + Out-LogFile ("Reading file " + $HawkAppdataPath) -Action $global:HawkAppData = ConvertFrom-Json -InputObject ([string](Get-Content $HawkAppdataPath)) } # if we don't have an xml file then do nothing else { - Out-LogFile ("No HawkAppData File found " + $HawkAppdataPath) + Out-LogFile ("No HawkAppData File found " + $HawkAppdataPath) -Information } } \ No newline at end of file diff --git a/Hawk/internal/functions/Test-AzureADConnection.ps1 b/Hawk/internal/functions/Test-AzureADConnection.ps1 deleted file mode 100644 index b17ea9f..0000000 --- a/Hawk/internal/functions/Test-AzureADConnection.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -<# -.SYNOPSIS - Test if we have a connection with the AzureAD Cmdlets -.DESCRIPTION - Test if we have a connection with the AzureAD Cmdlets -.EXAMPLE - PS C:\> - Explanation of what the example does -.INPUTS - Inputs (if any) -.OUTPUTS - Output (if any) -.NOTES - General notes -#> -Function Test-AzureADConnection { - - $TestModule = Get-Module AzureAD -ListAvailable -ErrorAction SilentlyContinue - $MinimumVersion = New-Object -TypeName Version -ArgumentList "2.0.2.140" - - if ($null -eq $TestModule) { - Out-LogFile "Please Install the AzureAD Module with the following command:" - Out-LogFile "Install-Module AzureAD" - break - } - # Since we are not null pull the highest version - else { - $TestModuleVersion = ($TestModule | Sort-Object -Property Version -Descending)[0].version - } - - # Test the version we need at least 2.0.2.140 - if ($TestModuleVersion -lt $MinimumVersion) { - Out-LogFile ("AzureAD Module Installed Version: " + $TestModuleVersion) - Out-LogFile ("Miniumum Required Version: " + $MinimumVersion) - Out-LogFile "Please update the module with: Update-Module AzureAD" - break - } - # Do nothing - else { } - - try { - $Null = Get-AzureADTenantDetail -ErrorAction Stop - } - catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException] { - #Out-LogFile "Please connect to AzureAD prior to running this cmdlet" - Out-LogFile "Connecting-AzureAD" - Connect-AzureAD - } -} \ No newline at end of file diff --git a/Hawk/internal/functions/Test-EXOConnection.ps1 b/Hawk/internal/functions/Test-EXOConnection.ps1 index 7a97624..2a2a0ad 100644 --- a/Hawk/internal/functions/Test-EXOConnection.ps1 +++ b/Hawk/internal/functions/Test-EXOConnection.ps1 @@ -20,8 +20,8 @@ Function Test-EXOConnection { } catch [System.Management.Automation.CommandNotFoundException] { # Connect to EXO if we couldn't find the command - Out-LogFile "Not Connected to Exchange Online" - Out-LogFile "Connecting to EXO using CloudConnect Module" + Out-LogFile "Not Connected to Exchange Online" -Information + Out-LogFile "Connecting to EXO using Exchange Online Module" -Action Connect-ExchangeOnline } } \ No newline at end of file diff --git a/Hawk/internal/functions/Test-GraphConnection.ps1 b/Hawk/internal/functions/Test-GraphConnection.ps1 index e54309d..2b377b3 100644 --- a/Hawk/internal/functions/Test-GraphConnection.ps1 +++ b/Hawk/internal/functions/Test-GraphConnection.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Test if we are connected to Graph and connect if not .DESCRIPTION @@ -15,19 +15,19 @@ #> Function Test-GraphConnection { - # Get tenant details to test that Connect-MgGraph has been called - try { $null = Get-MgOrganization -ErrorAction stop } + try { + $null = Get-MgOrganization -ErrorAction Stop + } catch { - # Write to the screen if we don't have a log file path yet - if ([string]::IsNullOrEmpty($Hawk.Logfile)) { + # Fallback if $Hawk is not initialized + if ($null -eq $Hawk) { Write-Output "Connecting to MGGraph using MGGraph Module" } - # Otherwise output to the log file else { - Out-LogFile "Connecting to MGGraph using MGGraph Module" + # $Hawk exists, so we can safely use Out-LogFile + Out-LogFile -String "Connecting to MGGraph using MGGraph Module" -Action } - # Connect to the MG Graph. The following scopes allow to retrieve Domain, Organization, and Sku data from the Graph. - Connect-MGGraph -Scopes "User.Read.All","Directory.Read.All" - Select-MgProfile -Name "v1.0" + + Connect-MGGraph } -}#End Function Test-GraphConnection \ No newline at end of file +} diff --git a/Hawk/internal/functions/Test-MSOLConnection.ps1 b/Hawk/internal/functions/Test-MSOLConnection.ps1 deleted file mode 100644 index 45445b9..0000000 --- a/Hawk/internal/functions/Test-MSOLConnection.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -<# -.SYNOPSIS - Test if we are connected to MSOL and connect if we are not -.DESCRIPTION - Test if we are connected to MSOL and connect if we are not -.EXAMPLE - PS C:\> - Explanation of what the example does -.INPUTS - Inputs (if any) -.OUTPUTS - Output (if any) -.NOTES - General notes -#> -Function Test-MSOLConnection { - try { $null = Get-MsolCompanyInformation -ErrorAction Stop } - catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] { - - # Write to the screen if we don't have a log file path yet - if ([string]::IsNullOrEmpty($Hawk.Logfile)) { - Write-Output "Connecting to MSOLService using MSOnline Module" - } - # Otherwise output to the log file - else { - Out-LogFile "Connecting to MSOLService using MSOnline Module" - } - - # Connect to the MSOl Service (This should have been installed with the CloudConnect Module) - Connect-MsolService - - } -} \ No newline at end of file diff --git a/Hawk/internal/functions/Test-MicrosoftIP.ps1 b/Hawk/internal/functions/Test-MicrosoftIP.ps1 index d447e09..f75ae2b 100644 --- a/Hawk/internal/functions/Test-MicrosoftIP.ps1 +++ b/Hawk/internal/functions/Test-MicrosoftIP.ps1 @@ -3,6 +3,7 @@ Determine if an IP listed in on the O365 XML list .DESCRIPTION Determine if an IP listed in on the O365 XML list + This function uses the System.Net.IPNetwork.dll to parse the IP Addresses. This is the only use for this DLL .PARAMETER IPtoTest IP that is being tested against the Microsoft IP List .PARAMETER Type @@ -28,29 +29,26 @@ Function Test-MicrosoftIP { # Check if we have imported all of our IP Addresses if ($null -eq $MSFTIPList) { - Out-Logfile "Building MSFTIPList" + Out-Logfile "Building MSFTIPList" -Action # Load our networking dll pulled from https://github.com/lduchosal/ipnetwork [string]$dll = join-path (Split-path (((get-module Hawk)[0]).path) -Parent) "\bin\System.Net.IPNetwork.dll" $Error.Clear() - Out-LogFile ("Loading Networking functions from " + $dll) + Out-LogFile ("Loading Networking functions from " + $dll) -Action [Reflection.Assembly]::LoadFile($dll) if ($Error.Count -gt 0) { - Out-Logfile "[WARNING] - DLL Failed to load can't process IPs" + Out-Logfile "DLL Failed to load can't process IPs" -isError Return "Unknown" } $Error.clear() - # Read in the XML file from the internet - # Out-LogFile ("Reading XML for MSFT IP Addresses https://support.content.office.net/en-us/static/O365IPAddresses.xml") - # [xml]$msftxml = (Invoke-webRequest -Uri https://support.content.office.net/en-us/static/O365IPAddresses.xml).content $MSFTJSON = (Invoke-WebRequest -uri ("https://endpoints.office.com/endpoints/Worldwide?ClientRequestId=" + (new-guid).ToString())).content | ConvertFrom-Json if ($Error.Count -gt 0) { - Out-Logfile "[WARNING] - Unable to retrieve JSON file" + Out-Logfile "Unable to retrieve JSON file" -isError Return "Unknown" } @@ -76,8 +74,8 @@ Function Test-MicrosoftIP { } } - Out-LogFile ("Found " + $ipv6.Count + " unique MSFT IPv6 address ranges") - Out-LogFile ("Found " + $ipv4.count + " unique MSFT IPv4 address ranges") + Out-LogFile ("Found " + $ipv6.Count + " unique MSFT IPv6 address ranges") -Information + Out-LogFile ("Found " + $ipv4.count + " unique MSFT IPv4 address ranges") -Information # New up using our networking dll we need to pull these all in as network objects foreach ($ip in $ipv6) { @@ -94,7 +92,7 @@ Function Test-MicrosoftIP { $output | Add-Member -MemberType NoteProperty -Value $ipv4objects -Name IPv4Objects # Create a global variable to hold our IP list so we can keep using it - Out-LogFile "Creating global variable `$MSFTIPList" + Out-LogFile "Creating global variable `$MSFTIPList" -Action New-Variable -Name MSFTIPList -Value $output -Scope global } @@ -135,4 +133,4 @@ Function Test-MicrosoftIP { # Return the value of test true = in MSFT network Return $test } -} +} \ No newline at end of file diff --git a/Hawk/internal/functions/Test-RecipientAge.ps1 b/Hawk/internal/functions/Test-RecipientAge.ps1 index 4b4b60d..76d836b 100644 --- a/Hawk/internal/functions/Test-RecipientAge.ps1 +++ b/Hawk/internal/functions/Test-RecipientAge.ps1 @@ -3,7 +3,8 @@ .SYNOPSIS Check to see if a recipient object was created since our start date .DESCRIPTION - Check to see if a recipient object was created since our start date + Check to see if a recipient object was created since our start date. + This will be used to determine if a new user has been created within the time frame specified. .PARAMETER RecipientID Recipient object ID that is being retrieved .EXAMPLE diff --git a/Hawk/internal/functions/Test-SuspiciousInboxRule.ps1 b/Hawk/internal/functions/Test-SuspiciousInboxRule.ps1 new file mode 100644 index 0000000..1f4e530 --- /dev/null +++ b/Hawk/internal/functions/Test-SuspiciousInboxRule.ps1 @@ -0,0 +1,78 @@ +Function Test-SuspiciousInboxRule { + <# + .SYNOPSIS + Internal helper function to detect suspicious inbox rule patterns. + + .DESCRIPTION + Analyzes inbox rule properties to identify potentially suspicious configurations + like external forwarding, message deletion, or targeting of security-related content. + Used by both rule creation and modification audit functions. + + .PARAMETER Rule + The parsed inbox rule object to analyze. + + .PARAMETER Reasons + [ref] array to store the reasons why a rule was flagged as suspicious. + + .OUTPUTS + Boolean indicating if the rule matches suspicious patterns. + Populates the Reasons array parameter with explanations if suspicious. + + .EXAMPLE + $reasons = @() + $isSuspicious = Test-SuspiciousInboxRule -Rule $ruleObject -Reasons ([ref]$reasons) + #> + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [object]$Rule, + + [Parameter(Mandatory = $true)] + [ref]$Reasons + ) + + $isSuspicious = $false + $suspiciousReasons = @() + + # Check forwarding/redirection configurations + if ($Rule.Param_ForwardTo) { + $isSuspicious = $true + $suspiciousReasons += "forwards to: $($Rule.Param_ForwardTo)" + } + if ($Rule.Param_ForwardAsAttachmentTo) { + $isSuspicious = $true + $suspiciousReasons += "forwards as attachment to: $($Rule.Param_ForwardAsAttachmentTo)" + } + if ($Rule.Param_RedirectTo) { + $isSuspicious = $true + $suspiciousReasons += "redirects to: $($Rule.Param_RedirectTo)" + } + + # Check deletion/move to deleted items + if ($Rule.Param_DeleteMessage) { + $isSuspicious = $true + $suspiciousReasons += "deletes messages" + } + if ($Rule.Param_MoveToFolder -eq 'Deleted Items') { + $isSuspicious = $true + $suspiciousReasons += "moves to Deleted Items" + } + + # Check for suspicious keywords in subject filters + if ($Rule.Param_SubjectContainsWords -match 'password|credentials|login|secure|security') { + $isSuspicious = $true + $suspiciousReasons += "suspicious subject filter: $($Rule.Param_SubjectContainsWords)" + } + + # Check for targeting of security-related senders + if ($Rule.Param_From -match 'security|admin|support|microsoft|helpdesk') { + $isSuspicious = $true + $suspiciousReasons += "targets security sender: $($Rule.Param_From)" + } + + # Update the reasons array with our findings + $Reasons.Value = $suspiciousReasons + + return $isSuspicious +} \ No newline at end of file diff --git a/Hawk/internal/functions/Test-UserObject.ps1 b/Hawk/internal/functions/Test-UserObject.ps1 index a4783b8..8f2bebc 100644 --- a/Hawk/internal/functions/Test-UserObject.ps1 +++ b/Hawk/internal/functions/Test-UserObject.ps1 @@ -32,8 +32,8 @@ Function Test-UserObject { Return $Output } else { - Out-LogFile "[ERROR] - Unable to determine if input is a UserPrincipalName" - Out-LogFile "Please provide a UPN or array of objects with propertly UserPrincipalName populated" + Out-LogFile "Unable to determine if input is a UserPrincipalName" -isError + Out-LogFile "Please provide a UPN or array of objects with propertly UserPrincipalName populated" -Information Write-Error "Unable to determine if input is a User Principal Name" -ErrorAction Stop } } @@ -43,8 +43,8 @@ Function Test-UserObject { Return $ToTest } else { - Out-LogFile "[ERROR] - Unable to determine if input is a UserPrincipalName" - Out-LogFile "Please provide a UPN or array of objects with propertly UserPrincipalName populated" + Out-LogFile "Unable to determine if input is a UserPrincipalName" -isError + Out-LogFile "Please provide a UPN or array of objects with propertly UserPrincipalName populated" -Information Write-Error "Unable to determine if input is a User Principal Name" -ErrorAction Stop } -} +} \ No newline at end of file diff --git a/Hawk/internal/functions/Write-HawkBanner.ps1 b/Hawk/internal/functions/Write-HawkBanner.ps1 new file mode 100644 index 0000000..a15c515 --- /dev/null +++ b/Hawk/internal/functions/Write-HawkBanner.ps1 @@ -0,0 +1,31 @@ +Function Write-HawkBanner { + <# + .SYNOPSIS + Displays the Hawk welcome banner. + .DESCRIPTION + Displays an ASCII art banner when starting Hawk operations. + The banner is sized to fit most terminal windows. + .EXAMPLE + Write-HawkBanner + Displays the Hawk welcome banner + #> + [CmdletBinding()] + param() + + $banner = @' +======================================== + __ __ __ + / / / /___ __ __/ /__ + / /_/ / __ `/ | /| / / //_/ + / __ / /_/ /| |/ |/ / ,< +/_/ /_/\__,_/ |__/|__/_/|_| + +======================================== + +Microsoft Cloud Security Analysis Tool +https://cloudforensicator.com + +'@ + + Write-Output $banner +} \ No newline at end of file diff --git a/Hawk/internal/scripts/license.ps1 b/Hawk/internal/scripts/license.ps1 index f4cc504..205a8d3 100644 --- a/Hawk/internal/scripts/license.ps1 +++ b/Hawk/internal/scripts/license.ps1 @@ -1,5 +1,5 @@ New-PSFLicense -Product 'Hawk' -Manufacturer 'Paul Navarro' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2020-11-25") -Text @" -Copyright (c) 2020 Paul Navarro +Copyright (c) 2023 Paul Navarro Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Hawk/internal/scripts/pre_commit_hook_scripts/Invoke-PowerShellScriptAnalyzer.ps1 b/Hawk/internal/scripts/pre_commit_hook_scripts/Invoke-PowerShellScriptAnalyzer.ps1 new file mode 100644 index 0000000..fd14d4a --- /dev/null +++ b/Hawk/internal/scripts/pre_commit_hook_scripts/Invoke-PowerShellScriptAnalyzer.ps1 @@ -0,0 +1,42 @@ +$ErrorActionPreference = 'Stop' + +# Import the module +Import-Module PSScriptAnalyzer + +$settings = Join-Path (Get-Location) 'Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1' + +# Define the list of files to exclude - including the analyzer script itself +$excludedFiles = @( + 'Invoke-PowerShellScriptAnalyzer.ps1', + 'pre_commit_hook_scripts/Invoke-PowerShellScriptAnalyzer.ps1', + 'internal/scripts/pre_commit_hook_scripts/Invoke-PowerShellScriptAnalyzer.ps1' +) + +$files = git diff --cached --name-only --diff-filter=AM | Where-Object { $_ -match '\.(ps1|psm1|psd1)$' } +$hasErrors = $false + +foreach ($file in $files) { + # Check if file is in excluded list using any variation of the path + $isExcluded = $false + foreach ($excludedFile in $excludedFiles) { + if ($file -match [regex]::Escape($excludedFile)) { + $isExcluded = $true + break + } + } + + if (-not $isExcluded) { + Write-Output "Analyzing $file..." + $results = Invoke-ScriptAnalyzer -Path $file -Settings $settings + if ($results) { + $results | Format-Table -AutoSize + $hasErrors = $true + } + } + else { + Write-Output "Skipping analysis for excluded file: $file" + } +} + +if ($hasErrors) { exit 1 } +exit 0 \ No newline at end of file diff --git a/Hawk/tests/general/FileIntegrity.Tests.ps1 b/Hawk/tests/general/FileIntegrity.Tests.ps1 index 89e6c9c..3446be9 100644 --- a/Hawk/tests/general/FileIntegrity.Tests.ps1 +++ b/Hawk/tests/general/FileIntegrity.Tests.ps1 @@ -1,73 +1,66 @@ -$moduleRoot = (Resolve-Path "$global:testroot\..").Path +$script:moduleRoot = (Resolve-Path "$PSScriptRoot\..").Path -. "$global:testroot\general\FileIntegrity.Exceptions.ps1" +. "$PSScriptRoot\FileIntegrity.Exceptions.ps1" Describe "Verifying integrity of module files" { BeforeAll { - function Get-FileEncoding - { - <# - .SYNOPSIS - Tests a file for encoding. - - .DESCRIPTION - Tests a file for encoding. - - .PARAMETER Path - The file to test - #> + function Get-FileEncoding { + <# + .SYNOPSIS + Tests a file for encoding. + .DESCRIPTION + Tests a file for encoding. + .PARAMETER Path + The file to test + .OUTPUTS + System.String + #> [CmdletBinding()] + [OutputType([string])] Param ( [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] [Alias('FullName')] [string] $Path ) - - if ($PSVersionTable.PSVersion.Major -lt 6) - { - [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path - } - else - { - [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path + + process { + if ($PSVersionTable.PSVersion.Major -lt 6) { + [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path + } + else { + [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path + } + + if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } + elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } + elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } + elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } + else { 'Unknown' } } - - if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } - elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } - elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } - elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } - else { 'Unknown' } } } Context "Validating PS1 Script files" { - $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" - - foreach ($file in $allFiles) - { - $name = $file.FullName.Replace("$moduleRoot\", '') - + $allFiles = Get-ChildItem -Path $script:moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$script:moduleRoot\tests\*" + + foreach ($file in $allFiles) { + $name = $file.FullName.Replace("$script:moduleRoot\", '') + It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } { Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' } - - It "[$name] Should have no trailing space" -TestCases @{ file = $file } { - ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty - } - + $tokens = $null $parseErrors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) - + [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) + It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { $parseErrors | Should -BeNullOrEmpty } - - foreach ($command in $global:BannedCommands) - { - if ($global:MayContainCommand["$command"] -notcontains $file.Name) - { + + foreach ($command in $script:BannedCommands) { + if ($script:MayContainCommand["$command"] -notcontains $file.Name) { It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } { $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty } @@ -75,21 +68,16 @@ Describe "Verifying integrity of module files" { } } } - + Context "Validating help.txt help files" { - $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" - - foreach ($file in $allFiles) - { - $name = $file.FullName.Replace("$moduleRoot\", '') - + $allFiles = Get-ChildItem -Path $script:moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$script:moduleRoot\tests\*" + + foreach ($file in $allFiles) { + $name = $file.FullName.Replace("$script:moduleRoot\", '') + It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } { Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' } - - It "[$name] Should have no trailing space" -TestCases @{ file = $file } { - ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0 - } } } } \ No newline at end of file diff --git a/Hawk/tests/general/Help.Tests.ps1 b/Hawk/tests/general/Help.Tests.ps1 index 28fc10d..43daf9a 100644 --- a/Hawk/tests/general/Help.Tests.ps1 +++ b/Hawk/tests/general/Help.Tests.ps1 @@ -10,7 +10,7 @@ .PARAMETER SkipTest Disables this test. - + .PARAMETER CommandPath List of paths under which the script files are stored. This test assumes that all functions have their own file that is named after themselves. @@ -34,13 +34,13 @@ Param ( [switch] $SkipTest, - + [string[]] $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"), - + [string] $ModuleName = "Hawk", - + [string] $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1" ) @@ -58,60 +58,60 @@ $commands = Get-Command -Module (Get-Module $ModuleName) -CommandType $commandTy foreach ($command in $commands) { $commandName = $command.Name - + # Skip all functions that are on the exclusions list if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } - + # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets $Help = Get-Help $commandName -ErrorAction SilentlyContinue - + Describe "Test help for $commandName" { - + # If help is not found, synopsis in auto-generated help is the syntax diagram It "should not be auto-generated" -TestCases @{ Help = $Help } { $Help.Synopsis | Should -Not -BeLike '*`[``]*' } - + # Should be a description for every function It "gets description for $commandName" -TestCases @{ Help = $Help } { $Help.Description | Should -Not -BeNullOrEmpty } - + # Should be at least one example It "gets example code from $commandName" -TestCases @{ Help = $Help } { ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty } - + # Should be at least one example description It "gets example help from $commandName" -TestCases @{ Help = $Help } { ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty } - + Context "Test parameter help for $commandName" { - + $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' - + $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common $parameterNames = $parameters.Name $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique foreach ($parameter in $parameters) { $parameterName = $parameter.Name $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName - + # Should be a description for every parameter It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } { $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty } - + $codeMandatory = $parameter.IsMandatory.toString() It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { $parameterHelp.Required | Should -Be $codeMandatory } - + if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } - + $codeType = $parameter.ParameterType.Name - + if ($parameter.ParameterType.IsEnum) { # Enumerations often have issues with the typename not being reliably available $names = $parameter.ParameterType::GetNames($parameter.ParameterType) diff --git a/Hawk/tests/general/PSScriptAnalyzer.Tests.ps1 b/Hawk/tests/general/PSScriptAnalyzer.Tests.ps1 deleted file mode 100644 index 4b228b7..0000000 --- a/Hawk/tests/general/PSScriptAnalyzer.Tests.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -[CmdletBinding()] -Param ( - [switch] - $SkipTest, - - [string[]] - $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions") -) - -if ($SkipTest) { return } - -$global:__pester_data.ScriptAnalyzer = New-Object System.Collections.ArrayList - -Describe 'Invoking PSScriptAnalyzer against commandbase' { - $commandFiles = Foreach ($file in $CommandPath) {Get-ChildItem -Path $file -Recurse | Where-Object Name -like "*.ps1"} - $scriptAnalyzerRules = Get-ScriptAnalyzerRule - - foreach ($file in $commandFiles) - { - Context "Analyzing $($file.BaseName)" { - $analysis = Invoke-ScriptAnalyzer -Path $file.FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess - - forEach ($rule in $scriptAnalyzerRules) - { - It "Should pass $rule" -TestCases @{ analysis = $analysis; rule = $rule } { - If ($analysis.RuleName -contains $rule) - { - $analysis | Where-Object RuleName -EQ $rule -outvariable failures | ForEach-Object { $null = $global:__pester_data.ScriptAnalyzer.Add($_) } - - 1 | Should -Be 0 - } - else - { - 0 | Should -Be 0 - } - } - } - } - } -} \ No newline at end of file diff --git a/Hawk/tests/general/Test-PreCommitHook.ps1 b/Hawk/tests/general/Test-PreCommitHook.ps1 new file mode 100644 index 0000000..942d4b0 --- /dev/null +++ b/Hawk/tests/general/Test-PreCommitHook.ps1 @@ -0,0 +1,110 @@ +# This file contains examples of both good and bad PowerShell code for testing PSScriptAnalyzer. +# To test the pre-commit hook or VS Code integration: +# 1. Uncomment the "Bad Code Examples" section +# 2. Try to commit the changes +# 3. Observe the PSScriptAnalyzer warnings/errors + +#region Good Code Examples - These will pass PSScriptAnalyzer +# testss +function Test-GoodFunction { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, + HelpMessage = "Enter a string parameter")] + [string]$Parameter + ) + + Write-Output $Parameter +} + +function Test-ValidatedFunction { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, + HelpMessage = "Enter a file path to process")] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + $items = Get-ChildItem -Path $Path + Write-Output $items +} +function Test-AdvancedFunction { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, + HelpMessage = "Enter a size in bytes")] + [ValidateRange(0, [int]::MaxValue)] + [int]$SizeInBytes + ) + + $result = [PSCustomObject]@{ + SizeInBytes = $SizeInBytes + SizeInKB = [math]::Round($SizeInBytes / 1KB, 2) + SizeInMB = [math]::Round($SizeInBytes / 1MB, 2) + } + + Write-Output $result +} + +#endregion + +#region Bad Code Examples - Uncomment to test PSScriptAnalyzer +# NOTE: The following code is intentionally written with issues to demonstrate PSScriptAnalyzer rules + +<# +# Bad function - Multiple issues +function test-badfunction { # Wrong capitalization + param([string]$param1) # Missing CmdletBinding + + $Global:badVariable = $param1 # Using global variable + + write-output $badVariable # Incorrect capitalization + +} # Trailing whitespace after this line + +# More issues +$unusedVariable = "test" # Variable declared but never used + +# Bad parameter validation +function Test-BadValidation { + param( + [string] + $Parameter # Missing mandatory and help message + ) + + Write-host $Parameter # Write-Host instead of Write-Output +} + +# Aliases and positional parameters +dir C:\ | where {$_.Length -gt 1000} # Using aliases instead of full cmdlet names +#> +#endregion + +#region Testing Instructions +<# +To test PSScriptAnalyzer integration: + +1. VS Code Testing: + - Uncomment the "Bad Code Examples" region + - Observe the squiggly lines indicating issues + - Hover over the lines to see the specific rule violations + +2. Pre-commit Hook Testing: + - Uncomment the "Bad Code Examples" region + - Stage the changes: git add Test-PreCommitHook.ps1 + - Try to commit: git commit -m "test: Testing PSScriptAnalyzer" + - The commit should fail with PSScriptAnalyzer warnings + +3. Remember to comment out the bad code when done testing! + +Common Issues Demonstrated: +- PSAvoidGlobalVars +- PSAvoidUsingCmdletAliases +- PSAvoidUsingWriteHost +- PSUseDeclaredVarsMoreThanAssignments +- PSUseConsistentWhitespace +- PSUseCmdletCorrectly +#> +#endregion + diff --git a/Hawk/tests/pester.ps1 b/Hawk/tests/pester.ps1 index 94f787b..4acf34b 100644 --- a/Hawk/tests/pester.ps1 +++ b/Hawk/tests/pester.ps1 @@ -1,14 +1,14 @@ param ( $TestGeneral = $true, - + $TestFunctions = $true, - + [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] [Alias('Show')] $Output = "None", - + $Include = "*", - + $Exclude = "" ) @@ -78,7 +78,7 @@ if ($TestFunctions) { if ($file.Name -notlike $Include) { continue } if ($file.Name -like $Exclude) { continue } - + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" $config.Run.Path = $file.FullName diff --git a/README.md b/README.md index 3eae3a9..80daca0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ # Hawk Documentation and "How to" videos + https://cloudforensicator.com/ + # Hawk + Github ## Who can contribute: -Everyone is welcome to contribute to this tool. The goal of the Hawk tool is to be a community lead tool and provides + +Everyone is welcome to contribute to this tool. The goal of the Hawk tool is to be a community lead tool and provides security support professionals with the tools they need to quickly and easily gather data from O365 and Azure. ## What Hawk is and isn't -Hawk provides Limited analysis of the gathered data. This is by design! + +Hawk provides Limited analysis of the gathered data. This is by design! Hawk is here to help get all of the data in a single place it is not designed to make any significant conclusions about this data. This is intentional since it is impossible for the tool to know enough about your environment or what you are concerned about to make a legitimate analysis of the data. @@ -15,16 +19,20 @@ your environment or what you are concerned about to make a legitimate analysis o Hawk's goal is to quickly get you the data that is needed to come to a conclusion; not to make the conclusion for you. We've structured the exported data in a manner of which can help analysts quickly triage known malicious Indicators Of Compromise (IOC) but again is NOT an all exhaustive list. + ## How can I contribute: + Please post any issues you find to the Issue section. Those issues will be incorporated into your future capability implementation. If something is critical or I seem to have not done anything in some time please feel free to send an email to the -Hawk support alias hawk_feedback@microsoft.com. +Hawk support alias hawkpsmodule@gmail.com. # HAWK + Powershell Based tool for gathering information related to O365 intrusions and potential Breaches ## PURPOSE: + The Hawk module has been designed to ease the burden on O365 administrators who are performing a forensic analysis in their organization. @@ -32,25 +40,27 @@ It does NOT take the place of a human reviewing the data generated and is simply data gathering easier. ## HOW TO USE: -Hawk is divided into two primary forms of cmdlets; *user* based Cmdlets and *tenant* based cmdlets. -User based cmdlets take the form Verb-HawkUser. They all expect a -user switch and -will retrieve information specific to the user that is specified. Tenant based cmdlets take -the form Verb-HawkTenant. They don't need any switches and will return information +Hawk is divided into two primary forms of cmdlets; _user_ based Cmdlets and _tenant_ based cmdlets. + +User based cmdlets take the form Verb-HawkUser. They all expect a -user switch and +will retrieve information specific to the user that is specified. Tenant based cmdlets take +the form Verb-HawkTenant. They don't need any switches and will return information about the whole tenant. A good starting place is the Start-HawkTenantInvestigation this will run all the tenant based -cmdlets and provide a collection of data to start with. Once this data has been reviewed +cmdlets and provide a collection of data to start with. Once this data has been reviewed if there are specific user(s) that more information should be gathered on Start-HawkUserInvestigation will gather all the User specific information for a single user. All Hawk cmdlets include help that provides an overview of the data they gather and a listing -of all possible output files. Run Get-Help -full to see the full help output for a +of all possible output files. Run Get-Help -full to see the full help output for a given Hawk cmdlet. -Some of the Hawk cmdlets will flag results that should be further reviewed. These will appear -in _Investigate files. These are NOT indicative of unwanted activity but are simply things +Some of the Hawk cmdlets will flag results that should be further reviewed. These will appear +in \_Investigate files. These are NOT indicative of unwanted activity but are simply things that should reviewed. ## Disclaimer -Hawk is NOT an official MICROSOFT tool. Therefore use of the tool is covered exclusively by the license associated with this github repository. \ No newline at end of file + +Hawk is NOT an official MICROSOFT tool. Therefore use of the tool is covered exclusively by the license associated with this github repository. diff --git a/Resolving IP Locations b/Resolving IP Locations new file mode 100644 index 0000000..46b134b --- /dev/null +++ b/Resolving IP Locations @@ -0,0 +1 @@ +ÿþ \ No newline at end of file diff --git a/build/vsts-prerequisites.ps1 b/build/vsts-prerequisites.ps1 index 2e132e9..c72baa2 100644 --- a/build/vsts-prerequisites.ps1 +++ b/build/vsts-prerequisites.ps1 @@ -1,11 +1,15 @@ -param ( +#vsts-prequisites.ps1 + +param ( [string] $Repository = 'PSGallery' ) -$modules = @("Pester", "PSFramework", "PSModuleDevelopment", "PSScriptAnalyzer") +$modules = @("Pester", "PSFramework", "PSModuleDevelopment") # Automatically add missing dependencies +# TODO: uncomment this block of code below and fix RobustCloudCommand error. + $data = Import-PowerShellDataFile -Path "$PSScriptRoot\..\Hawk\Hawk.psd1" foreach ($dependency in $data.RequiredModules) { if ($dependency -is [string]) { @@ -19,7 +23,8 @@ foreach ($dependency in $data.RequiredModules) { } foreach ($module in $modules) { - Write-Host "Installing $module" -ForegroundColor Cyan + # Write-Output "Installing module: $module" + Write-Output "Installing $module" Install-Module $module -Force -SkipPublisherCheck -Repository $Repository Import-Module $module -Force -PassThru } \ No newline at end of file