From 08100d5e64b99fe2cbd5308bbfdff64ec09ea559 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Thu, 9 Jan 2025 19:36:17 -0500 Subject: [PATCH 01/23] Create Test-HawkLicenseType to determine whether Tenant has E3 or E5 licensing. --- .../functions/Test-HawkLicenseType.ps1 | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 Hawk/internal/functions/Test-HawkLicenseType.ps1 diff --git a/Hawk/internal/functions/Test-HawkLicenseType.ps1 b/Hawk/internal/functions/Test-HawkLicenseType.ps1 new file mode 100644 index 0000000..20c6004 --- /dev/null +++ b/Hawk/internal/functions/Test-HawkLicenseType.ps1 @@ -0,0 +1,73 @@ +function Test-HawkLicenseType { + <# + .SYNOPSIS + Identifies the Microsoft 365 license type (E5, E3, or other) for the current tenant and returns the corresponding retention period. + + .DESCRIPTION + The `Test-HawkLicenseType` function retrieves the list of subscribed SKUs for the tenant using the Microsoft Graph API. + It checks the license type based on the SkuPartNumber and returns the appropriate retention period in days: + - 365 days for E5 licenses (including equivalents like Developer Pack E5). + - 180 days for E3 licenses. + - 90 days as a default retention period if no matching license is found. + + .EXAMPLE + PS C:\> Test-HawkLicenseType + Returns the retention period based on the tenant's license type. + + .NOTES + Author: Jonathan Butler + Last Updated: January 9, 2025 + This function uses Microsoft Graph cmdlets to retrieve the tenant's license information. + + .REQUIREMENTS + - Microsoft.Graph module must be installed and authenticated. + - User must have permission to query the tenant's subscription details. + + .PARAMETER None + This function does not take any parameters. + + .RETURNS + [int] - Retention period in days: + - 365 for E5 or equivalent licenses. + - 180 for E3 or equivalent licenses. + - 90 by default if no matching license is found. + + .INPUTS + None. The function does not accept input from the pipeline. + + .OUTPUTS + [int] - Retention period in days. + + .COMPONENT + Microsoft Graph PowerShell Module + + .LINK + https://learn.microsoft.com/en-us/powershell/microsoftgraph + #> + + [CmdletBinding()] + [OutputType([int])] + param() + + try { + # Get tenant subscriptions + $subscriptions = Get-MgSubscribedSku + + # Check for E5 or equivalent license + if ($subscriptions.SkuPartNumber -match 'ENTERPRISEPREMIUM|SPE_E5|DEVELOPERPACK_E5|M365_E5') { + return 365 # E5 license retention period + } + + # Check for E3 or equivalent license + if ($subscriptions.SkuPartNumber -match 'ENTERPRISEPACK|M365_E3|DEVELOPERPACK_E3') { + return 180 # E3 license retention period + } + + # Default retention period + return 90 + } + catch { + Out-LogFile "Unable to determine license type. Defaulting to 90 days retention." -isError + return 90 + } +} From c3b9a31c557f165e93752be06199e98b8e291a24 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Thu, 9 Jan 2025 19:38:23 -0500 Subject: [PATCH 02/23] Create Test-HawkLicenseType to determine whether Tenant has E3 or E5 licensing. --- .../functions/Test-HawkLicenseType.ps1 | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/Hawk/internal/functions/Test-HawkLicenseType.ps1 b/Hawk/internal/functions/Test-HawkLicenseType.ps1 index 20c6004..58849b1 100644 --- a/Hawk/internal/functions/Test-HawkLicenseType.ps1 +++ b/Hawk/internal/functions/Test-HawkLicenseType.ps1 @@ -4,15 +4,15 @@ function Test-HawkLicenseType { Identifies the Microsoft 365 license type (E5, E3, or other) for the current tenant and returns the corresponding retention period. .DESCRIPTION - The `Test-HawkLicenseType` function retrieves the list of subscribed SKUs for the tenant using the Microsoft Graph API. - It checks the license type based on the SkuPartNumber and returns the appropriate retention period in days: + This function retrieves the list of subscribed SKUs for the tenant using the Microsoft Graph API. + It checks the license type based on the `SkuPartNumber` and returns the appropriate retention period in days: - 365 days for E5 licenses (including equivalents like Developer Pack E5). - - 180 days for E3 licenses. + - 180 days for E3 licenses (including equivalents like Developer Pack E3). - 90 days as a default retention period if no matching license is found. .EXAMPLE PS C:\> Test-HawkLicenseType - Returns the retention period based on the tenant's license type. + This command returns the retention period based on the tenant's Microsoft 365 license type. .NOTES Author: Jonathan Butler @@ -21,30 +21,27 @@ function Test-HawkLicenseType { .REQUIREMENTS - Microsoft.Graph module must be installed and authenticated. - - User must have permission to query the tenant's subscription details. + - The user must have permission to query the tenant's subscription details. .PARAMETER None This function does not take any parameters. - .RETURNS - [int] - Retention period in days: - - 365 for E5 or equivalent licenses. - - 180 for E3 or equivalent licenses. - - 90 by default if no matching license is found. - .INPUTS - None. The function does not accept input from the pipeline. + None. This function does not accept input from the pipeline. .OUTPUTS - [int] - Retention period in days. + [int] - Returns the retention period in days based on the identified license type: + - 365 for E5 or equivalent licenses. + - 180 for E3 or equivalent licenses. + - 90 as the default retention period. .COMPONENT Microsoft Graph PowerShell Module .LINK https://learn.microsoft.com/en-us/powershell/microsoftgraph - #> + #> [CmdletBinding()] [OutputType([int])] param() From c8c3788bf6336fecd0fd890efb65cf5816e67c62 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Thu, 9 Jan 2025 19:40:13 -0500 Subject: [PATCH 03/23] Create Test-HawkLicenseType to determine whether Tenant has E3 or E5 licensing. --- .../functions/Test-HawkLicenseType.ps1 | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/Hawk/internal/functions/Test-HawkLicenseType.ps1 b/Hawk/internal/functions/Test-HawkLicenseType.ps1 index 58849b1..d84687c 100644 --- a/Hawk/internal/functions/Test-HawkLicenseType.ps1 +++ b/Hawk/internal/functions/Test-HawkLicenseType.ps1 @@ -4,43 +4,35 @@ function Test-HawkLicenseType { Identifies the Microsoft 365 license type (E5, E3, or other) for the current tenant and returns the corresponding retention period. .DESCRIPTION - This function retrieves the list of subscribed SKUs for the tenant using the Microsoft Graph API. - It checks the license type based on the `SkuPartNumber` and returns the appropriate retention period in days: - - 365 days for E5 licenses (including equivalents like Developer Pack E5). - - 180 days for E3 licenses (including equivalents like Developer Pack E3). - - 90 days as a default retention period if no matching license is found. + 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` to return the appropriate audit log retention period: + - 365 days for E5 licenses (including equivalents like Developer Pack E5) + - 180 days for E3 licenses (including equivalents like Developer Pack E3) + - 90 days as a default retention period if no matching license is found .EXAMPLE - PS C:\> Test-HawkLicenseType - This command returns the retention period based on the tenant's Microsoft 365 license type. + PS> Test-HawkLicenseType + 365 + + Returns 365 days retention period because the tenant has an E5 license. + + .EXAMPLE + PS> Test-HawkLicenseType + 180 + + Returns 180 days retention period because the tenant has an E3 license. + + .EXAMPLE + PS> Test-HawkLicenseType + 90 + + Returns default 90 days retention period because tenant license type could not be determined. .NOTES Author: Jonathan Butler Last Updated: January 9, 2025 - This function uses Microsoft Graph cmdlets to retrieve the tenant's license information. - - .REQUIREMENTS - - Microsoft.Graph module must be installed and authenticated. - - The user must have permission to query the tenant's subscription details. - - .PARAMETER None - This function does not take any parameters. - - .INPUTS - None. This function does not accept input from the pipeline. - - .OUTPUTS - [int] - Returns the retention period in days based on the identified license type: - - 365 for E5 or equivalent licenses. - - 180 for E3 or equivalent licenses. - - 90 as the default retention period. - - .COMPONENT - Microsoft Graph PowerShell Module .LINK https://learn.microsoft.com/en-us/powershell/microsoftgraph - #> [CmdletBinding()] [OutputType([int])] @@ -67,4 +59,4 @@ function Test-HawkLicenseType { Out-LogFile "Unable to determine license type. Defaulting to 90 days retention." -isError return 90 } -} +} \ No newline at end of file From b6dff2316c89e708134f1d994e5053737b8d6092 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Thu, 9 Jan 2025 21:33:49 -0500 Subject: [PATCH 04/23] Update Test-HawkLicenseType and add MaxAuditDays to Initialize-HawkGlobalObject --- .../functions/Initialize-HawkGlobalObject.ps1 | 14 ++++++++------ Hawk/internal/functions/Test-HawkLicenseType.ps1 | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 82d35ba..92b16c0 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -301,12 +301,13 @@ #TODO: Discard below once migration to configuration is completed $Output = [PSCustomObject]@{ - FilePath = $OutputPath - DaysToLookBack = $Days - StartDate = $StartDate - EndDate = $EndDate + FilePath = $OutputPath + DaysToLookBack = $Days + StartDate = $StartDate + EndDate = $EndDate AdvancedAzureLicense = $AdvancedAzureLicense - WhenCreated = (Get-Date -Format g) + WhenCreated = (Get-Date -Format g) + MaxAuditDays = (Test-HawkLicenseType) } # Create the script hawk variable @@ -319,7 +320,8 @@ # 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 { + } + else { $prop.Value } diff --git a/Hawk/internal/functions/Test-HawkLicenseType.ps1 b/Hawk/internal/functions/Test-HawkLicenseType.ps1 index d84687c..35f9ace 100644 --- a/Hawk/internal/functions/Test-HawkLicenseType.ps1 +++ b/Hawk/internal/functions/Test-HawkLicenseType.ps1 @@ -56,7 +56,7 @@ function Test-HawkLicenseType { return 90 } catch { - Out-LogFile "Unable to determine license type. Defaulting to 90 days retention." -isError + Out-LogFile "Unable to determine license type. Defaulting to 90 days retention." -information return 90 } } \ No newline at end of file From a0bb88bc7d04e3c80d0536766cbf6d781e41feb6 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 09:23:33 -0500 Subject: [PATCH 05/23] Removed Azure AD license check and associated variable as this is no longer needed since Hawk has been migrated to the graph API and the new Test-HawkLicenseTyp has been implmented to check for license types. --- .../functions/Initialize-HawkGlobalObject.ps1 | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 92b16c0..2de1d36 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -279,19 +279,6 @@ } } - # 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 - } - # Configuration Example, currently not used #TODO: Implement Configuration system across entire project Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value $Days -PassThru | Register-PSFConfig @@ -305,7 +292,6 @@ DaysToLookBack = $Days StartDate = $StartDate EndDate = $EndDate - AdvancedAzureLicense = $AdvancedAzureLicense WhenCreated = (Get-Date -Format g) MaxAuditDays = (Test-HawkLicenseType) } From 516826cfbbb8aecdbf98d16d528f09d87f6a0da5 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 10:53:17 -0500 Subject: [PATCH 06/23] Removed Azure AD license check and associated variable as this is no longer needed since Hawk has been migrated to the graph API and the new Test-HawkLicenseTyp has been implmented to check for license types. --- .../User/Start-HawkUserInvestigation.ps1 | 72 ++++---- .../functions/Get-AllUnifiedAuditLogEntry.ps1 | 12 +- .../functions/Initialize-HawkGlobalObject.ps1 | 164 ++++++++++-------- .../functions/Test-HawkLicenseType.ps1 | 62 ------- Hawk/internal/functions/Test-LicenseType.ps1 | 84 +++++++++ 5 files changed, 213 insertions(+), 181 deletions(-) delete mode 100644 Hawk/internal/functions/Test-HawkLicenseType.ps1 create mode 100644 Hawk/internal/functions/Test-LicenseType.ps1 diff --git a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 index 35054fe..3fafd50 100644 --- a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 +++ b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 @@ -70,50 +70,50 @@ 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-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-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-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 - } + # if ($PSCmdlet.ShouldProcess("Running Get-HawkUserMobileDevice for $User")) { + # Out-LogFile "Running Get-HawkUserMobileDevice" -Action + # Get-HawkUserMobileDevice -User $User + # } } } } 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/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 2de1d36..e6c709e 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -1,41 +1,41 @@ Function Initialize-HawkGlobalObject { <# -.SYNOPSIS - Create global variable $Hawk for use by all Hawk cmdlets. -.DESCRIPTION - Creates the global variable $Hawk and populates it with information needed by the other Hawk cmdlets. - - * Checks for latest version of the Hawk module - * Creates path for output files - * 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 SkipUpdate - Skips checking for the latest version of the Hawk Module -.PARAMETER DaysToLookBack - Defines the # of days to look back in the availible logs. - Valid values are 1-90 -.PARAMETER StartDate - First day that data will be retrieved -.PARAMETER EndDate - Last day that data will be retrieved -.PARAMETER FilePath - Provide an output file path. -.OUTPUTS - Creates the $Hawk global variable and populates it with a custom PS object with the following properties - - Property Name Contents - ========== ========== - FilePath Path to output files - DaysToLookBack Number of day back in time we are searching - StartDate Calculated start date for searches based on DaysToLookBack - EndDate One day in the future - WhenCreated Date and time that the variable was created -.EXAMPLE - Initialize-HawkGlobalObject -Force - - This Command will force the creation of a new $Hawk variable even if one already exists. -#> + .SYNOPSIS + Create global variable $Hawk for use by all Hawk cmdlets. + .DESCRIPTION + Creates the global variable $Hawk and populates it with information needed by the other Hawk cmdlets. + + * Checks for latest version of the Hawk module + * Creates path for output files + * Records target start and end dates for searches (in UTC) + .PARAMETER Force + Switch to force the function to run and allow the variable to be recreated + .PARAMETER SkipUpdate + Skips checking for the latest version of the Hawk Module + .PARAMETER DaysToLookBack + Defines the # of days to look back in the availible logs. + Valid values are 1-90 + .PARAMETER StartDate + First day that data will be retrieved (in UTC) + .PARAMETER EndDate + Last day that data will be retrieved (in UTC) + .PARAMETER FilePath + Provide an output file path. + .OUTPUTS + Creates the $Hawk global variable and populates it with a custom PS object with the following properties + + Property Name Contents + ========== ========== + FilePath Path to output files + DaysToLookBack Number of day back in time we are searching + StartDate Calculated start date for searches based on DaysToLookBack (UTC) + EndDate One day in the future (UTC) + WhenCreated Date and time that the variable was created (UTC) + .EXAMPLE + Initialize-HawkGlobalObject -Force + + This Command will force the creation of a new $Hawk variable even if one already exists. + #> [CmdletBinding()] param ( @@ -73,9 +73,9 @@ [CmdletBinding(SupportsShouldProcess)] param([string]$RootPath) - # Create a folder ID based on date + # 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 -UFormat %Y%m%d_%H%M).tostring() + [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 @@ -181,43 +181,60 @@ [string]$OutputPath = Set-LoggingPath -path $FilePath } + # Get license information and validate retention period + try { + $LicenseInfo = Test-LicenseType + $MaxDaysToGoBack = $LicenseInfo.RetentionPeriod + $LicenseType = $LicenseInfo.LicenseType + + Write-Information "Detecting M365 license type to determine maximum log retention period" + Write-Information "M365 License type detected: $LicenseType" + Write-Information "Max log retention: $MaxDaysToGoBack days" + + } catch { + Write-Information "Failed to detect license type. Max days of log retention is unknown." + $MaxDaysToGoBack = 90 + $LicenseType = "Unknown" + } + # We need to ask for start and end date if daystolookback was not set if ($null -eq $StartDate) { # 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)" + $StartRead = Read-Host "`nPlease Enter First Day of Search Window (1-$MaxDaysToGoBack, Date, Default 90)" # 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 #### - # if we have a null entry (just hit enter) then set startread to the default of 90 + # if we have a null entry (just hit enter) then set startread to the default of 365 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 "Value provided is outside of valid Range 1-365" Write-Information "Setting StartDate to default of Today - 90 days" $StartRead = 90 } - # Calculate our startdate setting it to midnight - [DateTime]$StartDate = ((Get-Date).AddDays(-$StartRead)).Date - Write-Information ("Start Date: " + $StartDate + "") + # Calculate our startdate setting it to midnight UTC + [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-$StartRead)).Date + Write-Information ("Start Date (UTC): " + $StartDate + "") } elseif (!($null -eq ($StartRead -as [DateTime]))) { #### DATE TIME Provided #### - # Convert the input to a date time object - [DateTime]$StartDate = (Get-Date $StartRead).Date + # 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).AddDays(-90).Date) -and ($StartDate -le (Get-Date).Date)) { + # Test to make sure the date time is not further back than 365 and < today + # I DONT KNOW WHAT TO CHANGE HERE + if ($StartDate -ge ((Get-date).ToUniversalTime().AddDays(-365).Date) -and ($StartDate -le (Get-Date).ToUniversalTime().Date)) { #Valid Date do nothing } else { - Write-Information ("Date provided beyond acceptable range of 90 days.") + Write-Information ("Date provided beyond acceptable range of 365 days.") Write-Information ("Setting date to default of Today - 90 days.") - [DateTime]$StartDate = ((Get-Date).AddDays(-90)).Date + [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-90)).Date } } else { @@ -227,22 +244,22 @@ if ($null -eq $EndDate) { # Read in the end date - $EndRead = Read-Host "`nPlease Enter Last Day of Search Window (1-90, date, Default Today)" + $EndRead = Read-Host "`nPlease Enter Last Day of Search Window (1-365, 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 90 + # if we have a null entry (just hit enter) then set startread to the default of today if ([string]::IsNullOrEmpty($EndRead)) { - [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date + [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(1)).Date } else { - # Calculate our startdate setting it to midnight - Write-Information ("End Date: " + $EndRead + " days.") + # 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).AddDays( - ($EndRead - 1))).Date + [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays( - ($EndRead - 1))).Date } # Validate that the start date is further back in time than the end date @@ -250,30 +267,29 @@ Write-Error "StartDate Cannot be More Recent than EndDate" -ErrorAction Stop } else { - Write-Information ("End Date: " + $EndDate + "`n") + Write-Information ("End Date (UTC): " + $EndDate + "`n") } } elseif (!($null -eq ($EndRead -as [DateTime]))) { #### DATE TIME Provided #### - # Convert the input to a date time object - [DateTime]$EndDate = ((Get-Date $EndRead).AddDays(1)).Date + # Convert the input to a UTC date time object + [DateTime]$EndDate = ((Get-Date $EndRead).ToUniversalTime().AddDays(1)).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).AddDays(1)).Date + [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(1)).Date } - elseif ($EndDate -gt (get-Date).AddDays(2)) { - Write-Information "EndDate to Far in the furture." + 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).AddDays(1)).Date + [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(1)).Date } - Write-Information ("Setting EndDate by Date to " + $EndDate + "`n") + Write-Information ("End Date (UTC): " + $EndDate + "`n") } - else { Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop } @@ -288,12 +304,12 @@ #TODO: Discard below once migration to configuration is completed $Output = [PSCustomObject]@{ - FilePath = $OutputPath - DaysToLookBack = $Days - StartDate = $StartDate - EndDate = $EndDate - WhenCreated = (Get-Date -Format g) - MaxAuditDays = (Test-HawkLicenseType) + FilePath = $OutputPath + DaysToLookBack = $Days + StartDate = $StartDate + EndDate = $EndDate + MaxDays = $MaxDaysToGoBack + WhenCreated = (Get-Date).ToUniversalTime().ToString("g") } # Create the script hawk variable @@ -301,21 +317,19 @@ 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 { + } 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." } diff --git a/Hawk/internal/functions/Test-HawkLicenseType.ps1 b/Hawk/internal/functions/Test-HawkLicenseType.ps1 deleted file mode 100644 index 35f9ace..0000000 --- a/Hawk/internal/functions/Test-HawkLicenseType.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -function Test-HawkLicenseType { - <# - .SYNOPSIS - Identifies the Microsoft 365 license type (E5, E3, or other) for the current tenant and returns the 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` to return the appropriate audit log retention period: - - 365 days for E5 licenses (including equivalents like Developer Pack E5) - - 180 days for E3 licenses (including equivalents like Developer Pack E3) - - 90 days as a default retention period if no matching license is found - - .EXAMPLE - PS> Test-HawkLicenseType - 365 - - Returns 365 days retention period because the tenant has an E5 license. - - .EXAMPLE - PS> Test-HawkLicenseType - 180 - - Returns 180 days retention period because the tenant has an E3 license. - - .EXAMPLE - PS> Test-HawkLicenseType - 90 - - Returns default 90 days retention period because tenant license type could not be determined. - - .NOTES - Author: Jonathan Butler - Last Updated: January 9, 2025 - - .LINK - https://learn.microsoft.com/en-us/powershell/microsoftgraph - #> - [CmdletBinding()] - [OutputType([int])] - param() - - try { - # Get tenant subscriptions - $subscriptions = Get-MgSubscribedSku - - # Check for E5 or equivalent license - if ($subscriptions.SkuPartNumber -match 'ENTERPRISEPREMIUM|SPE_E5|DEVELOPERPACK_E5|M365_E5') { - return 365 # E5 license retention period - } - - # Check for E3 or equivalent license - if ($subscriptions.SkuPartNumber -match 'ENTERPRISEPACK|M365_E3|DEVELOPERPACK_E3') { - return 180 # E3 license retention period - } - - # Default retention period - return 90 - } - catch { - Out-LogFile "Unable to determine license type. Defaulting to 90 days retention." -information - return 90 - } -} \ 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 From 6014f6202cc86558052d444edcb1b9291c29e41b Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 12:03:49 -0500 Subject: [PATCH 07/23] Add prompt for user in the event they enter a day search range outside their license limit. --- .../functions/Initialize-HawkGlobalObject.ps1 | 163 +++++++++--------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index e6c709e..8cd33bb 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -181,117 +181,118 @@ [string]$OutputPath = Set-LoggingPath -path $FilePath } - # Get license information and validate retention period try { $LicenseInfo = Test-LicenseType $MaxDaysToGoBack = $LicenseInfo.RetentionPeriod $LicenseType = $LicenseInfo.LicenseType - + Write-Information "Detecting M365 license type to determine maximum log retention period" Write-Information "M365 License type detected: $LicenseType" Write-Information "Max log retention: $MaxDaysToGoBack days" - + } catch { Write-Information "Failed to detect license type. Max days of log retention is unknown." $MaxDaysToGoBack = 90 $LicenseType = "Unknown" } - - # We need to ask for start and end date if daystolookback was not set - if ($null -eq $StartDate) { - - # Read in our # of days back or the actual start date - $StartRead = Read-Host "`nPlease Enter First Day of Search Window (1-$MaxDaysToGoBack, Date, Default 90)" - - # 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 #### - - # if we have a null entry (just hit enter) then set startread to the default of 365 - if ([string]::IsNullOrEmpty($StartRead)) { $StartRead = 90 } - elseif (($StartRead -gt 90) -or ($StartRead -lt 1)) { - Write-Information "Value provided is outside of valid Range 1-365" - Write-Information "Setting StartDate to default of Today - 90 days" - $StartRead = 90 + + # Ensure MaxDaysToGoBack does not exceed 365 days + if ($MaxDaysToGoBack -gt 365) { $MaxDaysToGoBack = 365 } + + # Prompt for Start Date if not set + while ($null -eq $StartDate) { + + # Read input from user + Write-Output "`nPlease specify the first day of the search window:" + Write-Output " - Enter a number of days to go back (1-$MaxDaysToGoBack)" + Write-Output " - OR enter a date in MM/DD/YYYY format" + $StartRead = Read-Host "Default is 90 days back" + + # Determine if input is a valid date + if ($null -eq ($StartRead -as [DateTime])) { + + #### Not a DateTime #### + if ([string]::IsNullOrEmpty($StartRead)) { + $StartRead = 90 } - - # Calculate our startdate setting it to midnight UTC + + # Validate the entered days back + if ($StartRead -gt $MaxDaysToGoBack) { + Write-Warning "You have entered a time frame greater than your license allows ($MaxDaysToGoBack days)." + $Proceed = Read-Host "Press ENTER to proceed or type 'R' to re-enter the value" + if ($Proceed -eq 'R') { continue } + } + + if ($StartRead -gt 365) { + Write-Warning "Log retention cannot exceed 365 days. Setting retention to 365 days." + $StartRead = 365 + } + + # Calculate start date [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-$StartRead)).Date - Write-Information ("Start Date (UTC): " + $StartDate + "") - } - elseif (!($null -eq ($StartRead -as [DateTime]))) { - #### DATE TIME Provided #### - - # Convert the input to a UTC date time object + Write-Information "Start Date (UTC): $StartDate" + + } elseif (!($null -eq ($StartRead -as [DateTime]))) { + + #### DateTime Provided #### [DateTime]$StartDate = (Get-Date $StartRead).ToUniversalTime().Date - - # Test to make sure the date time is not further back than 365 and < today - # I DONT KNOW WHAT TO CHANGE HERE - if ($StartDate -ge ((Get-date).ToUniversalTime().AddDays(-365).Date) -and ($StartDate -le (Get-Date).ToUniversalTime().Date)) { - #Valid Date do nothing + + # Validate the date + if ($StartDate -lt ((Get-Date).ToUniversalTime().AddDays(-$MaxDaysToGoBack))) { + Write-Warning "The date entered exceeds your license retention period of $MaxDaysToGoBack days." + $Proceed = Read-Host "Press ENTER to proceed or type 'R' to re-enter the date" + if ($Proceed -eq 'R') { $StartDate = $null; continue } } - else { - Write-Information ("Date provided beyond acceptable range of 365 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(-365))) { + Write-Warning "The date cannot exceed 365 days. Setting to the maximum limit of 365 days." + [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-365)).Date } - } - else { - Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop + + Write-Information "Start Date (UTC): $StartDate" + + } else { + Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop } } + # End date logic remains unchanged if ($null -eq $EndDate) { - # Read in the end date - $EndRead = Read-Host "`nPlease Enter Last Day of Search Window (1-365, 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 + Write-Output "`nPlease specify the last day of the search window:" + Write-Output " - Enter a number of days to go back from today (1-365)" + Write-Output " - OR enter a specific date in MM/DD/YYYY format" + $EndRead = Read-Host "Default is today's date" + + + + # End date validation + if ($null -eq ($EndRead -as [DateTime])) { if ([string]::IsNullOrEmpty($EndRead)) { - [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(1)).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 + [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date + } else { + Write-Information "End Date (UTC): $EndRead days." + [DateTime]$EndDate = ((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 + Write-Error "StartDate cannot be more recent than EndDate" -ErrorAction Stop + } else { + Write-Information "End Date (UTC): $EndDate`n" } - else { - Write-Information ("End Date (UTC): " + $EndDate + "`n") - } - } - elseif (!($null -eq ($EndRead -as [DateTime]))) { - #### DATE TIME Provided #### + } elseif (!($null -eq ($EndRead -as [DateTime]))) { + [DateTime]$EndDate = (Get-Date $EndRead).ToUniversalTime().Date - # Convert the input to a UTC date time object - [DateTime]$EndDate = ((Get-Date $EndRead).ToUniversalTime().AddDays(1)).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 - } - 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 + Write-Warning "EndDate is earlier than StartDate. Setting EndDate to today." + [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date + } elseif ($EndDate -gt ((Get-Date).ToUniversalTime().AddDays(1))) { + Write-Warning "EndDate too far in the future. Setting EndDate to today." + [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date } - Write-Information ("End Date (UTC): " + $EndDate + "`n") - } - else { - Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop + Write-Information "End Date (UTC): $EndDate`n" + } else { + Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop } } From 95ba314d498adca39fc8a7ee438e545420b906d2 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 18:39:21 -0500 Subject: [PATCH 08/23] Update multiple functions to update the user with completion status. --- Hawk/functions/General/Update-HawkModule.ps1 | 229 ++++++++------- ...t-HawkTenantAdminEmailForwardingChange.ps1 | 5 +- .../Get-HawkTenantAdminInboxRuleCreation.ps1 | 7 +- ...t-HawkTenantAdminInboxRuleModification.ps1 | 7 +- .../Get-HawkTenantAdminInboxRuleRemoval.ps1 | 7 +- ...HawkTenantAdminMailboxPermissionChange.ps1 | 3 +- .../Tenant/Get-HawkTenantAuthHistory.ps1 | 3 +- .../Tenant/Get-HawkTenantAzureAppAuditLog.ps1 | 5 +- .../Tenant/Get-HawkTenantDomainActivity.ps1 | 3 +- .../Get-HawkTenantEDiscoveryConfiguration.ps1 | 6 +- .../Tenant/Get-HawkTenantEDiscoveryLog.ps1 | 3 +- .../Tenant/Get-HawkTenantEntraIDAdmin.ps1 | 3 +- .../Tenant/Get-HawkTenantEntraIDUser.ps1 | 3 +- .../Tenant/Get-HawkTenantRbacChange.ps1 | 3 +- .../Tenant/Search-HawkTenantActivityByIP.ps1 | 3 +- .../Tenant/Start-HawkTenantInvestigation.ps1 | 2 - .../functions/User/Get-HawkUserAdminAudit.ps1 | 3 +- .../User/Get-HawkUserEmailForwarding.ps1 | 3 +- .../functions/User/Get-HawkUserHiddenRule.ps1 | 5 +- Hawk/functions/User/Get-HawkUserInboxRule.ps1 | 5 +- .../User/Get-HawkUserMailboxAuditing.ps1 | 6 +- .../User/Get-HawkUserMobileDevice.ps1 | 3 +- Hawk/functions/User/Get-HawkUserPWNCheck.ps1 | 3 +- .../User/Start-HawkUserInvestigation.ps1 | 1 - .../PSScriptAnalyzerSettings.psd1 | 2 + Hawk/internal/functions/Get-IPGeolocation.ps1 | 4 +- .../functions/Initialize-HawkGlobalObject.ps1 | 268 +++++++++--------- Hawk/internal/functions/Out-LogFile.ps1 | 67 ++++- .../functions/Out-MultipleFileType.ps1 | 2 +- .../internal/functions/Test-EXOConnection.ps1 | 2 +- .../functions/Test-GraphConnection.ps1 | 2 +- Hawk/internal/functions/Write-HawkBanner.ps1 | 10 + .../Write-HawkConfigurationComplete.ps1 | 25 ++ 33 files changed, 416 insertions(+), 287 deletions(-) create mode 100644 Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 diff --git a/Hawk/functions/General/Update-HawkModule.ps1 b/Hawk/functions/General/Update-HawkModule.ps1 index 2d8e22f..c4c7ed8 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 - ) - - # 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 + <# + .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 { + 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 + if ($PSCmdlet.ShouldProcess("Hawk Module", "Update 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 diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 index 097a8c1..6809f9b 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 @@ -199,8 +199,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..5add1dc 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 @@ -85,8 +85,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 +96,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..b91c5d1 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 @@ -88,8 +88,8 @@ Function Get-HawkTenantAdminInboxRuleModification { 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 + Out-LogFile "Found suspicious rule modification: '$($rule.Param_Name)'" -Notice + } } } @@ -99,7 +99,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..25fd78e 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 @@ -88,8 +88,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 +99,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..f46d86d 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 @@ -98,7 +98,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-HawkTenantAuthHistory.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 index 9d195fd..0ecdea5 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAuthHistory.ps1 @@ -64,7 +64,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..c00c705 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 @@ -41,7 +41,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 +68,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-HawkTenantDomainActivity.ps1 b/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 index e512b61..119c83a 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantDomainActivity.ps1 @@ -36,7 +36,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..d342632 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 @@ -116,11 +116,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..73607e8 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 @@ -50,7 +50,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-HawkTenantEntraIDAdmin.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 index b1f32a8..c278470 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEntraIDAdmin.ps1 @@ -82,7 +82,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..17082fa 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 @@ -56,7 +56,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-HawkTenantRbacChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 index 5402280..ca050b8 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantRbacChange.ps1 @@ -100,7 +100,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..ccf962d 100644 --- a/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 +++ b/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 @@ -63,7 +63,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..f3c398d 100644 --- a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 +++ b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 @@ -33,8 +33,6 @@ [CmdletBinding(SupportsShouldProcess)] param() - Write-HawkBanner - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { Initialize-HawkGlobalObject } diff --git a/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 b/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 index 401f595..d829fed 100644 --- a/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 +++ b/Hawk/functions/User/Get-HawkUserAdminAudit.ps1 @@ -81,7 +81,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-HawkUserEmailForwarding.ps1 b/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 index ef2320a..294a6e5 100644 --- a/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 +++ b/Hawk/functions/User/Get-HawkUserEmailForwarding.ps1 @@ -55,7 +55,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..b8f477d 100644 --- a/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 +++ b/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 @@ -61,7 +61,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 +131,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..9040e42 100644 --- a/Hawk/functions/User/Get-HawkUserInboxRule.ps1 +++ b/Hawk/functions/User/Get-HawkUserInboxRule.ps1 @@ -54,7 +54,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..fbff7e3 100644 --- a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 +++ b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 @@ -118,7 +118,8 @@ $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 @@ -139,7 +140,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-HawkUserMobileDevice.ps1 b/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 index 20ef3b9..a36249e 100644 --- a/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 +++ b/Hawk/functions/User/Get-HawkUserMobileDevice.ps1 @@ -48,7 +48,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..c66c3ea 100644 --- a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 +++ b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 @@ -34,7 +34,8 @@ " # get the access key from the user - $hibpkey = Read-Host "haveibeenpwned.com apikey" + Out-LogFile "haveibeenpwned.com apikey" -isPrompt -NoNewLine + $hibpkey = Read-Host } }#End of BEGIN block diff --git a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 index 3fafd50..a5ef865 100644 --- a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 +++ b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 @@ -50,7 +50,6 @@ [array]$UserPrincipalName ) - 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-IPGeolocation.ps1 b/Hawk/internal/functions/Get-IPGeolocation.ps1 index dbf1ff5..1e5c7f7 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 # add the access key to the appdata file Add-HawkAppData -name access_key -Value $Accesskey diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 8cd33bb..2afe67e 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -48,251 +48,283 @@ Function Test-LoggingPath { param([string]$PathToTest) - + + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + # 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 UTC] [!] - 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 UTC] [!] - Directory $PathToTest Not Found" Return $false } } - + Function New-LoggingFolder { [CmdletBinding(SupportsShouldProcess)] param([string]$RootPath) - - # Create a folder ID based on UTC date - [string]$TenantName = (Get-MGDomain | Where-Object { $_.isDefault }).ID + + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + + # Test Graph connection silently first + $null = Test-GraphConnection 2>$null + + # 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_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" + Write-Information "[$timestamp UTC] [+] - Path $FullOutputPath already exists" } - # If it is not there make it else { - Write-Information ("Creating subfolder with name " + $FullOutputPath) - $null = New-Item $FullOutputPath -ItemType Directory + Write-Information "[$timestamp UTC] [-] - Creating subfolder $FullOutputPath" + $null = New-Item $FullOutputPath -ItemType Directory -ErrorAction Stop } - + Return $FullOutputPath } - + Function Set-LoggingPath { [CmdletBinding(SupportsShouldProcess)] param ([string]$Path) - - # If no value of Path is provided prompt and gather from the user + + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + + # 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 UTC] [>] - Please provide an output directory" + + # If the input is null or empty, prompt again + if ([string]::IsNullOrEmpty($UserPath)) { + Write-Host "[$timestamp UTC] [-] - 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-Host "[$timestamp UTC] [!] - Error: Path not a valid directory: $UserPath" -ForegroundColor Red $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 UTC] [!] - Error: Provided path is not a valid directory: $Path" -ErrorAction Stop } } - + 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 } } + + ### Main ### $InformationPreference = "Continue" 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 2>$null + } + 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" - #### 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 - } + + + + # If the global variable Hawk doesn't exist or we have -force then set the variable up + Out-LogFile -string "Setting Up initial Hawk environment variable" -NoDisplay try { $LicenseInfo = Test-LicenseType $MaxDaysToGoBack = $LicenseInfo.RetentionPeriod $LicenseType = $LicenseInfo.LicenseType - - Write-Information "Detecting M365 license type to determine maximum log retention period" - Write-Information "M365 License type detected: $LicenseType" - Write-Information "Max log retention: $MaxDaysToGoBack days" - + + 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 { - Write-Information "Failed to detect license type. Max days of log retention is unknown." + Out-LogFile -string "Failed to detect license type. Max days of log retention is unknown." -Information $MaxDaysToGoBack = 90 $LicenseType = "Unknown" } - + # Ensure MaxDaysToGoBack does not exceed 365 days if ($MaxDaysToGoBack -gt 365) { $MaxDaysToGoBack = 365 } - + # Prompt for Start Date if not set while ($null -eq $StartDate) { - + # Read input from user - Write-Output "`nPlease specify the first day of the search window:" - Write-Output " - Enter a number of days to go back (1-$MaxDaysToGoBack)" - Write-Output " - OR enter a date in MM/DD/YYYY format" - $StartRead = Read-Host "Default is 90 days back" - + 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 + + # Determine if input is a valid date if ($null -eq ($StartRead -as [DateTime])) { - + #### Not a DateTime #### - if ([string]::IsNullOrEmpty($StartRead)) { - $StartRead = 90 + if ([string]::IsNullOrEmpty($StartRead)) { + $StartRead = 90 } - + # Validate the entered days back if ($StartRead -gt $MaxDaysToGoBack) { - Write-Warning "You have entered a time frame greater than your license allows ($MaxDaysToGoBack days)." + Out-LogFile -string "You have entered a time frame greater than your license allows ($MaxDaysToGoBack days)." -isWarning $Proceed = Read-Host "Press ENTER to proceed or type 'R' to re-enter the value" if ($Proceed -eq 'R') { continue } } - + if ($StartRead -gt 365) { - Write-Warning "Log retention cannot exceed 365 days. Setting retention to 365 days." + Out-LogFile -string "Log retention cannot exceed 365 days. Setting retention to 365 days." -isWarning $StartRead = 365 } - + # 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 -NoNewLine + } elseif (!($null -eq ($StartRead -as [DateTime]))) { - + #### DateTime Provided #### [DateTime]$StartDate = (Get-Date $StartRead).ToUniversalTime().Date - + # Validate the date if ($StartDate -lt ((Get-Date).ToUniversalTime().AddDays(-$MaxDaysToGoBack))) { - Write-Warning "The date entered exceeds your license retention period of $MaxDaysToGoBack days." + Out-LogFile -string "The date entered exceeds your license retention period of $MaxDaysToGoBack days." -isWarning $Proceed = Read-Host "Press ENTER to proceed or type 'R' to re-enter the date" if ($Proceed -eq 'R') { $StartDate = $null; continue } } - + if ($StartDate -lt ((Get-Date).ToUniversalTime().AddDays(-365))) { - Write-Warning "The date cannot exceed 365 days. Setting to the maximum limit of 365 days." + 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 } - - Write-Information "Start Date (UTC): $StartDate" - + + 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 + break } } # End date logic remains unchanged if ($null -eq $EndDate) { - Write-Output "`nPlease specify the last day of the search window:" - Write-Output " - Enter a number of days to go back from today (1-365)" - Write-Output " - OR enter a specific date in MM/DD/YYYY format" - $EndRead = Read-Host "Default is today's date" - - + 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 # End date validation if ($null -eq ($EndRead -as [DateTime])) { if ([string]::IsNullOrEmpty($EndRead)) { [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date } else { - Write-Information "End Date (UTC): $EndRead days." + Out-LogFile -string "End Date (UTC): $EndRead days." -Information [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays(-($EndRead - 1))).Date } if ($StartDate -gt $EndDate) { - Write-Error "StartDate cannot be more recent than EndDate" -ErrorAction Stop + Out-LogFile -string "StartDate cannot be more recent than EndDate" -isError } else { - Write-Information "End Date (UTC): $EndDate`n" + 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) { - Write-Warning "EndDate is earlier than StartDate. Setting EndDate to today." + 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))) { - Write-Warning "EndDate too far in the future. Setting EndDate to today." + Out-LogFile -string "EndDate too far in the future. Setting EndDate to today." -isWarning [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date } - Write-Information "End Date (UTC): $EndDate`n" + 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 } } @@ -303,35 +335,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 - MaxDays = $MaxDaysToGoBack - 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 9ba305a..c6f22d8 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 @@ -106,26 +136,26 @@ $ScreenOutput = -not $NoDisplay $LogOutput = $true - # Get the current date - [string]$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + # Get the current date in UTC + [string]$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss UTC") [string]$logstring = "" # Build the log string based on the type of message if ($action) { - $logstring = "[$timestamp] - [ACTION] - $string" + $logstring = "[$timestamp] [-] - $string" } elseif ($isError) { - $logstring = "[$timestamp] - [ERROR] - $string" + $logstring = "[$timestamp] [!] - ERROR: $string" } elseif ($notice) { - $logstring = "[$timestamp] - [INVESTIGATE] - $string" + $logstring = "[$timestamp] [*] - INVESTIGATE: $string" # Write to the investigation file [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt" $logstring | Out-File -FilePath $InvestigateFile -Append } elseif ($silentnotice) { - $logstring = "[$timestamp] - [INVESTIGATE] - Additional Information: $string" + $logstring = "[$timestamp] [*] - Additional Information: $string" # Write to the investigation file [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt" @@ -136,7 +166,13 @@ $LogOutput = $false } elseif ($Information) { - $logstring = "[$timestamp] - [INFO] - $string" + $logstring = "[$timestamp] [+] - $string" + } + elseif ($isWarning) { + $logstring = "[$timestamp] [~] - WARNING: $string" + } + elseif ($isPrompt) { + $logstring = "[$timestamp] [>] - $string" } else { $logstring = "[$timestamp] - $string" @@ -149,6 +185,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-EXOConnection.ps1 b/Hawk/internal/functions/Test-EXOConnection.ps1 index 2a2a0ad..57893da 100644 --- a/Hawk/internal/functions/Test-EXOConnection.ps1 +++ b/Hawk/internal/functions/Test-EXOConnection.ps1 @@ -20,7 +20,7 @@ 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" -Information + Out-LogFile "Not Connected to Exchange Online" -action Out-LogFile "Connecting to EXO using Exchange Online Module" -Action Connect-ExchangeOnline } diff --git a/Hawk/internal/functions/Test-GraphConnection.ps1 b/Hawk/internal/functions/Test-GraphConnection.ps1 index 2b377b3..d3f879f 100644 --- a/Hawk/internal/functions/Test-GraphConnection.ps1 +++ b/Hawk/internal/functions/Test-GraphConnection.ps1 @@ -25,7 +25,7 @@ Function Test-GraphConnection { } else { # $Hawk exists, so we can safely use Out-LogFile - Out-LogFile -String "Connecting to MGGraph using MGGraph Module" -Action + Write-Output "Connecting to MGGraph using MGGraph Module" } Connect-MGGraph 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..151542c --- /dev/null +++ b/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 @@ -0,0 +1,25 @@ +Function Write-HawkConfigurationComplete { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [PSCustomObject]$Hawk + ) + + Out-LogFile "Configuration Complete!" -Information + Out-LogFile "Your Hawk environment is now set up with the following settings:" -action + Out-LogFile ("Hawk Version: " + (Get-Module Hawk).version) -Action + + # 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) -action + } + + Out-LogFile "`Happy hunting! 🦅`n" -action +} From de17ffdb8ab77c5e142f0b6909e949012d1e1205 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 18:40:09 -0500 Subject: [PATCH 09/23] Update multiple functions to update the user with completion status. --- Hawk/internal/functions/Out-LogFile.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hawk/internal/functions/Out-LogFile.ps1 b/Hawk/internal/functions/Out-LogFile.ps1 index c6f22d8..946c56c 100644 --- a/Hawk/internal/functions/Out-LogFile.ps1 +++ b/Hawk/internal/functions/Out-LogFile.ps1 @@ -169,7 +169,7 @@ $logstring = "[$timestamp] [+] - $string" } elseif ($isWarning) { - $logstring = "[$timestamp] [~] - WARNING: $string" + $logstring = "[$timestamp] [-] - WARNING: $string" } elseif ($isPrompt) { $logstring = "[$timestamp] [>] - $string" From e0684f3a0a25064f7685c27252963cf53a90feab Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 18:42:00 -0500 Subject: [PATCH 10/23] Update multiple functions to update the user with completion status. --- .../Write-HawkConfigurationComplete.ps1 | 83 ++++++++++++++----- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 b/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 index 151542c..822fb21 100644 --- a/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 +++ b/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 @@ -1,25 +1,62 @@ Function Write-HawkConfigurationComplete { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [PSCustomObject]$Hawk - ) - - Out-LogFile "Configuration Complete!" -Information - Out-LogFile "Your Hawk environment is now set up with the following settings:" -action - Out-LogFile ("Hawk Version: " + (Get-Module Hawk).version) -Action - - # 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 + <# + .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:" -action + Out-LogFile ("Hawk Version: " + (Get-Module Hawk).version) -Action + + # 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) -action + } + + Out-LogFile "`Happy hunting! 🦅`n" -action } - - Out-LogFile -string ("{0} = {1}" -f $prop.Name, $value) -action - } - - Out-LogFile "`Happy hunting! 🦅`n" -action -} + } \ No newline at end of file From 292862c57962f1a8eecb4890325503b71fc28f88 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 18:45:28 -0500 Subject: [PATCH 11/23] Update multiple functions to update the user with completion status. --- Hawk/functions/General/Update-HawkModule.ps1 | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Hawk/functions/General/Update-HawkModule.ps1 b/Hawk/functions/General/Update-HawkModule.ps1 index c4c7ed8..93a569c 100644 --- a/Hawk/functions/General/Update-HawkModule.ps1 +++ b/Hawk/functions/General/Update-HawkModule.ps1 @@ -41,17 +41,17 @@ # If we can then look for an updated version of the module else { - Write-Output "Checking for latest version online" + 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] - Write-Output ("Found Version " + $onlineversion.version + " Online") - + Out-LogFile ("Found Version " + $onlineversion.version + " Online") -Information + if ($null -eq $onlineversion){ - Write-Output "[ERROR] - Unable to check Hawk version in Gallery" + Out-LogFile "[ERROR] - Unable to check Hawk version in Gallery" -isError } 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) + 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" @@ -71,7 +71,7 @@ } # If the versions match then we don't need to upgrade else { - Write-Output "Latest Version Installed" + Out-LogFile "Latest Version Installed" -Information } } } @@ -82,9 +82,9 @@ If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { # Update the module if ($PSCmdlet.ShouldProcess("Hawk Module", "Update module")) { - Write-Output "Downloading Updated Hawk Module" + Out-LogFile "Downloading Updated Hawk Module" -Action Update-Module Hawk -Force - Write-Output "Update Finished" + 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 @@ -92,11 +92,11 @@ # 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" + 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 - Write-Warning "Updated Hawk Module loaded in New PowerShell Window. `nPlease Close this Window." + Out-LogFile "Updated Hawk Module loaded in New PowerShell Window. Please Close this Window." -Notice break } } @@ -104,19 +104,19 @@ # 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" + Out-LogFile "Starting Elevated Prompt" -Action 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" + 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" - Write-Warning "Updated Hawk Module loaded in New PowerShell Window. `nPlease Close this Window." + 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 { - Write-Output "Skipping Upgrade" + Out-LogFile "Skipping Upgrade" -Action } } \ No newline at end of file From 2d42e4052e338ab50c51afbd0c0049601b166636 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 19:27:47 -0500 Subject: [PATCH 12/23] Update Get-HawkUserMailBoxAuditing to inform user of long wait periods. --- .../User/Get-HawkUserMailboxAuditing.ps1 | 2 + .../User/Start-HawkUserInvestigation.ps1 | 72 +++++++++---------- .../functions/Get-SimpleUnifiedAuditLog.ps1 | 48 +++++++++---- 3 files changed, 74 insertions(+), 48 deletions(-) diff --git a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 index fbff7e3..bf1a0e6 100644 --- a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 +++ b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 @@ -124,11 +124,13 @@ # 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 events, this can take a while..." -action # Process and output flattened data $ParsedGroupLogs = $groupLogs | Get-SimpleUnifiedAuditLog diff --git a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 index a5ef865..2a332f0 100644 --- a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 +++ b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 @@ -69,50 +69,50 @@ 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-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-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-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 - # } + if ($PSCmdlet.ShouldProcess("Running Get-HawkUserMobileDevice for $User")) { + Out-LogFile "Running Get-HawkUserMobileDevice" -Action + Get-HawkUserMobileDevice -User $User + } } } } 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 { From 6a0bfe9a4eddaee260c034d6bd1aca1304315d56 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Fri, 10 Jan 2025 19:28:10 -0500 Subject: [PATCH 13/23] Update Get-HawkUserMailBoxAuditing to inform user of long wait periods. --- Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 index bf1a0e6..e0e73bf 100644 --- a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 +++ b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 @@ -130,7 +130,7 @@ if ($groupLogs.Count -gt 0) { Out-LogFile ("Found " + $groupLogs.Count + " ExchangeItemGroup events.") -Information - Out-LogFile "Processing all events, this can take a while..." -action + Out-LogFile "Processing all ExchangeItemGroup events, this can take a while..." -action # Process and output flattened data $ParsedGroupLogs = $groupLogs | Get-SimpleUnifiedAuditLog From 91ce086001f787515cd995143117e3dd7ed4d42a Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sat, 11 Jan 2025 06:10:41 -0500 Subject: [PATCH 14/23] Add check at beginning of intialize-hawkglobalobject to check for half initlized hawk object. Revert back to word tagging instead of symbol tagging. --- Hawk/changelog.md | 7 ++ .../functions/Initialize-HawkGlobalObject.ps1 | 66 ++++++++++++++----- Hawk/internal/functions/Out-LogFile.ps1 | 14 ++-- .../internal/functions/Test-EXOConnection.ps1 | 2 +- .../functions/Test-GraphConnection.ps1 | 7 +- .../Write-HawkConfigurationComplete.ps1 | 6 +- 6 files changed, 71 insertions(+), 31 deletions(-) diff --git a/Hawk/changelog.md b/Hawk/changelog.md index 223a63c..976fe36 100644 --- a/Hawk/changelog.md +++ b/Hawk/changelog.md @@ -88,3 +88,10 @@ - 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 + + 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/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 2afe67e..ec3e79e 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -46,6 +46,30 @@ [string]$FilePath ) + # Validation to handle interrupted initialization states + # If Hawk initialization was interrupted (e.g., by CTRL+C during path input), + # it can leave behind a partially initialized Hawk object. This causes + # recursion errors on subsequent runs due to the partial state. + # + # Two cleanup scenarios: + # 1. Force parameter: Always remove any existing Hawk object + # 2. Incomplete object: Remove if missing essential properties + # (FilePath, StartDate, EndDate) + # + # This validation runs before any function definitions or calls to prevent + # call depth overflow errors from recursion. + # Attempted to put in its own function, but resulted in issues due to order of the call stack + + if ($Force) { + Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue + } + + # Then check if the Hawk object exists but is incomplete + if ($null -ne (Get-Variable -Name Hawk -ErrorAction SilentlyContinue) -and + ($null -eq $Hawk.FilePath -or $null -eq $Hawk.StartDate -or $null -eq $Hawk.EndDate)) { + Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue + } + Function Test-LoggingPath { param([string]$PathToTest) @@ -59,13 +83,13 @@ } # If it is not a folder return false and write an error else { - Write-Information "[$timestamp UTC] [!] - Path provided $PathToTest was not found to be a folder." + Write-Information "[$timestamp UTC] - [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 "[$timestamp UTC] [!] - Directory $PathToTest Not Found" + Write-Information "[$timestamp UTC] - [ERROR] - Directory $PathToTest Not Found" Return $false } } @@ -75,9 +99,20 @@ param([string]$RootPath) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + + Write-Information "[$timestamp UTC] - [ACTION] - Connecting to Microsoft Graph" - # Test Graph connection silently first - $null = Test-GraphConnection 2>$null + try { + # Test Graph connection + $null = Test-GraphConnection + + # If successful, display a success message + Write-Information "[$timestamp UTC] - [INFO] - Connected to Microsoft Graph Successfully" + } + catch { + # If it fails, display an error message + Write-Error "[$timestamp UTC] - [ERROR] - Failed to connect to Microsoft Graph" + } # Get tenant name $TenantName = (Get-MGDomain -ErrorAction Stop | Where-Object { $_.isDefault }).ID @@ -86,10 +121,10 @@ $FullOutputPath = Join-Path $RootPath $FolderID if (Test-Path $FullOutputPath) { - Write-Information "[$timestamp UTC] [+] - Path $FullOutputPath already exists" + Write-Information "[$timestamp UTC] - [ERROR] - Path $FullOutputPath already exists" } else { - Write-Information "[$timestamp UTC] [-] - Creating subfolder $FullOutputPath" + Write-Information "[$timestamp UTC] - [ACTION] - Creating subfolder $FullOutputPath" $null = New-Item $FullOutputPath -ItemType Directory -ErrorAction Stop } @@ -107,11 +142,11 @@ # Setup a while loop to get a valid path Do { # Ask the user for the output path - [string]$UserPath = Read-Host "[$timestamp UTC] [>] - Please provide an output directory" + [string]$UserPath = Read-Host "[$timestamp UTC] - [PROMPT] - Please provide an output directory" # If the input is null or empty, prompt again if ([string]::IsNullOrEmpty($UserPath)) { - Write-Host "[$timestamp UTC] [-] - Directory path cannot be empty. Please enter in a new path." + Write-Host "[$timestamp UTC] - [INFO] - Directory path cannot be empty. Please enter in a new path." $ValidPath = $false } # If the path is valid, create the subfolder @@ -121,7 +156,7 @@ } # If the path is invalid, prompt again else { - Write-Host "[$timestamp UTC] [!] - Error: Path not a valid directory: $UserPath" -ForegroundColor Red + Write-Host "[$timestamp UTC] - [ERROR] - Path not a valid directory: $UserPath" -ForegroundColor Red $ValidPath = $false } } @@ -135,7 +170,7 @@ } # If the provided path fails validation, stop the process else { - Write-Error "[$timestamp UTC] [!] - Error: Provided path is not a valid directory: $Path" -ErrorAction Stop + Write-Error "[$timestamp UTC] - [ERROR] - Provided path is not a valid directory: $Path" -ErrorAction Stop } } @@ -152,8 +187,6 @@ } } - - ### Main ### $InformationPreference = "Continue" @@ -173,7 +206,7 @@ # 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 2>$null + $Hawk.FilePath = Set-LoggingPath -ErrorAction Stop } else { $Hawk.FilePath = Set-LoggingPath -path $FilePath -ErrorAction Stop 2>$null @@ -198,11 +231,8 @@ # Test Graph connection Out-LogFile "Testing Graph Connection" -Action - Test-GraphConnection - - - + Test-GraphConnection # If the global variable Hawk doesn't exist or we have -force then set the variable up Out-LogFile -string "Setting Up initial Hawk environment variable" -NoDisplay @@ -292,7 +322,7 @@ 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 " OR enter a specific date in MM/DD/YYYY format" -isPrompt Out-LogFile " Default is today's date:" -isPrompt -NoNewLine $EndRead = Read-Host diff --git a/Hawk/internal/functions/Out-LogFile.ps1 b/Hawk/internal/functions/Out-LogFile.ps1 index 946c56c..fd63146 100644 --- a/Hawk/internal/functions/Out-LogFile.ps1 +++ b/Hawk/internal/functions/Out-LogFile.ps1 @@ -142,20 +142,20 @@ # Build the log string based on the type of message if ($action) { - $logstring = "[$timestamp] [-] - $string" + $logstring = "[$timestamp] - [ACTION] - $string" } elseif ($isError) { - $logstring = "[$timestamp] [!] - ERROR: $string" + $logstring = "[$timestamp] - [ERROR] - $string" } elseif ($notice) { - $logstring = "[$timestamp] [*] - INVESTIGATE: $string" + $logstring = "[$timestamp] - [INVESTIGATE] - $string" # Write to the investigation file [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt" $logstring | Out-File -FilePath $InvestigateFile -Append } elseif ($silentnotice) { - $logstring = "[$timestamp] [*] - Additional Information: $string" + $logstring = "[$timestamp] - [INVESTIGATE] - Additional Information: $string" # Write to the investigation file [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt" @@ -166,13 +166,13 @@ $LogOutput = $false } elseif ($Information) { - $logstring = "[$timestamp] [+] - $string" + $logstring = "[$timestamp] - [INFO] - $string" } elseif ($isWarning) { - $logstring = "[$timestamp] [-] - WARNING: $string" + $logstring = "[$timestamp] - [WARNING] - $string" } elseif ($isPrompt) { - $logstring = "[$timestamp] [>] - $string" + $logstring = "[$timestamp] - [PROMPT] - $string" } else { $logstring = "[$timestamp] - $string" diff --git a/Hawk/internal/functions/Test-EXOConnection.ps1 b/Hawk/internal/functions/Test-EXOConnection.ps1 index 57893da..2a2a0ad 100644 --- a/Hawk/internal/functions/Test-EXOConnection.ps1 +++ b/Hawk/internal/functions/Test-EXOConnection.ps1 @@ -20,7 +20,7 @@ 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" -action + Out-LogFile "Not Connected to Exchange Online" -Information Out-LogFile "Connecting to EXO using Exchange Online Module" -Action Connect-ExchangeOnline } diff --git a/Hawk/internal/functions/Test-GraphConnection.ps1 b/Hawk/internal/functions/Test-GraphConnection.ps1 index d3f879f..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 - Write-Output "Connecting to MGGraph using MGGraph Module" + Write-Output "[$timestamp UTC] - [ACTION] - Connecting to Microsoft Graph API" } Connect-MGGraph diff --git a/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 b/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 index 822fb21..fab4123 100644 --- a/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 +++ b/Hawk/internal/functions/Write-HawkConfigurationComplete.ps1 @@ -42,8 +42,8 @@ process { Out-LogFile "Configuration Complete!" -Information - Out-LogFile "Your Hawk environment is now set up with the following settings:" -action - Out-LogFile ("Hawk Version: " + (Get-Module Hawk).version) -Action + 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) { @@ -54,7 +54,7 @@ $prop.Value } - Out-LogFile -string ("{0} = {1}" -f $prop.Name, $value) -action + Out-LogFile -string ("{0} = {1}" -f $prop.Name, $value) -Information } Out-LogFile "`Happy hunting! 🦅`n" -action From 7c60ff4d020edff0b4bfd63c34fd23e4fe8f2f94 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sat, 11 Jan 2025 06:34:44 -0500 Subject: [PATCH 15/23] Add check at beginning of intialize-hawkglobalobject to check for half initlized hawk object. Revert back to word tagging instead of symbol tagging. --- Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index ec3e79e..5fbd617 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -197,9 +197,9 @@ # Create the global $Hawk variable immediately with minimal properties $Global:Hawk = [PSCustomObject]@{ FilePath = $null # Will be set shortly - DaysToLookBack = $null - StartDate = $null - EndDate = $null + DaysToLookBack = 90 + StartDate = ((Get-Date).ToUniversalTime().AddDays(-90)).Date # 90 days back in UTC + EndDate = (Get-Date).ToUniversalTime().Date # Today's date in UTC WhenCreated = $null } @@ -234,9 +234,6 @@ Test-GraphConnection - # If the global variable Hawk doesn't exist or we have -force then set the variable up - Out-LogFile -string "Setting Up initial Hawk environment variable" -NoDisplay - try { $LicenseInfo = Test-LicenseType $MaxDaysToGoBack = $LicenseInfo.RetentionPeriod From 70b992a97bee05769d82d56b2d7107b80bf8b6af Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sat, 11 Jan 2025 10:11:36 -0500 Subject: [PATCH 16/23] Remove bug where program crashes if uses CTRL Cs at point of being asked for start or end date. --- .../functions/Initialize-HawkGlobalObject.ps1 | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 5fbd617..65d78fb 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -60,13 +60,32 @@ # call depth overflow errors from recursion. # Attempted to put in its own function, but resulted in issues due to order of the call stack +# Validation to handle interrupted initialization states +# If Hawk initialization was interrupted (e.g., by CTRL+C during path input), +# it can leave behind a partially initialized Hawk object. This causes +# recursion errors on subsequent runs due to the partial state. +# +# Two cleanup scenarios: +# 1. Force parameter: Always remove any existing Hawk object +# 2. Incomplete object: Remove if missing essential properties +# (FilePath, StartDate, EndDate) +# +# This validation runs before any function definitions or calls to prevent +# call depth overflow errors from recursion. +# Attempted to put in its own function, but resulted in issues due to order of the call stack + if ($Force) { Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue } # Then check if the Hawk object exists but is incomplete if ($null -ne (Get-Variable -Name Hawk -ErrorAction SilentlyContinue) -and - ($null -eq $Hawk.FilePath -or $null -eq $Hawk.StartDate -or $null -eq $Hawk.EndDate)) { + ($null -eq $Hawk.FilePath -or + $null -eq $Hawk.StartDate -or + $null -eq $Hawk.EndDate -or + # Add additional checks for partially initialized date properties + ($Hawk.PSObject.Properties.Name -contains 'StartDate' -and $null -eq $Hawk.StartDate) -or + ($Hawk.PSObject.Properties.Name -contains 'EndDate' -and $null -eq $Hawk.EndDate))) { Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue } From 74d10dd0efdb1b095fdbc6e49363982b93d8695f Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sat, 11 Jan 2025 11:14:19 -0500 Subject: [PATCH 17/23] Add test-hawkglobalobject function and have every public function call it before executing to ensure that hawk global object is fully configured prior to hawk execution. --- ...t-HawkTenantAdminEmailForwardingChange.ps1 | 5 + .../Get-HawkTenantAdminInboxRuleCreation.ps1 | 5 + ...t-HawkTenantAdminInboxRuleModification.ps1 | 6 + .../Get-HawkTenantAdminInboxRuleRemoval.ps1 | 5 + ...HawkTenantAdminMailboxPermissionChange.ps1 | 6 + ...et-HawkTenantAppAndSPNCredentialDetail.ps1 | 3 +- .../Tenant/Get-HawkTenantAuditLog.ps1 | 6 +- .../Tenant/Get-HawkTenantAuthHistory.ps1 | 13 +- .../Tenant/Get-HawkTenantAzureAppAuditLog.ps1 | 10 +- .../Tenant/Get-HawkTenantConfiguration.ps1 | 7 + .../Tenant/Get-HawkTenantConsentGrant.ps1 | 5 + .../Tenant/Get-HawkTenantDomainActivity.ps1 | 7 + .../Get-HawkTenantEDiscoveryConfiguration.ps1 | 4 +- .../Tenant/Get-HawkTenantEDiscoveryLog.ps1 | 7 + .../Tenant/Get-HawkTenantEXOAdmin.ps1 | 5 + .../Tenant/Get-HawkTenantEntraIDAdmin.ps1 | 5 +- .../Tenant/Get-HawkTenantEntraIDUser.ps1 | 5 +- .../Tenant/Get-HawkTenantInboxRule.ps1 | 5 + .../Get-HawkTenantMailItemsAccessed.ps1 | 5 + .../Tenant/Get-HawkTenantRbacChange.ps1 | 5 + .../Tenant/Search-HawkTenantActivityByIP.ps1 | 6 + .../Tenant/Start-HawkTenantInvestigation.ps1 | 7 +- .../functions/User/Get-HawkUserAdminAudit.ps1 | 5 + .../User/Get-HawkUserAuthHistory.ps1 | 6 + Hawk/functions/User/Get-HawkUserAutoReply.ps1 | 5 + .../User/Get-HawkUserConfiguration.ps1 | 4 + .../User/Get-HawkUserEmailForwarding.ps1 | 6 + .../functions/User/Get-HawkUserHiddenRule.ps1 | 6 + Hawk/functions/User/Get-HawkUserInboxRule.ps1 | 6 + .../User/Get-HawkUserMailboxAuditing.ps1 | 5 + .../User/Get-HawkUserMessageTrace.ps1 | 5 + .../User/Get-HawkUserMobileDevice.ps1 | 5 + Hawk/functions/User/Get-HawkUserPWNCheck.ps1 | 189 +++++++++--------- .../User/Start-HawkUserInvestigation.ps1 | 4 + .../functions/Initialize-HawkGlobalObject.ps1 | 51 ++--- .../functions/Text-HawkGlobalObject.ps1 | 37 ++++ 36 files changed, 314 insertions(+), 152 deletions(-) create mode 100644 Hawk/internal/functions/Text-HawkGlobalObject.ps1 diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminEmailForwardingChange.ps1 index 6809f9b..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. diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleCreation.ps1 index 5add1dc..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" diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 index b91c5d1..bb67e79 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" diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleRemoval.ps1 index 25fd78e..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" diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminMailboxPermissionChange.ps1 index f46d86d..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" 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 0ecdea5..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 diff --git a/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAzureAppAuditLog.ps1 index c00c705..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 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 5e5e682..5be6e33 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 119c83a..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 diff --git a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryConfiguration.ps1 index d342632..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" diff --git a/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEDiscoveryLog.ps1 index 73607e8..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" 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 c278470..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 diff --git a/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 index 17082fa..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 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 ca050b8..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 diff --git a/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 b/Hawk/functions/Tenant/Search-HawkTenantActivityByIP.ps1 index ccf962d..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" diff --git a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 index f3c398d..487bda2 100644 --- a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 +++ b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 @@ -33,9 +33,10 @@ [CmdletBinding(SupportsShouldProcess)] param() - 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 d829fed..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" 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 294a6e5..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" diff --git a/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 b/Hawk/functions/User/Get-HawkUserHiddenRule.ps1 index b8f477d..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" diff --git a/Hawk/functions/User/Get-HawkUserInboxRule.ps1 b/Hawk/functions/User/Get-HawkUserInboxRule.ps1 index 9040e42..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" diff --git a/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 b/Hawk/functions/User/Get-HawkUserMailboxAuditing.ps1 index e0e73bf..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" 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 a36249e..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" diff --git a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 index c66c3ea..bd71dd5 100644 --- a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 +++ b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 @@ -1,93 +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 - Out-LogFile "haveibeenpwned.com apikey" -isPrompt -NoNewLine - $hibpkey = Read-Host - } - }#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 + } + }#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 2a332f0..d295c90 100644 --- a/Hawk/functions/User/Start-HawkUserInvestigation.ps1 +++ b/Hawk/functions/User/Start-HawkUserInvestigation.ps1 @@ -49,6 +49,10 @@ [Parameter(Mandatory = $true)] [array]$UserPrincipalName ) + # Check if Hawk object exists and is fully initialized + if (Test-HawkGlobalObject) { + Initialize-HawkGlobalObject + } # Check if the logging filepath is set diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 65d78fb..fd26ac9 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -46,47 +46,20 @@ [string]$FilePath ) - # Validation to handle interrupted initialization states - # If Hawk initialization was interrupted (e.g., by CTRL+C during path input), - # it can leave behind a partially initialized Hawk object. This causes - # recursion errors on subsequent runs due to the partial state. - # - # Two cleanup scenarios: - # 1. Force parameter: Always remove any existing Hawk object - # 2. Incomplete object: Remove if missing essential properties - # (FilePath, StartDate, EndDate) - # - # This validation runs before any function definitions or calls to prevent - # call depth overflow errors from recursion. - # Attempted to put in its own function, but resulted in issues due to order of the call stack - -# Validation to handle interrupted initialization states -# If Hawk initialization was interrupted (e.g., by CTRL+C during path input), -# it can leave behind a partially initialized Hawk object. This causes -# recursion errors on subsequent runs due to the partial state. -# -# Two cleanup scenarios: -# 1. Force parameter: Always remove any existing Hawk object -# 2. Incomplete object: Remove if missing essential properties -# (FilePath, StartDate, EndDate) -# -# This validation runs before any function definitions or calls to prevent -# call depth overflow errors from recursion. -# Attempted to put in its own function, but resulted in issues due to order of the call stack if ($Force) { Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue } - # Then check if the Hawk object exists but is incomplete - if ($null -ne (Get-Variable -Name Hawk -ErrorAction SilentlyContinue) -and - ($null -eq $Hawk.FilePath -or - $null -eq $Hawk.StartDate -or - $null -eq $Hawk.EndDate -or - # Add additional checks for partially initialized date properties - ($Hawk.PSObject.Properties.Name -contains 'StartDate' -and $null -eq $Hawk.StartDate) -or - ($Hawk.PSObject.Properties.Name -contains 'EndDate' -and $null -eq $Hawk.EndDate))) { - 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 { @@ -216,9 +189,9 @@ # Create the global $Hawk variable immediately with minimal properties $Global:Hawk = [PSCustomObject]@{ FilePath = $null # Will be set shortly - DaysToLookBack = 90 - StartDate = ((Get-Date).ToUniversalTime().AddDays(-90)).Date # 90 days back in UTC - EndDate = (Get-Date).ToUniversalTime().Date # Today's date in UTC + DaysToLookBack = $null + StartDate = $null + EndDate = $null WhenCreated = $null } diff --git a/Hawk/internal/functions/Text-HawkGlobalObject.ps1 b/Hawk/internal/functions/Text-HawkGlobalObject.ps1 new file mode 100644 index 0000000..f27c63a --- /dev/null +++ b/Hawk/internal/functions/Text-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 From 6a7a0be67106d9d592e03199a2492fafc5f99ff0 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sat, 11 Jan 2025 11:38:43 -0500 Subject: [PATCH 18/23] Append investigation folder with seconds to avoid duplicate folder creation error. --- .../functions/Initialize-HawkGlobalObject.ps1 | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index fd26ac9..a428092 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -91,36 +91,40 @@ param([string]$RootPath) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - - Write-Information "[$timestamp UTC] - [ACTION] - Connecting to Microsoft Graph" try { - # Test Graph connection - $null = Test-GraphConnection + # Test Graph connection first to see if we're already connected + try { + $null = Get-MgOrganization -ErrorAction Stop + Write-Information "[$timestamp UTC] - [INFO] - Already connected to Microsoft Graph" + } + catch { + # Only show connecting message if we actually need to connect + Write-Information "[$timestamp UTC] - [ACTION] - Connecting to Microsoft Graph" + $null = Test-GraphConnection + Write-Information "[$timestamp UTC] - [INFO] - Connected to Microsoft Graph Successfully" + } - # If successful, display a success message - Write-Information "[$timestamp UTC] - [INFO] - Connected to Microsoft Graph Successfully" - } - catch { - # If it fails, display an error message - Write-Error "[$timestamp UTC] - [ERROR] - Failed to connect to Microsoft Graph" - } + # 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") - # 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_HHmm") + $FullOutputPath = Join-Path $RootPath $FolderID - $FullOutputPath = Join-Path $RootPath $FolderID + if (Test-Path $FullOutputPath) { + Write-Information "[$timestamp UTC] - [ERROR] - Path $FullOutputPath already exists" + } + else { + Write-Information "[$timestamp UTC] - [ACTION] - Creating subfolder $FullOutputPath" + $null = New-Item $FullOutputPath -ItemType Directory -ErrorAction Stop + } - if (Test-Path $FullOutputPath) { - Write-Information "[$timestamp UTC] - [ERROR] - Path $FullOutputPath already exists" + Return $FullOutputPath } - else { - Write-Information "[$timestamp UTC] - [ACTION] - Creating subfolder $FullOutputPath" - $null = New-Item $FullOutputPath -ItemType Directory -ErrorAction Stop + catch { + # If it fails at any point, display an error message + Write-Error "[$timestamp UTC] - [ERROR] - Failed to create logging folder: $_" } - - Return $FullOutputPath } Function Set-LoggingPath { From 45d4abe7d91752be6214e00239ae6c27f621d69c Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sat, 11 Jan 2025 11:45:49 -0500 Subject: [PATCH 19/23] Change UTC to Z in timestamp format to conform to ISO 8601 standard and save room on CLU --- .../functions/Initialize-HawkGlobalObject.ps1 | 33 ++++++++++--------- Hawk/internal/functions/Out-LogFile.ps1 | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index a428092..dbcd847 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -65,7 +65,8 @@ Function Test-LoggingPath { param([string]$PathToTest) - $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + # 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) { @@ -75,13 +76,13 @@ } # If it is not a folder return false and write an error else { - Write-Information "[$timestamp UTC] - [ERROR] - 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 "[$timestamp UTC] - [ERROR] - Directory $PathToTest Not Found" + Write-Information "[$timestamp] - [ERROR] - Directory $PathToTest Not Found" Return $false } } @@ -90,19 +91,20 @@ [CmdletBinding(SupportsShouldProcess)] param([string]$RootPath) - $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + # 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 UTC] - [INFO] - Already connected to Microsoft Graph" + Write-Information "[$timestamp] - [INFO] - Already connected to Microsoft Graph" } catch { # Only show connecting message if we actually need to connect - Write-Information "[$timestamp UTC] - [ACTION] - Connecting to Microsoft Graph" + Write-Information "[$timestamp] - [ACTION] - Connecting to Microsoft Graph" $null = Test-GraphConnection - Write-Information "[$timestamp UTC] - [INFO] - Connected to Microsoft Graph Successfully" + Write-Information "[$timestamp] - [INFO] - Connected to Microsoft Graph Successfully" } # Get tenant name @@ -112,10 +114,10 @@ $FullOutputPath = Join-Path $RootPath $FolderID if (Test-Path $FullOutputPath) { - Write-Information "[$timestamp UTC] - [ERROR] - Path $FullOutputPath already exists" + Write-Information "[$timestamp] - [ERROR] - Path $FullOutputPath already exists" } else { - Write-Information "[$timestamp UTC] - [ACTION] - Creating subfolder $FullOutputPath" + Write-Information "[$timestamp] - [ACTION] - Creating subfolder $FullOutputPath" $null = New-Item $FullOutputPath -ItemType Directory -ErrorAction Stop } @@ -123,7 +125,7 @@ } catch { # If it fails at any point, display an error message - Write-Error "[$timestamp UTC] - [ERROR] - Failed to create logging folder: $_" + Write-Error "[$timestamp] - [ERROR] - Failed to create logging folder: $_" } } @@ -131,18 +133,19 @@ [CmdletBinding(SupportsShouldProcess)] param ([string]$Path) - $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + # 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 to get a valid path Do { # Ask the user for the output path - [string]$UserPath = Read-Host "[$timestamp UTC] - [PROMPT] - Please provide an output directory" + [string]$UserPath = Read-Host "[$timestamp] - [PROMPT] - Please provide an output directory" # If the input is null or empty, prompt again if ([string]::IsNullOrEmpty($UserPath)) { - Write-Host "[$timestamp UTC] - [INFO] - Directory path cannot be empty. Please enter in a new path." + 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 @@ -152,7 +155,7 @@ } # If the path is invalid, prompt again else { - Write-Host "[$timestamp UTC] - [ERROR] - Path not a valid directory: $UserPath" -ForegroundColor Red + Write-Host "[$timestamp] - [ERROR] - Path not a valid directory: $UserPath" -ForegroundColor Red $ValidPath = $false } } @@ -166,7 +169,7 @@ } # If the provided path fails validation, stop the process else { - Write-Error "[$timestamp UTC] - [ERROR] - Provided path is not a valid directory: $Path" -ErrorAction Stop + Write-Error "[$timestamp] - [ERROR] - Provided path is not a valid directory: $Path" -ErrorAction Stop } } diff --git a/Hawk/internal/functions/Out-LogFile.ps1 b/Hawk/internal/functions/Out-LogFile.ps1 index fd63146..42bdc94 100644 --- a/Hawk/internal/functions/Out-LogFile.ps1 +++ b/Hawk/internal/functions/Out-LogFile.ps1 @@ -137,7 +137,7 @@ $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 From d6e385e1aabb47b12204919f2c46493b9d6f4834 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sat, 11 Jan 2025 14:33:28 -0500 Subject: [PATCH 20/23] Trim all whitespace across code base when accepting user input. --- Hawk/functions/User/Get-HawkUserPWNCheck.ps1 | 2 +- Hawk/internal/functions/Get-IPGeolocation.ps1 | 2 +- .../functions/Initialize-HawkGlobalObject.ps1 | 12 +++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 index bd71dd5..6a33a80 100644 --- a/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 +++ b/Hawk/functions/User/Get-HawkUserPWNCheck.ps1 @@ -39,7 +39,7 @@ # get the access key from the user Out-LogFile "haveibeenpwned.com apikey" -isPrompt -NoNewLine - $hibpkey = Read-Host + $hibpkey = (Read-Host).Trim() } }#End of BEGIN block diff --git a/Hawk/internal/functions/Get-IPGeolocation.ps1 b/Hawk/internal/functions/Get-IPGeolocation.ps1 index 1e5c7f7..62bb3c7 100644 --- a/Hawk/internal/functions/Get-IPGeolocation.ps1 +++ b/Hawk/internal/functions/Get-IPGeolocation.ps1 @@ -33,7 +33,7 @@ Function Get-IPGeolocation { # get the access key from the user # get the access key from the user Out-LogFile "ipstack.com accesskey" -isPrompt -NoNewLine - $Accesskey = Read-Host + $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/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index dbcd847..3714663 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -141,7 +141,7 @@ # Setup a while loop to get a valid path Do { # Ask the user for the output path - [string]$UserPath = Read-Host "[$timestamp] - [PROMPT] - Please provide an output directory" + [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)) { @@ -260,7 +260,7 @@ 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 + $StartRead = (Read-Host).Trim() # Determine if input is a valid date @@ -274,7 +274,8 @@ # 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 - $Proceed = Read-Host "Press ENTER to proceed or type 'R' to re-enter the value" + 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 } } @@ -296,7 +297,8 @@ # Validate the date if ($StartDate -lt ((Get-Date).ToUniversalTime().AddDays(-$MaxDaysToGoBack))) { Out-LogFile -string "The date entered exceeds your license retention period of $MaxDaysToGoBack days." -isWarning - $Proceed = Read-Host "Press ENTER to proceed or type 'R' to re-enter the date" + 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 } } @@ -320,7 +322,7 @@ 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 + $EndRead = (Read-Host).Trim() # End date validation if ($null -eq ($EndRead -as [DateTime])) { From e18117ffb16a9d186a0b901fb975e009ed02e487 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sat, 11 Jan 2025 19:03:45 -0500 Subject: [PATCH 21/23] Implement more robust input validation for user input in Initialize-HawkGloabalObject --- ...t-HawkTenantAdminInboxRuleModification.ps1 | 8 +- .../functions/Initialize-HawkGlobalObject.ps1 | 105 +++++++++++++++--- Hawk/internal/functions/Out-LogFile.ps1 | 4 +- 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 index bb67e79..84dbe6c 100644 --- a/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 +++ b/Hawk/functions/Tenant/Get-HawkTenantAdminInboxRuleModification.ps1 @@ -86,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)'" -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 } } } diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 3714663..a0993c2 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -145,7 +145,7 @@ # 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." + 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 @@ -251,25 +251,33 @@ # Ensure MaxDaysToGoBack does not exceed 365 days if ($MaxDaysToGoBack -gt 365) { $MaxDaysToGoBack = 365 } - # Prompt for Start Date if not set + # Start date validation: Add check for negative numbers while ($null -eq $StartDate) { - - # Read input from user 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 " 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 + } + + # Validate numeric value + if ($StartRead -notmatch '^\d+$') { + Out-LogFile -string "Please enter a valid number of days." -isError + continue + } # Validate the entered days back if ($StartRead -gt $MaxDaysToGoBack) { @@ -287,14 +295,19 @@ # Calculate start date [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-$StartRead)).Date Write-Output "" - Out-LogFile -string "Start date set to: $StartDate [UTC]" -Information -NoNewLine - - } elseif (!($null -eq ($StartRead -as [DateTime]))) { - - #### DateTime Provided #### + Out-LogFile -string "Start date set to: $StartDate [UTC]" -Information + } + # Handle DateTime input + elseif (!($null -eq ($StartRead -as [DateTime]))) { [DateTime]$StartDate = (Get-Date $StartRead).ToUniversalTime().Date # Validate the date + if ($StartDate -gt (Get-Date).ToUniversalTime()) { + Out-LogFile -string "Start date cannot be in the future." -isError + $StartDate = $null + continue + } + 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 @@ -308,13 +321,74 @@ } Out-LogFile -string "Start Date (UTC): $StartDate" -Information - - } else { + } + else { Out-LogFile -string "Invalid date information provided. Could not determine if this was a date or an integer." -isError - break + $StartDate = $null + continue } } + # 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]$tempEndDate = (Get-Date).ToUniversalTime().Date + } + else { + # 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 + } + + 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]))) { + [DateTime]$tempEndDate = (Get-Date $EndRead).ToUniversalTime().Date + + if ($StartDate -gt $tempEndDate) { + Out-LogFile -string "End date must be more recent than start date ($StartDate)." -isError + continue + } + 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 + } + + $EndDate = $tempEndDate + 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 + continue + } + } # End date logic remains unchanged if ($null -eq $EndDate) { Write-Output "`n" @@ -371,7 +445,6 @@ Write-HawkConfigurationComplete -Hawk $Hawk - } else { Out-LogFile -string "Valid Hawk Object already exists no actions will be taken." -Information diff --git a/Hawk/internal/functions/Out-LogFile.ps1 b/Hawk/internal/functions/Out-LogFile.ps1 index 42bdc94..5ed64ad 100644 --- a/Hawk/internal/functions/Out-LogFile.ps1 +++ b/Hawk/internal/functions/Out-LogFile.ps1 @@ -145,7 +145,7 @@ $logstring = "[$timestamp] - [ACTION] - $string" } elseif ($isError) { - $logstring = "[$timestamp] - [ERROR] - $string" + $logstring = "[$timestamp] - [ERROR] - $string" } elseif ($notice) { $logstring = "[$timestamp] - [INVESTIGATE] - $string" @@ -172,7 +172,7 @@ $logstring = "[$timestamp] - [WARNING] - $string" } elseif ($isPrompt) { - $logstring = "[$timestamp] - [PROMPT] - $string" + $logstring = "[$timestamp] - [PROMPT] - $string" } else { $logstring = "[$timestamp] - $string" From 50c5ec86abd7d7179ce0318d5ed1cdc12521e727 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sun, 12 Jan 2025 08:13:04 -0500 Subject: [PATCH 22/23] fix spacing on directory input error prompts. --- Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index a0993c2..04898c7 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -114,7 +114,7 @@ $FullOutputPath = Join-Path $RootPath $FolderID if (Test-Path $FullOutputPath) { - Write-Information "[$timestamp] - [ERROR] - Path $FullOutputPath already exists" + Write-Information "[$timestamp] - [ERROR] - Path $FullOutputPath already exists" } else { Write-Information "[$timestamp] - [ACTION] - Creating subfolder $FullOutputPath" @@ -125,7 +125,7 @@ } catch { # If it fails at any point, display an error message - Write-Error "[$timestamp] - [ERROR] - Failed to create logging folder: $_" + Write-Error "[$timestamp] - [ERROR] - Failed to create logging folder: $_" } } @@ -155,7 +155,7 @@ } # If the path is invalid, prompt again else { - Write-Host "[$timestamp] - [ERROR] - Path not a valid directory: $UserPath" -ForegroundColor Red + Write-Information "[$timestamp] - [ERROR] - Path not a valid directory: $UserPath" $ValidPath = $false } } @@ -169,7 +169,7 @@ } # If the provided path fails validation, stop the process else { - Write-Error "[$timestamp] - [ERROR] - Provided path is not a valid directory: $Path" -ErrorAction Stop + Write-Error "[$timestamp] - [ERROR] - Provided path is not a valid directory: $Path" } } From 61722ed02b17ce8b3f0d45fd87dc89191c63aed5 Mon Sep 17 00:00:00 2001 From: Jonathan Butler Date: Sun, 12 Jan 2025 08:19:49 -0500 Subject: [PATCH 23/23] Change Text-HawkGlobalObject to TestHawkGlobalObject --- .../{Text-HawkGlobalObject.ps1 => Test-HawkGlobalObject.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Hawk/internal/functions/{Text-HawkGlobalObject.ps1 => Test-HawkGlobalObject.ps1} (100%) diff --git a/Hawk/internal/functions/Text-HawkGlobalObject.ps1 b/Hawk/internal/functions/Test-HawkGlobalObject.ps1 similarity index 100% rename from Hawk/internal/functions/Text-HawkGlobalObject.ps1 rename to Hawk/internal/functions/Test-HawkGlobalObject.ps1