diff --git a/Hawk/changelog.md b/Hawk/changelog.md index c8b6386..2f2aee0 100644 --- a/Hawk/changelog.md +++ b/Hawk/changelog.md @@ -92,3 +92,7 @@ ## 3.X.X (2025-X-XX) - Implemented UTC timestamps to avoid using local timestamps +- Implemented PROMPT tag to display to screen when prompting user +- Added functionality to expand detect M365 license type to determine max log retention time +- Added ability to expand search up to 365 days + diff --git a/Hawk/functions/General/Update-HawkModule.ps1 b/Hawk/functions/General/Update-HawkModule.ps1 index 2d8e22f..93a569c 100644 --- a/Hawk/functions/General/Update-HawkModule.ps1 +++ b/Hawk/functions/General/Update-HawkModule.ps1 @@ -1,109 +1,122 @@ Function Update-HawkModule { -<# -.SYNOPSIS - Hawk upgrade check -.DESCRIPTION - Hawk upgrade check -.PARAMETER ElevatedUpdate - Update Module -.EXAMPLE - Update-HawkModule - Checks for update to Hawk Module on PowerShell Gallery -.NOTES - General notes -#> - param - ( - [switch]$ElevatedUpdate - ) + <# + .SYNOPSIS + Hawk upgrade check. + + .DESCRIPTION + Hawk upgrade check. Checks the PowerShell Gallery for newer versions of the Hawk module and handles the update process, including elevation of privileges if needed. + + .PARAMETER ElevatedUpdate + Switch parameter indicating the function is running in an elevated context. + + .PARAMETER WhatIf + Shows what would happen if the command runs. The command is not run. + + .PARAMETER Confirm + Prompts you for confirmation before running the command. + + .EXAMPLE + Update-HawkModule + + Checks for update to Hawk Module on PowerShell Gallery. + + .NOTES + Requires elevation to Administrator rights to perform the update. + #> + [CmdletBinding(SupportsShouldProcess)] + param + ( + [switch]$ElevatedUpdate + ) + + # If ElevatedUpdate is true then we are running from a forced elevation and we just need to run without prompting + if ($ElevatedUpdate) { + # Set upgrade to true + $Upgrade = $true + } + else { + + # See if we can do an upgrade check + if ($null -eq (Get-Command Find-Module)) { } + + # If we can then look for an updated version of the module + else { + Out-LogFile "Checking for latest version online" -Action + $onlineversion = Find-Module -name Hawk -erroraction silentlycontinue + $Localversion = (Get-Module Hawk | Sort-Object -Property Version -Descending)[0] + Out-LogFile ("Found Version " + $onlineversion.version + " Online") -Information - # If ElevatedUpdate is true then we are running from a forced elevation and we just need to run without prompting - if ($ElevatedUpdate) { - # Set upgrade to true - $Upgrade = $true - } - else { - - # See if we can do an upgrade check - if ($null -eq (Get-Command Find-Module)) { } - - # If we can then look for an updated version of the module - else { - Write-Output "Checking for latest version online" - $onlineversion = Find-Module -name Hawk -erroraction silentlycontinue - $Localversion = (Get-Module Hawk | Sort-Object -Property Version -Descending)[0] - Write-Output ("Found Version " + $onlineversion.version + " Online") - - if ($null -eq $onlineversion){ - Write-Output "[ERROR] - Unable to check Hawk version in Gallery" - } - elseif (([version]$onlineversion.version) -gt ([version]$localversion.version)) { - Write-Output "New version of Hawk module found online" - Write-Output ("Local Version: " + $localversion.version + " Online Version: " + $onlineversion.version) - - # Prompt the user to upgrade or not - $title = "Upgrade version" - $message = "A Newer version of the Hawk Module has been found Online. `nUpgrade to latest version?" - $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Stops the function and provides directions for upgrading." - $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Continues running current function" - $options = [System.Management.Automation.Host.ChoiceDescription[]]($Yes, $No) - $result = $host.ui.PromptForChoice($title, $message, $options, 0) - - # Check to see what the user choose - switch ($result) { - 0 { $Upgrade = $true; Send-AIEvent -Event Upgrade -Properties @{"Upgrade" = "True" } - } - 1 { $Upgrade = $false; Send-AIEvent -Event Upgrade -Properties @{"Upgrade" = "False" } - } - } - } - # If the versions match then we don't need to upgrade - else { - Write-Output "Latest Version Installed" - } - } - } - - # If we determined that we want to do an upgrade make the needed checks and do it - if ($Upgrade) { - # Determine if we have an elevated powershell prompt - If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - # Update the module - Write-Output "Downloading Updated Hawk Module" - Update-Module Hawk -Force - Write-Output "Update Finished" - Start-Sleep 3 - - # If Elevated update then this prompt was created by the Update-HawkModule function and we can close it out otherwise leave it up - if ($ElevatedUpdate) { exit } - - # If we didn't elevate then we are running in the admin prompt and we need to import the new hawk module - else { - Write-Output "Starting new PowerShell Window with the updated Hawk Module loaded" - - # We can't load a new copy of the same module from inside the module so we have to start a new window - Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk -force" -Verb RunAs - Write-Warning "Updated Hawk Module loaded in New PowerShell Window. `nPlease Close this Window." - break - } - - } - # If we are not running as admin we need to start an admin prompt - else { - # Relaunch as an elevated process: - Write-Output "Starting Elevated Prompt" - Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk;Update-HawkModule -ElevatedUpdate" -Verb RunAs -Wait - - Write-Output "Starting new PowerShell Window with the updated Hawk Module loaded" - - # We can't load a new copy of the same module from inside the module so we have to start a new window - Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk -force" - Write-Warning "Updated Hawk Module loaded in New PowerShell Window. `nPlease Close this Window." - break - } - } - # Since upgrade is false we log and continue - else { - Write-Output "Skipping Upgrade" - } -} \ No newline at end of file + if ($null -eq $onlineversion){ + Out-LogFile "[ERROR] - Unable to check Hawk version in Gallery" -isError + } + elseif (([version]$onlineversion.version) -gt ([version]$localversion.version)) { + Out-LogFile "New version of Hawk module found online" -Information + Out-LogFile ("Local Version: " + $localversion.version + " Online Version: " + $onlineversion.version) -Information + + # Prompt the user to upgrade or not + $title = "Upgrade version" + $message = "A Newer version of the Hawk Module has been found Online. `nUpgrade to latest version?" + $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Stops the function and provides directions for upgrading." + $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Continues running current function" + $options = [System.Management.Automation.Host.ChoiceDescription[]]($Yes, $No) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + # Check to see what the user choose + switch ($result) { + 0 { $Upgrade = $true; Send-AIEvent -Event Upgrade -Properties @{"Upgrade" = "True" } + } + 1 { $Upgrade = $false; Send-AIEvent -Event Upgrade -Properties @{"Upgrade" = "False" } + } + } + } + # If the versions match then we don't need to upgrade + else { + Out-LogFile "Latest Version Installed" -Information + } + } + } + + # If we determined that we want to do an upgrade make the needed checks and do it + if ($Upgrade) { + # Determine if we have an elevated powershell prompt + If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + # Update the module + if ($PSCmdlet.ShouldProcess("Hawk Module", "Update module")) { + Out-LogFile "Downloading Updated Hawk Module" -Action + Update-Module Hawk -Force + Out-LogFile "Update Finished" -Action + Start-Sleep 3 + + # If Elevated update then this prompt was created by the Update-HawkModule function and we can close it out otherwise leave it up + if ($ElevatedUpdate) { exit } + + # If we didn't elevate then we are running in the admin prompt and we need to import the new hawk module + else { + Out-LogFile "Starting new PowerShell Window with the updated Hawk Module loaded" -Action + + # We can't load a new copy of the same module from inside the module so we have to start a new window + Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk -force" -Verb RunAs + Out-LogFile "Updated Hawk Module loaded in New PowerShell Window. Please Close this Window." -Notice + break + } + } + } + # If we are not running as admin we need to start an admin prompt + else { + # Relaunch as an elevated process: + Out-LogFile "Starting Elevated Prompt" -Action + Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk;Update-HawkModule -ElevatedUpdate" -Verb RunAs -Wait + + Out-LogFile "Starting new PowerShell Window with the updated Hawk Module loaded" -Action + + # We can't load a new copy of the same module from inside the module so we have to start a new window + Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk -force" + Out-LogFile "Updated Hawk Module loaded in New PowerShell Window. Please Close this Window." -Notice + break + } + } + # Since upgrade is false we log and continue + else { + Out-LogFile "Skipping Upgrade" -Action + } + } \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 index 097a8c1..bea58fd 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 @@ -37,6 +37,11 @@ Function Get-HawkTenantAdminEmailForwardingChange { [CmdletBinding()] param() + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + # 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. @@ -199,8 +204,9 @@ Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations @( } 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 + Out-LogFile "Get-HawkTenantAdminEmailForwardingChange completed successfully" -Information + Out-LogFile "No forwarding changes found in filtered results" -action + Out-LogFile "Retrieved $($AllMailboxChanges.Count) total operations, but none involved forwarding changes" -action } } catch { diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 index 8824351..fdae3d0 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 @@ -42,6 +42,11 @@ Function Get-HawkTenantAdminInboxRuleCreation { [CmdletBinding()] param() + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -85,8 +90,8 @@ Function Get-HawkTenantAdminInboxRuleCreation { 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 + Out-LogFile "Found suspicious rule creation: '$($rule.Param_Name)'" -Notice + } } } @@ -96,7 +101,8 @@ Function Get-HawkTenantAdminInboxRuleCreation { } } else { - Out-LogFile "No admin inbox rule creation events found in audit logs" -Information + Out-LogFile "Get-HawkTenantAdminInboxRuleCreation completed successfully" -Information + Out-LogFile "No admin inbox rule creation events found in audit logs" -Action } } catch { diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 index ccb1829..84dbe6c 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 @@ -45,6 +45,12 @@ Function Get-HawkTenantAdminInboxRuleModification { [CmdletBinding()] param() + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -80,16 +86,16 @@ Function Get-HawkTenantAdminInboxRuleModification { 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 + # Just enhance the log message but keep all data processing the same + Out-LogFile "Found suspicious rule modification: Operation=$($rule.Operation) User=$($rule.UserId)" -Notice } } } @@ -99,7 +105,8 @@ Function Get-HawkTenantAdminInboxRuleModification { } } else { - Out-LogFile "No inbox rule modifications found in audit logs" -Information + Out-LogFile "Get-HawkTenantAdminInboxRuleModification completed successfully" -Information + Out-LogFile "No inbox rule modifications found in audit logs" -action } } catch { diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 index 2bfbc4d..29785fd 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 @@ -38,6 +38,11 @@ Function Get-HawkTenantAdminInboxRuleRemoval { [CmdletBinding()] param() + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -88,8 +93,8 @@ Function Get-HawkTenantAdminInboxRuleRemoval { 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 + Out-LogFile "Found suspicious rule removal: '$($rule.Param_Name)'" -Notice + } } } @@ -99,7 +104,8 @@ Function Get-HawkTenantAdminInboxRuleRemoval { } } else { - Out-LogFile "No inbox rule removals found in audit logs" -Information + Out-LogFile "Get-HawkTenantAdminInboxRuleRemoval completed successfully" -Information + Out-LogFile "No inbox rule removals found in audit logs" -action } } catch { diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 index 2de5c15..b3b4134 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 @@ -29,6 +29,12 @@ Function Get-HawkTenantAdminMailboxPermissionChange { [CmdletBinding()] param() + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -98,7 +104,8 @@ Function Get-HawkTenantAdminMailboxPermissionChange { } } else { - Out-LogFile "No mailbox permission changes found in audit logs" -Information + Out-LogFile "Get-HawkTenantAdminMailBoxPermissionChange completed successfully" -Information + Out-LogFile "No mailbox permission changes found in audit logs" -action } } catch { diff --git a/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 index bf29009..6c0350c 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 @@ -23,7 +23,8 @@ param() BEGIN { - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { Initialize-HawkGlobalObject } diff --git a/Hawk/functions/Tenant/Get-HawkTenantAuditLog.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAuditLog.ps1 index 11273cf..4569c66 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAuditLog.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAuditLog.ps1 @@ -22,10 +22,12 @@ https://docs.microsoft.com/en-us/graph/api/resources/auditlog?view=graph-rest-1. #> BEGIN{ - #Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { Initialize-HawkGlobalObject } + Out-LogFile "Gathering Azure AD Audit Logs events" -Action } PROCESS{ diff --git a/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 index 9d195fd..28a4428 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 @@ -27,15 +27,12 @@ [int]$IntervalMinutes = 15 ) - # # Try to convert the submitted date into [datetime] format - # try { - # [datetime]$DateToStartSearch = Get-Date $StartDate - # } - # catch { - # Out-Logfile "[ERROR] - Unable to convert submitted date" - # break - # } + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + # Make sure the start date isn't more than 90 days in the past if ((Get-Date).adddays(-91) -gt $StartDate) { Out-Logfile "Start date is over 90 days in the past" -isError @@ -64,7 +61,8 @@ # 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) -Information + Out-LogFile "Get-HawkTenantAuthHistory completed successfully" -Information + Out-LogFile ("No results found for time period " + $CurrentStart + " - " + $CurrentEnd) -action } else { $output | Out-MultipleFileType -FilePrefix "Audit_Log_Full_$prefix" -Append -csv -json diff --git a/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 index c68d08a..d966844 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 @@ -21,9 +21,13 @@ #> Begin { #Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { - Initialize-HawkGlobalObject - } + # Check if Hawk object exists and is fully initialized + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Out-LogFile "Gathering Tenant information" -Action Test-EXOConnection }#End BEGIN @@ -41,7 +45,8 @@ $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." -Information + Out-LogFile "Get-HawkTenantAzureAppAuditLog completed successfully" -Information + Out-LogFile "No Application related events found in the search time frame." -Action } # If not null then we must have found some events so flag them @@ -67,6 +72,6 @@ else { } }#End PROCESS END{ -Out-LogFile "Completed gathering Tenant App Audit Logs" -Action +Out-LogFile "Completed gathering Tenant App Audit Logs" -Information }#End END } diff --git a/Hawk/functions/Tenant/Get-HawkTenantConfiguration.ps1 b/Hawk/functions/Tenant/Get-HawkTenantConfiguration.ps1 index a312581..a73fd91 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantConfiguration.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantConfiguration.ps1 @@ -58,6 +58,13 @@ .NOTES TODO: Put in some analysis ... flag some key things that we know we should #> + + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" diff --git a/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 b/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 index 0702b41..383162b 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 @@ -25,6 +25,11 @@ [CmdletBinding()] param() + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Out-LogFile "Gathering OAuth / Application Grants" -Action Test-GraphConnection diff --git a/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 b/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 index e512b61..ea78c43 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 @@ -27,6 +27,13 @@ Function Get-HawkTenantDomainActivity { Searches for all Domain configuration actions #> BEGIN{ + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + + Test-EXOConnection Send-AIEvent -Event "CmdRun" Out-LogFile "Gathering any changes to Domain configuration settings" -action @@ -36,7 +43,8 @@ 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." -Information + Out-LogFile "Get-HawkTenantDomainActivity completed successfully" -Information + Out-LogFile "No Domain configuration changes found." -Action } # If not null then we must have found some events so flag them else{ diff --git a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 index 3cd9472..9277823 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 @@ -59,10 +59,12 @@ #TO DO: UPDATE THIS FUNCTION TO FIND E-Discovery roles created via the graph API BEGIN { - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { Initialize-HawkGlobalObject } + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -116,11 +118,13 @@ $RoleAssignements | Export-Csv -Path (Join-Path -Path $TenantPath -ChildPath "CustomEDiscoveryRoles.csv") -NoTypeInformation } else { - Out-LogFile "No role assignments found" -Information + Out-LogFile "Get-HawkTenantEDiscoveryConfiguration completed successfully" -Information + Out-LogFile "No role assignments found" -action } } else { - Out-LogFile "No roles with eDiscovery cmdlets found" -Information + Out-LogFile "Get-HawkTenantEDiscoveryConfiguration completed successfully" -Information + Out-LogFile "No roles with eDiscovery cmdlets found" -action } #endregion diff --git a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 index fb601e8..5c015bf 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 @@ -41,6 +41,13 @@ - Cmdlet: Command that was executed (if applicable) #> # Search UAL audit logs for any Domain configuration changes + + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -50,7 +57,8 @@ $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 + Out-LogFile "Get-HawkTenantEDiscoveryLog completed successfully" -Information + Out-LogFile "No eDiscovery Logs found" -Action } # If not null then we must have found some events so flag them diff --git a/Hawk/functions/Tenant/Get-HawkTenantEXOAdmin.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEXOAdmin.ps1 index 6cd04ca..770a59a 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEXOAdmin.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEXOAdmin.ps1 @@ -14,6 +14,11 @@ .NOTES #> BEGIN{ + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Out-LogFile "Gathering Exchange Online Administrators" -Action Test-EXOConnection diff --git a/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 index b1f32a8..502de1c 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 @@ -20,10 +20,11 @@ param() BEGIN { - # Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { Initialize-HawkGlobalObject } + Out-LogFile "Gathering Microsoft Entra ID Administrators" -Action # Verify Graph API connection @@ -82,7 +83,8 @@ Out-LogFile "Successfully exported Microsoft Entra ID Administrators data" -Information } else { - Out-LogFile "No administrator roles found or accessible" -Information + Out-LogFile "Get-HawkTenantEntraID completed" -Information + Out-LogFile "No administrator roles found or accessible" -Action } } catch { diff --git a/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 index 23694c9..576bcf0 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 @@ -20,10 +20,11 @@ Properties selected for DFIR relevance. #> BEGIN { - # Initialize the Hawk environment if not already done - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { Initialize-HawkGlobalObject } + Out-LogFile "Gathering Entra ID Users" -Action # Ensure we have a valid Graph connection @@ -56,7 +57,8 @@ Out-MultipleFileType -FilePrefix "EntraIDUsers" -csv -json } else { - Out-LogFile "No users found" -Information + Out-LogFile "Get-HawkTenantEntraIDUser completed successfully" -Information + Out-LogFile "No users found" -Action } } END { diff --git a/Hawk/functions/Tenant/Get-HawkTenantInboxRule.ps1 b/Hawk/functions/Tenant/Get-HawkTenantInboxRule.ps1 index b9ff844..0a85c42 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantInboxRule.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantInboxRule.ps1 @@ -55,6 +55,11 @@ [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Test-EXOConnection Send-AIEvent -Event "CmdRun" diff --git a/Hawk/functions/Tenant/Get-HawkTenantMailItemsAccessed.ps1 b/Hawk/functions/Tenant/Get-HawkTenantMailItemsAccessed.ps1 index 4ce4025..bf42c3e 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantMailItemsAccessed.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantMailItemsAccessed.ps1 @@ -33,6 +33,11 @@ ) BEGIN { + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Out-LogFile "Starting Unified Audit Log (UAL) search for 'MailItemsAccessed'" -Action }#End Begin diff --git a/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 index 5402280..c3f3c4d 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 @@ -37,6 +37,11 @@ #> [CmdletBinding()] param() + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + # Verify EXO connection and send telemetry Test-EXOConnection @@ -100,7 +105,8 @@ } } else { - Out-LogFile "No RBAC changes found." -Information + Out-LogFile "Get-HawkTenantRbacChange completed successfully" -Information + Out-LogFile "No RBAC changes found." -action } } catch { diff --git a/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 b/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 index 7625933..1b68a71 100644 --- a/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 +++ b/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 @@ -44,6 +44,12 @@ [string]$IpAddress ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -63,7 +69,8 @@ # If we didn't get anything back log it if ($null -eq $ipevents) { - Out-LogFile ("No IP logon events found for IP " + $IpAddress) -Information + Out-LogFile "Get-HawkTenantActivityByIP completed successfully" -Information + Out-LogFile ("No IP logon events found for IP " + $IpAddress) -action } # If we did then process it diff --git a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 index 4b9bdbe..487bda2 100644 --- a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 +++ b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 @@ -33,11 +33,10 @@ [CmdletBinding(SupportsShouldProcess)] param() - Write-HawkBanner - - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { - Initialize-HawkGlobalObject - } + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } Out-LogFile "Starting Tenant Sweep" -action Send-AIEvent -Event "CmdRun" diff --git a/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 b/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 index 401f595..2b9576d 100644 --- a/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 +++ b/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 @@ -35,6 +35,11 @@ [Parameter(Mandatory = $true)] [array]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -81,7 +86,8 @@ $UserChanges | Out-MultipleFileType -FilePrefix "User_Changes" -csv -json -User $User } else { - Out-LogFile "No User Changes found." -Information + Out-LogFile "Get-HawkUserAdminAudit completed successfully" -Information + Out-LogFile "No User Changes found." -action } } catch { diff --git a/Hawk/functions/User/Get-HawkUserAuthHistory.ps1 b/Hawk/functions/User/Get-HawkUserAuthHistory.ps1 index 8996f10..738897e 100644 --- a/Hawk/functions/User/Get-HawkUserAuthHistory.ps1 +++ b/Hawk/functions/User/Get-HawkUserAuthHistory.ps1 @@ -38,6 +38,12 @@ [switch]$ResolveIPLocations ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" diff --git a/Hawk/functions/User/Get-HawkUserAutoReply.ps1 b/Hawk/functions/User/Get-HawkUserAutoReply.ps1 index be81c5d..e1cd318 100644 --- a/Hawk/functions/User/Get-HawkUserAutoReply.ps1 +++ b/Hawk/functions/User/Get-HawkUserAutoReply.ps1 @@ -30,6 +30,11 @@ ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Test-EXOConnection Send-AIEvent -Event "CmdRun" diff --git a/Hawk/functions/User/Get-HawkUserConfiguration.ps1 b/Hawk/functions/User/Get-HawkUserConfiguration.ps1 index f7df349..2302de4 100644 --- a/Hawk/functions/User/Get-HawkUserConfiguration.ps1 +++ b/Hawk/functions/User/Get-HawkUserConfiguration.ps1 @@ -43,6 +43,10 @@ [Parameter(Mandatory = $true)] [array]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } Test-EXOConnection Send-AIEvent -Event "CmdRun" diff --git a/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 b/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 index ef2320a..ffe2d0c 100644 --- a/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 +++ b/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 @@ -38,6 +38,12 @@ [array]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -55,7 +61,8 @@ # 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" -Information + Out-LogFile "Get-HawkUserEmailForwarding completed successfully" -Information + Out-LogFile "No forwarding configuration found" -action } # 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 11952fc..c184a7f 100644 --- a/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 +++ b/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 @@ -46,6 +46,12 @@ [System.Management.Automation.PSCredential]$EWSCredential ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -61,7 +67,7 @@ # Determine if the email address is null or empty [string]$EmailAddress = (Get-EXOMailbox $user).PrimarySmtpAddress if ([string]::IsNullOrEmpty($EmailAddress)) { - Write-Warning "No SMTP Address found. Skipping." + Out-LogFile "No SMTP Address found. Skipping." -isWarning return $null } @@ -131,7 +137,8 @@ # Log if no hidden rules are found if ($FoundHidden -eq $false) { - Out-LogFile ("No Hidden rules found for mailbox: " + $EmailAddress) -Information + Out-LogFile "Get-HawkUserHiddenRule completed successfully" -Information + Out-LogFile ("No Hidden rules found for mailbox: " + $EmailAddress) -action } } } diff --git a/Hawk/functions/User/Get-HawkUserInboxRule.ps1 b/Hawk/functions/User/Get-HawkUserInboxRule.ps1 index 2509230..acf94d2 100644 --- a/Hawk/functions/User/Get-HawkUserInboxRule.ps1 +++ b/Hawk/functions/User/Get-HawkUserInboxRule.ps1 @@ -40,6 +40,12 @@ Function Get-HawkUserInboxRule { ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -54,7 +60,10 @@ 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" -Information } + if ($null -eq $InboxRules) { + Out-LogFile "Get-HawkUserInboxRule completed successfully" -Information + Out-LogFile "No Inbox Rules found" -action + } else { # If the rules contains one of a number of known suspecious properties flag them foreach ($Rule in $InboxRules) { diff --git a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 index e56d7f9..4af71af 100644 --- a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 +++ b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 @@ -77,6 +77,11 @@ [array]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -118,16 +123,19 @@ $itemLogs | Out-MultipleFileType -FilePrefix "ExchangeItem_Logs" -csv -json -User $User } else { - Out-LogFile "No ExchangeItem events found." -Information + Out-LogFile "ExchangeItem event search completed successfully" -Information + Out-LogFile "No ExchangeItem events found." -Action } # Process ExchangeItemGroup records Out-LogFile "Searching Unified Audit Log for ExchangeItemGroup events." -action + Out-LogFile "Please be patient, this can take a while..." -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 + Out-LogFile "Processing all ExchangeItemGroup events, this can take a while..." -action # Process and output flattened data $ParsedGroupLogs = $groupLogs | Get-SimpleUnifiedAuditLog @@ -139,7 +147,8 @@ $groupLogs | Out-MultipleFileType -FilePrefix "ExchangeItemGroup_Logs" -csv -json -User $User } else { - Out-LogFile "No ExchangeItemGroup events found." -Information + Out-LogFile "ExchangeItemGroup search completed successfully" -Information + Out-LogFile "No ExchangeItemGroup events found." -action } # Summary logging diff --git a/Hawk/functions/User/Get-HawkUserMessageTrace.ps1 b/Hawk/functions/User/Get-HawkUserMessageTrace.ps1 index feea71b..10b6a35 100644 --- a/Hawk/functions/User/Get-HawkUserMessageTrace.ps1 +++ b/Hawk/functions/User/Get-HawkUserMessageTrace.ps1 @@ -27,6 +27,11 @@ Single UPN of a user, commans seperated list of UPNs, or array of objects that c [array]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Test-EXOConnection Send-AIEvent -Event "CmdRun" diff --git a/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 b/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 index 20ef3b9..b16a1f5 100644 --- a/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 +++ b/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 @@ -31,6 +31,11 @@ [array]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + Test-EXOConnection Send-AIEvent -Event "CmdRun" @@ -48,7 +53,8 @@ [array]$MobileDevices = Get-MobileDevice -mailbox $User if ($Null -eq $MobileDevices) { - Out-Logfile ("No devices found for user: " + $User) -Information + Out-LogFile "Get-HawkUserMobileDevice completed successfully" -Information + Out-Logfile ("No devices found for user: " + $User) -action } else { Out-Logfile ("Found " + $MobileDevices.count + " Devices") -Information diff --git a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 index fb465c2..6a33a80 100644 --- a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 +++ b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 @@ -1,92 +1,100 @@ 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 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 - - # Verify our UPN input - PROCESS {[array]$UserArray = Test-UserObject -ToTest $EmailAddress - $headers=@{'hibp-api-key' = $hibpkey} - - foreach ($Object in $UserArray) { - - [string]$User = $Object.UserPrincipalName - - # Convert the email to URL encoding - $uriEncodeEmail = [uri]::EscapeDataString($($user)) - - # 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 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 - - - } - }#End of PROCESS block - END { - Start-Sleep -Milliseconds 1500 - }#End of END block + <# + .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 + ) + + BEGIN { + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } + + 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 + Out-LogFile "haveibeenpwned.com apikey" -isPrompt -NoNewLine + $hibpkey = (Read-Host).Trim() + } + }#End of BEGIN block + + # Verify our UPN input + PROCESS { + # Used to silence PSSA parameter usage warning + if ($null -eq $EmailAddress) { return } + [array]$UserArray = Test-UserObject -ToTest $EmailAddress + $headers=@{'hibp-api-key' = $hibpkey} + + foreach ($Object in $UserArray) { + + [string]$User = $Object.UserPrincipalName + + # Convert the email to URL encoding + $uriEncodeEmail = [uri]::EscapeDataString($($user)) + + # 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 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 + + } + }#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 35054fe..d295c90 100644 --- a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 +++ b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 @@ -49,8 +49,11 @@ [Parameter(Mandatory = $true)] [array]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } - Write-HawkBanner # Check if the logging filepath is set if ([string]::IsNullOrEmpty($Hawk.FilePath)) { diff --git a/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 b/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 index 867f14b..8e9b487 100644 --- a/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 +++ b/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 @@ -8,5 +8,7 @@ # Exclude this as old test rules use Global Vars, will need to fix old tests and re-include this rule 'PSAvoidGlobalVars' 'PSUseDeclaredVarsMoreThanAssignments' + # Exclude this to allow the use of Write-Host + 'PSAvoidUsingWriteHost' ) } \ No newline at end of file diff --git a/Hawk/internal/functions/Get-AllUnifiedAuditLogEntry.ps1 b/Hawk/internal/functions/Get-AllUnifiedAuditLogEntry.ps1 index 3626efe..18cf8c4 100644 --- a/Hawk/internal/functions/Get-AllUnifiedAuditLogEntry.ps1 +++ b/Hawk/internal/functions/Get-AllUnifiedAuditLogEntry.ps1 @@ -16,24 +16,20 @@ .NOTES General notes #> - param - ( + 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")) { + if ($UnifiedSearch -match "-StartDate|-EndDate|-SessionCommand|-ResultSize|-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 } - - # 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 diff --git a/Hawk/internal/functions/Get-IPGeolocation.ps1 b/Hawk/internal/functions/Get-IPGeolocation.ps1 index dbf1ff5..62bb3c7 100644 --- a/Hawk/internal/functions/Get-IPGeolocation.ps1 +++ b/Hawk/internal/functions/Get-IPGeolocation.ps1 @@ -31,7 +31,9 @@ Function Get-IPGeolocation { 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" + # get the access key from the user + Out-LogFile "ipstack.com accesskey" -isPrompt -NoNewLine + $Accesskey = (Read-Host).Trim() # add the access key to the appdata file Add-HawkAppData -name access_key -Value $Accesskey diff --git a/Hawk/internal/functions/Get-SimpleUnifiedAuditLog.ps1 b/Hawk/internal/functions/Get-SimpleUnifiedAuditLog.ps1 index c60efde..681cc65 100644 --- a/Hawk/internal/functions/Get-SimpleUnifiedAuditLog.ps1 +++ b/Hawk/internal/functions/Get-SimpleUnifiedAuditLog.ps1 @@ -73,15 +73,6 @@ $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, @@ -137,7 +128,18 @@ # Recursively process nested hashtables { $_ -is [System.Collections.IDictionary] } { $nestedObject = ConvertTo-FlatObject -InputObject $_ -Prefix $key -PreserveTypes:$PreserveTypes - $flatProperties += $nestedObject + foreach ($nestedKey in $nestedObject.Keys) { + $uniqueKey = if ($flatProperties.ContainsKey($nestedKey)) { + $counter = 1 + while ($flatProperties.ContainsKey("${nestedKey}_$counter")) { + $counter++ + } + "${nestedKey}_$counter" + } else { + $nestedKey + } + $flatProperties[$uniqueKey] = $nestedObject[$nestedKey] + } } # Process arrays (excluding Parameters which was handled above) { $_ -is [System.Collections.IList] -and $prop.Name -ne 'Parameters' } { @@ -146,7 +148,18 @@ # Handle array of objects for ($i = 0; $i -lt $_.Count; $i++) { $nestedObject = ConvertTo-FlatObject -InputObject $_[$i] -Prefix "${key}_${i}" -PreserveTypes:$PreserveTypes - $flatProperties += $nestedObject + foreach ($nestedKey in $nestedObject.Keys) { + $uniqueKey = if ($flatProperties.ContainsKey($nestedKey)) { + $counter = 1 + while ($flatProperties.ContainsKey("${nestedKey}_$counter")) { + $counter++ + } + "${nestedKey}_$counter" + } else { + $nestedKey + } + $flatProperties[$uniqueKey] = $nestedObject[$nestedKey] + } } } else { @@ -162,7 +175,18 @@ # Recursively process nested objects { $_ -is [PSObject] } { $nestedObject = ConvertTo-FlatObject -InputObject $_ -Prefix $key -PreserveTypes:$PreserveTypes - $flatProperties += $nestedObject + foreach ($nestedKey in $nestedObject.Keys) { + $uniqueKey = if ($flatProperties.ContainsKey($nestedKey)) { + $counter = 1 + while ($flatProperties.ContainsKey("${nestedKey}_$counter")) { + $counter++ + } + "${nestedKey}_$counter" + } else { + $nestedKey + } + $flatProperties[$uniqueKey] = $nestedObject[$nestedKey] + } } # Handle simple values default { diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index a502195..c473ddd 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -46,103 +46,143 @@ [string]$FilePath ) + + if ($Force) { + Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue + } + + # Check for incomplete/interrupted initialization and force a fresh start + if ($null -ne (Get-Variable -Name Hawk -ErrorAction SilentlyContinue)) { + if (Test-HawkGlobalObject) { + Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue + + # Remove other related global variables that might exist + Remove-Variable -Name IPlocationCache -Scope Global -ErrorAction SilentlyContinue + Remove-Variable -Name MSFTIPList -Scope Global -ErrorAction SilentlyContinue + } + } + Function Test-LoggingPath { param([string]$PathToTest) - + + # Get the current timestamp in the format yyyy-MM-dd HH:mm:ssZ + $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss'Z'") + # First test if the path we were given exists if (Test-Path $PathToTest) { - # If the path exists verify that it is a folder if ((Get-Item $PathToTest).PSIsContainer -eq $true) { Return $true } # If it is not a folder return false and write an error else { - Write-Information ("Path provided " + $PathToTest + " was not found to be a folder.") + Write-Information "[$timestamp] - [ERROR] - Path provided $PathToTest was not found to be a folder." Return $false } } # If it doesn't exist then return false and write an error else { - Write-Information ("Directory " + $PathToTest + " Not Found") + Write-Information "[$timestamp] - [ERROR] - Directory $PathToTest Not Found" Return $false } } - + Function New-LoggingFolder { [CmdletBinding(SupportsShouldProcess)] param([string]$RootPath) + + # Get the current timestamp in the format yyyy-MM-dd HH:mm:ssZ + $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss'Z'") + + try { + # Test Graph connection first to see if we're already connected + try { + $null = Get-MgOrganization -ErrorAction Stop + Write-Information "[$timestamp] - [INFO] - Already connected to Microsoft Graph" + } + catch { + # Only show connecting message if we actually need to connect + Write-Information "[$timestamp] - [ACTION] - Connecting to Microsoft Graph" + $null = Test-GraphConnection + Write-Information "[$timestamp] - [INFO] - Connected to Microsoft Graph Successfully" + } + + # Get tenant name + $TenantName = (Get-MGDomain -ErrorAction Stop | Where-Object { $_.isDefault }).ID + [string]$FolderID = "Hawk_" + $TenantName.Substring(0, $TenantName.IndexOf('.')) + "_" + (Get-Date).ToUniversalTime().ToString("yyyyMMdd_HHmmss") + + $FullOutputPath = Join-Path $RootPath $FolderID + + if (Test-Path $FullOutputPath) { + Write-Information "[$timestamp] - [ERROR] - Path $FullOutputPath already exists" + } + else { + Write-Information "[$timestamp] - [ACTION] - Creating subfolder $FullOutputPath" + $null = New-Item $FullOutputPath -ItemType Directory -ErrorAction Stop + } + + Return $FullOutputPath - # Create a folder ID based on UTC date - [string]$TenantName = (Get-MGDomain | Where-Object { $_.isDefault }).ID - [string]$FolderID = "Hawk_" + $TenantName.Substring(0, $TenantName.IndexOf('.')) + "_" + (Get-Date).ToUniversalTime().ToString("yyyyMMdd_HHmm") - - # Add that ID to the given path - $FullOutputPath = Join-Path $RootPath $FolderID - - # Just in case we run this twice in a min lets not throw an error - if (Test-Path $FullOutputPath) { - Write-Information "Path Exists" } - # If it is not there make it - else { - Write-Information ("Creating subfolder with name " + $FullOutputPath) - $null = New-Item $FullOutputPath -ItemType Directory + catch { + # If it fails at any point, display an error message + Write-Error "[$timestamp] - [ERROR] - Failed to create logging folder: $_" } - - Return $FullOutputPath } - + Function Set-LoggingPath { [CmdletBinding(SupportsShouldProcess)] param ([string]$Path) - - # If no value of Path is provided prompt and gather from the user + + # Get the current timestamp in the format yyyy-MM-dd HH:mm:ssZ + $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss'Z'") + + # If no value for Path is provided, prompt and gather from the user if ([string]::IsNullOrEmpty($Path)) { - - # Setup a while loop so we can get a valid path + # Setup a while loop to get a valid path Do { - - # Ask the customer for the output path - [string]$UserPath = Read-Host "Please provide an output directory" - - # If the path is valid then create the subfolder - if (Test-LoggingPath -PathToTest $UserPath) { - + # Ask the user for the output path + [string]$UserPath = (Read-Host "[$timestamp] - [PROMPT] - Please provide an output directory").Trim() + + # If the input is null or empty, prompt again + if ([string]::IsNullOrEmpty($UserPath)) { + Write-Host "[$timestamp] - [INFO] - Directory path cannot be empty. Please enter in a new path." + $ValidPath = $false + } + # If the path is valid, create the subfolder + elseif (Test-LoggingPath -PathToTest $UserPath) { $Folder = New-LoggingFolder -RootPath $UserPath $ValidPath = $true } - # If the path if not valid then we need to loop thru again + # If the path is invalid, prompt again else { - Write-Information ("Path not a valid Directory " + $UserPath) + Write-Information "[$timestamp] - [ERROR] - Path not a valid directory: $UserPath" $ValidPath = $false } - } While ($ValidPath -eq $false) } - # If a value if provided go from there + # If a value for Path is provided, validate it else { - # If the provided path is valid then we can create the subfolder + # If the provided path is valid, create the subfolder if (Test-LoggingPath -PathToTest $Path) { $Folder = New-LoggingFolder -RootPath $Path } - # If the provided path fails validation then we just need to stop + # If the provided path fails validation, stop the process else { - Write-Error ("Provided Path is not valid " + $Path) -ErrorAction Stop + Write-Error "[$timestamp] - [ERROR] - Provided path is not a valid directory: $Path" } } - + Return $Folder } - Function New-ApplicationInsight { [CmdletBinding(SupportsShouldProcess)] param() # Initialize Application Insights client $insightkey = "b69ffd8b-4569-497c-8ee7-b71b8257390e" if ($Null -eq $Client) { - Write-Output "Initializing Application Insights" + Out-LogFile "Initializing Application Insights" -Action $Client = New-AIClient -key $insightkey } } @@ -152,143 +192,246 @@ if (($null -eq (Get-Variable -Name Hawk -ErrorAction SilentlyContinue)) -or ($Force -eq $true) -or ($null -eq $Hawk)) { - # Setup Applicaiton insights + Write-HawkBanner + + # Create the global $Hawk variable immediately with minimal properties + $Global:Hawk = [PSCustomObject]@{ + FilePath = $null # Will be set shortly + DaysToLookBack = $null + StartDate = $null + EndDate = $null + WhenCreated = $null + } + + # Set up the file path first, before any other operations + if ([string]::IsNullOrEmpty($FilePath)) { + # Suppress Graph connection output during initial path setup + $Hawk.FilePath = Set-LoggingPath -ErrorAction Stop + } + else { + $Hawk.FilePath = Set-LoggingPath -path $FilePath -ErrorAction Stop 2>$null + } + + # Now that FilePath is set, we can use Out-LogFile + Out-LogFile "Hawk output directory created at: $($Hawk.FilePath)" -Information + + # Setup Application insights + Out-LogFile "Setting up Application Insights" -Action New-ApplicationInsight ### Checking for Updates ### # If we are skipping the update log it if ($SkipUpdate) { - Write-Information "Skipping Update Check" + Out-LogFile -string "Skipping Update Check" -Information } # Check to see if there is an Update for Hawk else { Update-HawkModule } - # Test if we have a connection to Microsoft Graph - Write-Information "Testing Graph Connection" + # Test Graph connection + Out-LogFile "Testing Graph Connection" -Action + 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" + try { + $LicenseInfo = Test-LicenseType + $MaxDaysToGoBack = $LicenseInfo.RetentionPeriod + $LicenseType = $LicenseInfo.LicenseType - #### 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)) { - [string]$OutputPath = Set-LoggingPath - } - else { - [string]$OutputPath = Set-LoggingPath -path $FilePath + Out-LogFile -string "Detecting M365 license type to determine maximum log retention period" -action + Out-LogFile -string "M365 License type detected: $LicenseType" -Information + Out-LogFile -string "Max log retention: $MaxDaysToGoBack days" -action -NoNewLine + + } catch { + Out-LogFile -string "Failed to detect license type. Max days of log retention is unknown." -Information + $MaxDaysToGoBack = 90 + $LicenseType = "Unknown" } - # We need to ask for start and end date if daystolookback was not set - if ($null -eq $StartDate) { + # Ensure MaxDaysToGoBack does not exceed 365 days + if ($MaxDaysToGoBack -gt 365) { $MaxDaysToGoBack = 365 } + + # Start date validation: Add check for negative numbers + while ($null -eq $StartDate) { + Write-Output "`n" + Out-LogFile "Please specify the first day of the search window:" -isPrompt + Out-LogFile " Enter a number of days to go back (1-$MaxDaysToGoBack)" -isPrompt + Out-LogFile " OR enter a date in MM/DD/YYYY format" -isPrompt + Out-LogFile " Default is 90 days back: " -isPrompt -NoNewLine + $StartRead = (Read-Host).Trim() + + # Determine if input is a valid date + if ($null -eq ($StartRead -as [DateTime])) { + #### Not a DateTime #### + if ([string]::IsNullOrEmpty($StartRead)) { + $StartRead = 90 + } + + # Validate input is a positive number + if ($StartRead -match '^\-') { + Out-LogFile -string "Please enter a positive number of days." -isError + continue + } - # Read in our # of days back or the actual start date - $StartRead = Read-Host "`nPlease Enter First Day of Search Window (1-90, Date, Default 90)" + # Validate numeric value + if ($StartRead -notmatch '^\d+$') { + Out-LogFile -string "Please enter a valid number of days." -isError + continue + } - # Determine if the input was a date time - # True means it was NOT a datetime - if ($Null -eq ($StartRead -as [DateTime])) { - #### Not a Date time #### + # Validate the entered days back + if ($StartRead -gt $MaxDaysToGoBack) { + Out-LogFile -string "You have entered a time frame greater than your license allows ($MaxDaysToGoBack days)." -isWarning + Out-LogFile "Press ENTER to proceed or type 'R' to re-enter the value: " -isPrompt -NoNewLine + $Proceed = (Read-Host).Trim() + if ($Proceed -eq 'R') { continue } + } - # if we have a null entry (just hit enter) then set startread to the default of 90 - if ([string]::IsNullOrEmpty($StartRead)) { $StartRead = 90 } - elseif (($StartRead -gt 90) -or ($StartRead -lt 1)) { - Write-Information "Value provided is outside of valid Range 1-90" - Write-Information "Setting StartDate to default of Today - 90 days" - $StartRead = 90 + if ($StartRead -gt 365) { + Out-LogFile -string "Log retention cannot exceed 365 days. Setting retention to 365 days." -isWarning + $StartRead = 365 } - # Calculate our startdate setting it to midnight UTC + # Calculate start date [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-$StartRead)).Date - Write-Information ("Start Date (UTC): " + $StartDate + "") + Write-Output "" + Out-LogFile -string "Start date set to: $StartDate [UTC]" -Information + } + # Handle DateTime input elseif (!($null -eq ($StartRead -as [DateTime]))) { - #### DATE TIME Provided #### - - # Convert the input to a UTC date time object [DateTime]$StartDate = (Get-Date $StartRead).ToUniversalTime().Date - # Test to make sure the date time is > 90 and < today - if ($StartDate -ge ((Get-date).ToUniversalTime().AddDays(-90).Date) -and ($StartDate -le (Get-Date).ToUniversalTime().Date)) { - #Valid Date do nothing + # Validate the date + if ($StartDate -gt (Get-Date).ToUniversalTime()) { + Out-LogFile -string "Start date cannot be in the future." -isError + $StartDate = $null + continue } - else { - Write-Information ("Date provided beyond acceptable range of 90 days.") - Write-Information ("Setting date to default of Today - 90 days.") - [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-90)).Date + + if ($StartDate -lt ((Get-Date).ToUniversalTime().AddDays(-$MaxDaysToGoBack))) { + Out-LogFile -string "The date entered exceeds your license retention period of $MaxDaysToGoBack days." -isWarning + Out-LogFile "Press ENTER to proceed or type 'R' to re-enter the date:" -isPrompt -NoNewLine + $Proceed = (Read-Host).Trim() + if ($Proceed -eq 'R') { $StartDate = $null; continue } + } + + if ($StartDate -lt ((Get-Date).ToUniversalTime().AddDays(-365))) { + Out-LogFile -string "The date cannot exceed 365 days. Setting to the maximum limit of 365 days." -isWarning + [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-365)).Date + } + + Out-LogFile -string "Start Date (UTC): $StartDate" -Information } else { - Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop + Out-LogFile -string "Invalid date information provided. Could not determine if this was a date or an integer." -isError + $StartDate = $null + continue } } - if ($null -eq $EndDate) { - # Read in the end date - $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 - if ($Null -eq ($EndRead -as [DateTime])) { - #### Not a Date time #### - - # if we have a null entry (just hit enter) then set startread to the default of today + # End date logic with enhanced validation + while ($null -eq $EndDate) { + Write-Output "`n" + Out-LogFile "Please specify the last day of the search window:" -isPrompt + Out-LogFile " Enter a number of days to go back from today (1-365)" -isPrompt + Out-LogFile " OR enter a specific date in MM/DD/YYYY format" -isPrompt + Out-LogFile " Default is today's date:" -isPrompt -NoNewLine + $EndRead = (Read-Host).Trim() + + # End date validation + if ($null -eq ($EndRead -as [DateTime])) { if ([string]::IsNullOrEmpty($EndRead)) { - [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(1)).Date + [DateTime]$tempEndDate = (Get-Date).ToUniversalTime().Date } else { - # Calculate our enddate setting it to midnight UTC - Write-Information ("End Date (UTC): " + $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).ToUniversalTime().AddDays( - ($EndRead - 1))).Date + # Validate input is a positive number + if ($EndRead -match '^\-') { + Out-LogFile -string "Please enter a positive number of days." -isError + continue + } + + # Validate numeric value + if ($EndRead -notmatch '^\d+$') { + Out-LogFile -string "Please enter a valid number of days." -isError + continue + } + + Out-LogFile -string "End Date (UTC): $EndRead days." -Information + [DateTime]$tempEndDate = ((Get-Date).ToUniversalTime().AddDays(-($EndRead - 1))).Date } - # Validate that the start date is further back in time than the end date - if ($StartDate -gt $EndDate) { - Write-Error "StartDate Cannot be More Recent than EndDate" -ErrorAction Stop - } - else { - Write-Information ("End Date (UTC): " + $EndDate + "`n") + if ($StartDate -gt $tempEndDate) { + Out-LogFile -string "End date must be more recent than start date ($StartDate)." -isError + continue } + + $EndDate = $tempEndDate + Write-Output "" + Out-LogFile -string "End date set to: $EndDate [UTC]`n" -Information } elseif (!($null -eq ($EndRead -as [DateTime]))) { - #### DATE TIME Provided #### - # Convert the input to a UTC date time object - [DateTime]$EndDate = ((Get-Date $EndRead).ToUniversalTime().AddDays(1)).Date + [DateTime]$tempEndDate = (Get-Date $EndRead).ToUniversalTime().Date - # Test to make sure the end date is newer than the start date - if ($StartDate -gt $EndDate) { - Write-Information "EndDate Selected was older than start date." - Write-Information "Setting EndDate to today." - [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(1)).Date + if ($StartDate -gt $tempEndDate) { + Out-LogFile -string "End date must be more recent than start date ($StartDate)." -isError + continue } - elseif ($EndDate -gt (Get-Date).ToUniversalTime().AddDays(2)) { - Write-Information "EndDate too far in the future." - Write-Information "Setting EndDate to Today." - [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(1)).Date + elseif ($tempEndDate -gt ((Get-Date).ToUniversalTime().AddDays(1))) { + Out-LogFile -string "EndDate too far in the future. Setting EndDate to today." -isWarning + $tempEndDate = (Get-Date).ToUniversalTime().Date } - Write-Information ("End Date (UTC): " + $EndDate + "`n") + $EndDate = $tempEndDate + Out-LogFile -string "End date set to: $EndDate [UTC]`n" -Information } else { - Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop + Out-LogFile -string "Invalid date information provided. Could not determine if this was a date or an integer." -isError + continue } } + # End date logic remains unchanged + if ($null -eq $EndDate) { + Write-Output "`n" + Out-LogFile "Please specify the last day of the search window:" -isPrompt + Out-LogFile " Enter a number of days to go back from today (1-365)" -isPrompt + Out-LogFile " OR enter a specific date in MM/DD/YYYY format" -isPrompt + Out-LogFile " Default is today's date:" -isPrompt -NoNewLine + $EndRead = (Read-Host).Trim() + + # End date validation + if ($null -eq ($EndRead -as [DateTime])) { + if ([string]::IsNullOrEmpty($EndRead)) { + [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date + } else { + Out-LogFile -string "End Date (UTC): $EndRead days." -Information + [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(-($EndRead - 1))).Date + } - # Determine if we have access to a P1 or P2 Azure Ad License - # EMS SKU contains Azure P1 as part of the sku - # This uses Graph instead of MSOL - Test-GraphConnection - if ([bool] (Get-MgSubscribedSku | Where-Object { ($_.SkuPartNumber -like "*aad_premium*") -or ($_.SkuPartNumber -like "*EMS*") -or ($_.SkuPartNumber -like "*E5*") -or ($_.SkuPartNumber -like "*G5*") } )) { - Write-Information "Advanced Azure AD License Found" - [bool]$AdvancedAzureLicense = $true - } - else { - Write-Information "Advanced Azure AD License NOT Found" - [bool]$AdvancedAzureLicense = $false + if ($StartDate -gt $EndDate) { + Out-LogFile -string "StartDate cannot be more recent than EndDate" -isError + } else { + Write-Output "" + Out-LogFile -string "End date set to: $EndDate [UTC]`n" -Information + } + } elseif (!($null -eq ($EndRead -as [DateTime]))) { + [DateTime]$EndDate = (Get-Date $EndRead).ToUniversalTime().Date + + if ($StartDate -gt $EndDate) { + Out-LogFile -string "EndDate is earlier than StartDate. Setting EndDate to today." -isWarning + [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date + } elseif ($EndDate -gt ((Get-Date).ToUniversalTime().AddDays(1))) { + Out-LogFile -string "EndDate too far in the future. Setting EndDate to today." -isWarning + [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date + } + + Out-LogFile -string "End date set to: $EndDate [UTC]`n" -Information + } else { + Out-LogFile -string "Invalid date information provided. Could not determine if this was a date or an integer." -isError + } } # Configuration Example, currently not used @@ -298,35 +441,17 @@ Set-PSFConfig -Module 'Hawk' -Name 'FilePath' -Value $OutputPath -PassThru | Register-PSFConfig } - #TODO: Discard below once migration to configuration is completed - $Output = [PSCustomObject]@{ - FilePath = $OutputPath - DaysToLookBack = $Days - StartDate = $StartDate - EndDate = $EndDate - AdvancedAzureLicense = $AdvancedAzureLicense - WhenCreated = (Get-Date).ToUniversalTime().ToString("g") - } + # Continue populating the Hawk object with other properties + $Hawk.DaysToLookBack = $Days + $Hawk.StartDate = $StartDate + $Hawk.EndDate = $EndDate + $Hawk.WhenCreated = (Get-Date).ToUniversalTime().ToString("g") + + Write-HawkConfigurationComplete -Hawk $Hawk + - # 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" -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 - } } else { - Write-Information "Valid Hawk Object already exists no actions will be taken." + Out-LogFile -string "Valid Hawk Object already exists no actions will be taken." -Information } -} \ No newline at end of file +} diff --git a/Hawk/internal/functions/Out-LogFile.ps1 b/Hawk/internal/functions/Out-LogFile.ps1 index 5d8a73f..7dab9fe 100644 --- a/Hawk/internal/functions/Out-LogFile.ps1 +++ b/Hawk/internal/functions/Out-LogFile.ps1 @@ -2,6 +2,7 @@ <# .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 @@ -9,11 +10,13 @@ Message types: - Action: Represent ongoing operations or procedures. - - Error: Represent failures, exceptions, or error conditions that prevented successful execution. + - 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 + - Information: Represent successful completion or informational status updates that do not require action or investigation. + - Warning: Indicates warning conditions that need attention but aren't errors. + - Prompt: Indicates user input is being requested. .PARAMETER string The log message to be written. @@ -40,10 +43,22 @@ Switch indicating the log entry provides informational status or completion messages, for example: "Retrieved all results" or "Completed data export successfully." + .PARAMETER isWarning + Switch indicating the log entry is a warning message. + The output is prefixed with [WARNING] in the log file. + + .PARAMETER isPrompt + Switch indicating the log entry is a user prompt message. + The output is prefixed with [PROMPT] in the log file. + + .PARAMETER NoNewLine + Switch indicating the message should be written without a newline at the end, + useful for prompts where input should appear on the same line. + .EXAMPLE Out-LogFile "Routine scan completed." - Writes a simple log message with a timestamp to the log file and displays it on the screen. + Writes a simple log message with a UTC timestamp to the log file and displays it on the screen. .EXAMPLE Out-LogFile "Starting mailbox export operation" -action @@ -57,6 +72,12 @@ Writes a log message indicating an error condition. The output is prefixed with [ERROR] in the log file. + .EXAMPLE + Out-LogFile "Enter your selection: " -isPrompt -NoNewLine + + Writes a prompt message without a newline so user input appears on the same line. + The output is prefixed with [PROMPT] in the log file. + .EXAMPLE Out-LogFile "Detected suspicious login attempt from external IP" -notice @@ -74,7 +95,13 @@ 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 "System resource warning: High CPU usage" -isWarning + + Writes a warning message to indicate a concerning but non-critical condition. + The output is prefixed with [WARNING] in the log file. + .EXAMPLE Out-LogFile "Executing periodic health check" -NoDisplay @@ -91,7 +118,10 @@ [switch]$silentnotice, [switch]$isError, [switch]$NoDisplay, - [switch]$Information + [switch]$Information, + [switch]$isWarning, + [switch]$isPrompt, + [switch]$NoNewLine ) Write-PSFMessage -Message $string -ModuleName Hawk -FunctionName (Get-PSCallstack)[1].FunctionName @@ -107,7 +137,9 @@ $LogOutput = $true # Get the current date in UTC - [string]$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss UTC") + + [string]$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ssZ") + [string]$logstring = "" # Build the log string based on the type of message @@ -138,6 +170,12 @@ elseif ($Information) { $logstring = "[$timestamp] - [INFO] - $string" } + elseif ($isWarning) { + $logstring = "[$timestamp] - [WARNING] - $string" + } + elseif ($isPrompt) { + $logstring = "[$timestamp] - [PROMPT] - $string" + } else { $logstring = "[$timestamp] - $string" } @@ -149,6 +187,11 @@ # Write to screen if enabled if ($ScreenOutput) { - Write-Information -MessageData $logstring -InformationAction Continue + if ($NoNewLine) { + Write-Host $logstring -InformationAction Continue -NoNewLine + } + else { + Write-Information $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 7110618..3d1e9df 100644 --- a/Hawk/internal/functions/Out-MultipleFileType.ps1 +++ b/Hawk/internal/functions/Out-MultipleFileType.ps1 @@ -91,7 +91,7 @@ Function Out-MultipleFileType { end { if ($null -eq $AllObject) { - Out-LogFile "No Data Found" -Information + Out-LogFile "No Data Found" -Action } else { diff --git a/Hawk/internal/functions/Test-GraphConnection.ps1 b/Hawk/internal/functions/Test-GraphConnection.ps1 index 2b377b3..d4f19f3 100644 --- a/Hawk/internal/functions/Test-GraphConnection.ps1 +++ b/Hawk/internal/functions/Test-GraphConnection.ps1 @@ -20,12 +20,15 @@ Function Test-GraphConnection { } catch { # Fallback if $Hawk is not initialized + $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss") + if ($null -eq $Hawk) { - Write-Output "Connecting to MGGraph using MGGraph Module" + # Use standardized timestamp format when Hawk isn't initialized + Write-Output "[$timestamp UTC] - [ACTION] - Connecting to Microsoft Graph API" } else { # $Hawk exists, so we can safely use Out-LogFile - Out-LogFile -String "Connecting to MGGraph using MGGraph Module" -Action + Write-Output "[$timestamp UTC] - [ACTION] - Connecting to Microsoft Graph API" } Connect-MGGraph diff --git a/Hawk/internal/functions/Test-HawkGlobalObject.ps1 b/Hawk/internal/functions/Test-HawkGlobalObject.ps1 new file mode 100644 index 0000000..f27c63a --- /dev/null +++ b/Hawk/internal/functions/Test-HawkGlobalObject.ps1 @@ -0,0 +1,37 @@ +Function Test-HawkGlobalObject { + <# + .SYNOPSIS + Tests if the Hawk global object exists and is properly initialized. + + .DESCRIPTION + This is an internal helper function that verifies whether the Hawk global object + exists and contains all required properties properly initialized. It checks for: + - FilePath property existence and value + - StartDate property existence and value + - EndDate property existence and value + + .EXAMPLE + Test-HawkGlobalObject + Returns $true if Hawk object is properly initialized, $false otherwise. + + .OUTPUTS + Boolean indicating if reinitialization is needed + #> + [CmdletBinding()] + [OutputType([bool])] + param() + + # Return true (needs initialization) if: + # - Hawk object doesn't exist + # - Any required property is missing or null + if ([string]::IsNullOrEmpty($Hawk.FilePath) -or + $null -eq $Hawk.StartDate -or + $null -eq $Hawk.EndDate -or + ($Hawk.PSObject.Properties.Name -contains 'StartDate' -and $null -eq $Hawk.StartDate) -or + ($Hawk.PSObject.Properties.Name -contains 'EndDate' -and $null -eq $Hawk.EndDate)) { + return $true + } + + # Hawk object exists and is properly initialized + return $false +} \ No newline at end of file diff --git a/Hawk/internal/functions/Test-LicenseType.ps1 b/Hawk/internal/functions/Test-LicenseType.ps1 new file mode 100644 index 0000000..163316c --- /dev/null +++ b/Hawk/internal/functions/Test-LicenseType.ps1 @@ -0,0 +1,84 @@ +function Test-LicenseType { + <# + .SYNOPSIS + Identifies the Microsoft 365 license type (E5, E3, or other) for the current tenant and returns both the license type and corresponding retention period. + + .DESCRIPTION + This function retrieves the list of subscribed SKUs for the tenant using the Microsoft Graph API. It determines the license type based on the `SkuPartNumber` and returns both the license type and appropriate audit log retention period: + - E5 licenses (including equivalents like Developer Pack E5): 365 days retention + - E3 licenses (including equivalents like Developer Pack E3): 180 days retention + - Other/Unknown licenses: 90 days retention + + .EXAMPLE + PS> Test-HawkLicenseType + + LicenseType RetentionPeriod + ----------- --------------- + E5 365 + + Returns E5 license type and 365 days retention period. + + .EXAMPLE + PS> Test-HawkLicenseType + + LicenseType RetentionPeriod + ----------- --------------- + E3 180 + + Returns E3 license type and 180 days retention period. + + .EXAMPLE + PS> Test-HawkLicenseType + + LicenseType RetentionPeriod + ----------- --------------- + Unknown 90 + + Returns Unknown license type and default 90 days retention period. + + .NOTES + Author: Jonathan Butler + Last Updated: January 9, 2025 + + .LINK + https://learn.microsoft.com/en-us/powershell/microsoftgraph + #> + [CmdletBinding()] + [OutputType([PSCustomObject])] + param() + + try { + # Get tenant subscriptions + $subscriptions = Get-MgSubscribedSku + + # Create custom object to store both license type and retention period + $licenseInfo = [PSCustomObject]@{ + LicenseType = 'Unknown' + RetentionPeriod = 90 + } + + # Check for E5 or equivalent license + if ($subscriptions.SkuPartNumber -match 'ENTERPRISEPREMIUM|SPE_E5|DEVELOPERPACK_E5|M365_E5') { + $licenseInfo.LicenseType = 'E5' + $licenseInfo.RetentionPeriod = 365 + return $licenseInfo + } + + # Check for E3 or equivalent license + if ($subscriptions.SkuPartNumber -match 'ENTERPRISEPACK|M365_E3|DEVELOPERPACK_E3') { + $licenseInfo.LicenseType = 'E3' + $licenseInfo.RetentionPeriod = 180 + return $licenseInfo + } + + # Return default values for unknown license type + return $licenseInfo + } + catch { + Out-LogFile "Unable to determine license type. Defaulting to 90 days retention." -information + return [PSCustomObject]@{ + LicenseType = 'Unknown' + RetentionPeriod = 90 + } + } +} \ No newline at end of file diff --git a/Hawk/internal/functions/Write-HawkBanner.ps1 b/Hawk/internal/functions/Write-HawkBanner.ps1 index a15c515..a7745e6 100644 --- a/Hawk/internal/functions/Write-HawkBanner.ps1 +++ b/Hawk/internal/functions/Write-HawkBanner.ps1 @@ -25,7 +25,17 @@ Function Write-HawkBanner { Microsoft Cloud Security Analysis Tool https://cloudforensicator.com +======================================== + '@ Write-Output $banner + + Write-Information "Welcome to Hawk! Let's get your investigation environment set up." + Write-Information "We'll guide you through configuring the output file path and investigation date range." + Write-Information "You'll need to specify where logs should be saved and the time window for data retrieval." + Write-Information "If you're unsure, don't worry! Default options will be provided to help you out." + Write-Information "`nLet's get started!`n" + + } \ No newline at end of file diff --git a/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 b/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 new file mode 100644 index 0000000..fab4123 --- /dev/null +++ b/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 @@ -0,0 +1,62 @@ +Function Write-HawkConfigurationComplete { + <# + .SYNOPSIS + Displays the completed Hawk configuration settings. + + .DESCRIPTION + Outputs a summary of all configured Hawk settings after initialization is complete. + This includes version information and all properties of the Hawk configuration object, + formatted for easy reading. Null or empty values are displayed as "N/A". + + .PARAMETER Hawk + A PSCustomObject containing the Hawk configuration settings. This object must include + properties for FilePath, DaysToLookBack, StartDate, EndDate, and other required + configuration values. + + .EXAMPLE + PS C:\> Write-HawkConfigurationComplete -Hawk $Hawk + + Displays the complete Hawk configuration settings from the provided Hawk object, + including file paths, date ranges, and version information. + + .EXAMPLE + PS C:\> $config = Initialize-HawkGlobalObject + PS C:\> Write-HawkConfigurationComplete -Hawk $config + + Initializes a new Hawk configuration and displays the complete settings. + + .NOTES + This function is typically called automatically after Hawk initialization + but can be run manually to review current settings. + #> + [CmdletBinding()] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + HelpMessage = "PSCustomObject containing Hawk configuration settings" + )] + [PSCustomObject]$Hawk + ) + + process { + Out-LogFile "Configuration Complete!" -Information + Out-LogFile "Your Hawk environment is now set up with the following settings:" -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 -string ("{0} = {1}" -f $prop.Name, $value) -Information + } + + Out-LogFile "`Happy hunting! 🦅`n" -action + } + } \ No newline at end of file