internal/functions/Initialize-HawkGlobalObject.ps1
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 (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. .PARAMETER NonInteractive Switch to run the command in non-interactive mode. Requires all necessary parameters to be provided via command line rather than through interactive prompts. .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 ( [DateTime]$StartDate, [DateTime]$EndDate, [int]$DaysToLookBack, [string]$FilePath, [switch]$SkipUpdate, [switch]$NonInteractive, [switch]$Force ) if ($Force) { Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue } # Check for incomplete/interrupted initialization and force a fresh start if ($null -ne (Get-Variable -Name Hawk -ErrorAction SilentlyContinue)) { if (Test-HawkGlobalObject) { Remove-Variable -Name Hawk -Scope Global -ErrorAction SilentlyContinue # Remove other related global variables that might exist Remove-Variable -Name IPlocationCache -Scope Global -ErrorAction SilentlyContinue Remove-Variable -Name MSFTIPList -Scope Global -ErrorAction SilentlyContinue } } Function Test-LoggingPath { param([string]$PathToTest) # Get the current timestamp in the format yyyy-MM-dd HH:mm:ssZ $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss'Z'") # First test if the path we were given exists if (Test-Path $PathToTest) { # If the path exists verify that it is a folder if ((Get-Item $PathToTest).PSIsContainer -eq $true) { Return $true } # If it is not a folder return false and write an error else { Write-Information "[$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] - [ERROR] - Directory $PathToTest Not Found" Return $false } } Function New-LoggingFolder { [OutputType([System.Collections.Hashtable])] [CmdletBinding(SupportsShouldProcess)] param([string]$RootPath) # Get the current timestamp in the format yyyy-MM-dd HH:mm:ssZ $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss'Z'") try { # Test Graph connection first to see if we're already connected try { $null = Get-MgOrganization -ErrorAction Stop Write-Information "[$timestamp] - [INFO] - Already connected to Microsoft Graph" } catch { # Only show connecting message if we actually need to connect Write-Information "[$timestamp] - [ACTION] - Connecting to Microsoft Graph" $null = Test-GraphConnection Write-Information "[$timestamp] - [INFO] - Connected to Microsoft Graph Successfully" } # Get tenant name $org = Get-MgOrganization -ErrorAction Stop if (!$org) { throw "Could not retrieve tenant organization information" } # Use display name if available, otherwise fall back to tenant name $TenantName = if ($org.DisplayName) { $org.DisplayName } else { $org.Id } # Remove any invalid file system characters and spaces $TenantName = $TenantName -replace '[\\/:*?"<>|]', '' -replace '\s+', '_' [string]$FolderID = "Hawk_" + $TenantName + "_" + (Get-Date).ToUniversalTime().ToString("yyyyMMdd_HHmmss") $FullOutputPath = Join-Path $RootPath $FolderID if (Test-Path $FullOutputPath) { Write-Information "[$timestamp] - [ERROR] - Path $FullOutputPath already exists" } else { Write-Information "[$timestamp] - [ACTION] - Creating subfolder $FullOutputPath" $null = New-Item $FullOutputPath -ItemType Directory -ErrorAction Stop } # Return both path and tenant name return @{ Path = $FullOutputPath TenantName = $TenantName } } catch { # If it fails at any point, display an error message Write-Error "[$timestamp] - [ERROR] - Failed to create logging folder: $_" } } Function Set-LoggingPath { [CmdletBinding(SupportsShouldProcess)] param ( [string]$Path) # 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] - [PROMPT] - Please provide an output directory").Trim() # If the input is null or empty, prompt again if ([string]::IsNullOrEmpty($UserPath)) { Write-Host "[$timestamp] - [INFO] - Directory path cannot be empty. Please enter in a new path." $ValidPath = $false } # If the path is valid, create the subfolder elseif (Test-LoggingPath -PathToTest $UserPath) { $folderInfo = New-LoggingFolder -RootPath $UserPath $ValidPath = $true } # If the path is invalid, prompt again else { Write-Information "[$timestamp] - [ERROR] - Path not a valid directory: $UserPath" $ValidPath = $false } } While ($ValidPath -eq $false) } # If a value for Path is provided, validate it else { # If the provided path is valid, create the subfolder if (Test-LoggingPath -PathToTest $Path) { $folderInfo = New-LoggingFolder -RootPath $Path # Changed variable name for clarity } # If the provided path fails validation, stop the process else { Write-Error "[$timestamp] - [ERROR] - Provided path is not a valid directory: $Path" } } Return $folderInfo } Function New-ApplicationInsight { [CmdletBinding(SupportsShouldProcess)] param() # Initialize Application Insights client $insightkey = "aa3d4e74-29c0-4b4c-83ad-865669402baa" if ($Null -eq $Client) { 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)) { if ($NonInteractive) { Write-HawkBanner } else { Write-HawkBanner -DisplayWelcomeMessage } # 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 TenantName = $null } # Set up the file path first, before any other operations # Set up the file path first, before any other operations if ([string]::IsNullOrEmpty($FilePath)) { # Suppress Graph connection output during initial path setup $folderInfo = Set-LoggingPath -ErrorAction Stop $Hawk.FilePath = $folderInfo.Path $Hawk.TenantName = $folderInfo.TenantName } else { $folderInfo = Set-LoggingPath -path $FilePath -ErrorAction Stop 2>$null $Hawk.FilePath = $folderInfo.Path $Hawk.TenantName = $folderInfo.TenantName } # 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) { Out-LogFile -string "Skipping Update Check" -Information } # Check to see if there is an Update for Hawk else { Update-HawkModule } # Test Graph connection Out-LogFile "Testing Graph Connection" -Action Test-GraphConnection if (-not $NonInteractive) { try { $LicenseInfo = Test-LicenseType $MaxDaysToGoBack = $LicenseInfo.RetentionPeriod $LicenseType = $LicenseInfo.LicenseType Out-LogFile -string "Detecting M365 license type to determine maximum log retention period" -action Out-LogFile -string "M365 License type detected: $LicenseType" -Information Out-LogFile -string "Max log retention: $MaxDaysToGoBack days" -action -NoNewLine } catch { Out-LogFile -string "Failed to detect license type. Max days of log retention is unknown." -Information $MaxDaysToGoBack = 90 $LicenseType = "Unknown" } } # Ensure MaxDaysToGoBack does not exceed 365 days if ($MaxDaysToGoBack -gt 365) { $MaxDaysToGoBack = 365 } # Start date validation: Add check for negative numbers while ($null -eq $StartDate) { Write-Output "`n" Out-LogFile "Please specify the first day of the search window:" -isPrompt Out-LogFile " Enter a number of days to go back (1-$MaxDaysToGoBack)" -isPrompt Out-LogFile " OR enter a date in MM/DD/YYYY format" -isPrompt Out-LogFile " Default is 90 days back: " -isPrompt -NoNewLine [string]$StartRead = (Read-Host).Trim() # Determine if input is a valid date # Determine if input is a valid date if ($null -eq ($StartRead -as [DateTime])) { #### Not a DateTime => interpret as # of days #### if ([string]::IsNullOrEmpty($StartRead)) { $StartRead = "90" } # First check if it's a valid integer without converting if (-not ($StartRead -match '^\d+$')) { Out-LogFile -string "Invalid input. Please enter a number between 1 and 365, or a date in MM/DD/YYYY format." -isError continue } # Now safe to convert to integer since we validated the format [int]$StartRead = [int]$StartRead $StartDays = $StartRead # Validate the input is within range # Validate the input is within range if (($StartRead -gt 365) -or ($StartRead -lt 1)) { Out-LogFile -string "Days to go back must be between 1 and 365." -isError Remove-Variable -Name StartDate -ErrorAction SilentlyContinue continue } # Validate the entered days back if ($StartRead -gt $MaxDaysToGoBack) { Out-LogFile -string "The date entered exceeds your license retention period of $MaxDaysToGoBack days." -isWarning Out-LogFile "Press ENTER to proceed or type 'R' to re-enter the date:" -isPrompt -NoNewLine $Proceed = (Read-Host).Trim() if ($Proceed -eq 'R') { Remove-Variable -Name StartDate -ErrorAction SilentlyContinue; continue } } # At this point, we do not yet have EndDate set. So temporarily anchor from "today": [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-$StartRead)).Date Out-LogFile -string "Start date set to: ${StartDate}Z" -Information } elseif (!($null -eq ($StartRead -as [DateTime]))) { [DateTime]$StartDate = $StartRead -as [DateTime] # <--- Add this line # ========== The user entered a DateTime, so $StartDays stays 0 ========== # Validate the date if ($StartDate -gt (Get-Date).ToUniversalTime()) { Out-LogFile -string "Start date cannot be in the future." -isError Remove-Variable -Name StartDate -ErrorAction SilentlyContinue 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 $Proceed = (Read-Host).Trim() if ($Proceed -eq 'R') { Remove-Variable -Name StartDate -ErrorAction SilentlyContinue; continue } } if ($StartDate -lt ((Get-Date).ToUniversalTime().AddDays(-365))) { Out-LogFile -string "The date cannot exceed 365 days. Setting to the maximum limit of 365 days." -isWarning [DateTime]$StartDate = ((Get-Date).ToUniversalTime().AddDays(-365)).Date } Out-LogFile -string "Start Date: ${StartDate}Z" -Information } else { Out-LogFile -string "Invalid date information provided. Could not determine if this was a date or an integer." -isError $StartDate = $null continue } } # End date logic with enhanced validation while ($null -eq $EndDate) { Write-Output "" 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 "Invalid input. Please enter a number between 1 and 365, or a date in MM/DD/YYYY format." -isError continue } Out-LogFile -string "End Date: $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 } # --- FINAL FIX: Always move to next day at 00:00 UTC --- $tempEndDate = $tempEndDate.ToUniversalTime().Date.AddDays(1) $EndDate = $tempEndDate # Write-Output "" # Out-LogFile -string "End date set to: ${EndDate}Z`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 } # --- FINAL FIX: Always move to next day at 00:00 UTC --- $tempEndDate = $tempEndDate.ToUniversalTime().Date.AddDays(1) $EndDate = $tempEndDate # Out-LogFile -string "End date set to: ${EndDate}Z`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 except for final +1 day fix if ($null -eq $EndDate) { Out-LogFile "Please specify the last day of the search window:" -isPrompt Out-LogFile " Enter a number of days to go back from today (1-365)" -isPrompt Out-LogFile " OR enter a specific date in MM/DD/YYYY format" -isPrompt Out-LogFile " Default is today's date:" -isPrompt -NoNewLine $EndRead = (Read-Host).Trim() # End date validation if ($null -eq ($EndRead -as [DateTime])) { if ([string]::IsNullOrEmpty($EndRead)) { [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date } else { Out-LogFile -string "End Date: $EndRead days." -Information [DateTime]$EndDate = ((Get-Date).ToUniversalTime().AddDays( - ($EndRead - 1))).Date } if ($StartDate -gt $EndDate) { Out-LogFile -string "StartDate cannot be more recent than EndDate" -isError } else { # --- FINAL FIX: Always move to next day at 00:00 UTC --- $EndDate = $EndDate.ToUniversalTime().Date.AddDays(1) # Write-Output "" # Out-LogFile -string "End date set to: ${EndDate}Z`n" -Information } } elseif (!($null -eq ($EndRead -as [DateTime]))) { [DateTime]$EndDate = (Get-Date $EndRead).ToUniversalTime().Date if ($StartDate -gt $EndDate) { Out-LogFile -string "EndDate is earlier than StartDate. Setting EndDate to today." -isWarning [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date } elseif ($EndDate -gt ((Get-Date).ToUniversalTime().AddDays(1))) { Out-LogFile -string "EndDate too far in the future. Setting EndDate to today." -isWarning [DateTime]$EndDate = (Get-Date).ToUniversalTime().Date } # --- FINAL FIX: Always move to next day at 00:00 UTC --- $EndDate = $EndDate.ToUniversalTime().Date.AddDays(1) # Out-LogFile -string "End date set to: ${EndDate}Z`n" -Information } else { Out-LogFile -string "Invalid date information provided. Could not determine if this was a date or an integer." -isError } } # --- AFTER the EndDate block, do a final check to "re-anchor" StartDate if it was given in days --- if ($StartDays -gt 0) { # Recalculate StartDate based on EndDate = $EndDate and StartDays = $StartDays Out-LogFile -string "End date set to midnight UTC of next day to include all data from $($EndDate.AddDays(-1).Date.ToString('yyyy-MM-dd'))Z" -Information $StartDate = $EndDate.AddDays(-1).AddDays(-$StartDays).Date # (Optional) Additional validations again if necessary: if ($StartDate -gt (Get-Date).ToUniversalTime()) { Out-LogFile -string "Start date is in the future. Resetting to today's date." -isWarning $StartDate = (Get-Date).ToUniversalTime().Date } # If EndDate is today, adjust to current time if ($EndDate.Date -eq (Get-Date).Date) { $EndDate = (Get-Date).ToUniversalTime() Out-LogFile -string "Adjusting EndDate to current time: $EndDate" -Information } } # Configuration Example, currently not used #TODO: Implement Configuration system across entire project Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value $Days -PassThru | Register-PSFConfig if ($OutputPath) { Set-PSFConfig -Module 'Hawk' -Name 'FilePath' -Value $OutputPath -PassThru | Register-PSFConfig } # Continue populating the Hawk object with other properties $Hawk.DaysToLookBack = $DaysToLookBack $Hawk.StartDate = $StartDate $Hawk.EndDate = $EndDate $Hawk.WhenCreated = (Get-Date).ToUniversalTime().ToString("g") Write-HawkConfigurationComplete -Hawk $Hawk } else { Out-LogFile -string "Valid Hawk Object already exists no actions will be taken." -Information } } |