diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 2a4f171..326d406 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -7,7 +7,7 @@ on: branches: - master - Development - - bugfix/157-bug-multiple-failed-tests-after-fixing-workflow + - bugfix/162-modernize-authentication-to-replace-azuread-with-microsoft-graph jobs: validate: diff --git a/Hawk/Hawk.psd1 b/Hawk/Hawk.psd1 index 87edf14..2b53c63 100644 --- a/Hawk/Hawk.psd1 +++ b/Hawk/Hawk.psd1 @@ -3,7 +3,7 @@ RootModule = 'Hawk.psm1' # Version number of this module. - ModuleVersion = '3.1.1' + ModuleVersion = '3.1.2' # ID used to uniquely identify this module GUID = '1f6b6b91-79c4-4edf-83a1-66d2dc8c3d85' @@ -31,9 +31,8 @@ @{ModuleName = 'PSFramework'; ModuleVersion = '1.12.346' }, @{ModuleName = 'PSAppInsights'; ModuleVersion = '0.9.6' }, @{ModuleName = 'ExchangeOnlineManagement'; ModuleVersion = '3.0.0' }, - @{ModuleName = 'AzureAD'; ModuleVersion = '2.0.2.182' }, - @{ModuleName = 'Microsoft.Graph.Authentication'; ModuleVersion = '1.23.0' }, - @{ModuleName = 'Microsoft.Graph.Identity.DirectoryManagement'; ModuleVersion = '1.23.0' } + @{ModuleName = 'Microsoft.Graph.Authentication'; ModuleVersion = '2.25.0' }, + @{ModuleName = 'Microsoft.Graph.Identity.DirectoryManagement'; ModuleVersion = '2.25.0' } ) # Assemblies that must be loaded prior to importing this module @@ -50,7 +49,7 @@ 'Get-HawkTenantConfiguration', 'Get-HawkTenantEDiscoveryConfiguration', 'Get-HawkTenantInboxRules', - 'Get-HawkTenantConsentGrants', + 'Get-HawkTenantConsentGrant', 'Get-HawkTenantRBACChanges', 'Get-HawkTenantAzureAppAuditLog', 'Get-HawkUserAuthHistory', @@ -73,11 +72,11 @@ 'Get-HawkUserAutoReply', 'Get-HawkUserMessageTrace', 'Get-HawkUserMobileDevice', - 'Get-HawkTenantAZAdmins', + 'Get-HawkTenantAZAdmin', 'Get-HawkTenantEXOAdmins', 'Get-HawkTenantMailItemsAccessed', - 'Get-HawkTenantAppAndSPNCredentialDetails', - 'Get-HawkTenantAzureADUsers', + 'Get-HawkTenantAppAndSPNCredentialDetail', + 'Get-HawkTenantEntraIDUser', 'Get-HawkTenantDomainActivity', 'Get-HawkTenantEDiscoveryLog' diff --git a/Hawk/functions/Tenant/Get-HawkTenantAZAdmin.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAZAdmin.ps1 new file mode 100644 index 0000000..a6cc31d --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAZAdmin.ps1 @@ -0,0 +1,90 @@ +Function Get-HawkTenantAZAdmin { + <# + .SYNOPSIS + Tenant Azure Active Directory Administrator export using Microsoft Graph. + .DESCRIPTION + Tenant Azure Active Directory Administrator export. Reviewing administrator access is key to knowing who can make changes + to the tenant and conduct other administrative actions to users and applications. + .EXAMPLE + Get-HawkTenantAZAdmin + Gets all Azure AD Admins + .OUTPUTS + AzureADAdministrators.csv + .LINK + https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.directorymanagement/get-mgdirectoryrole + .NOTES + Requires Microsoft.Graph.Identity.DirectoryManagement module + #> + [CmdletBinding()] + param() + + BEGIN { + # Initializing Hawk Object if not present + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + Out-LogFile "Gathering Azure AD Administrators" + + Test-GraphConnection + } + + PROCESS { + try { + # Get all directory roles + $directoryRoles = Get-MgDirectoryRole -ErrorAction Stop + Out-LogFile "Retrieved $(($directoryRoles | Measure-Object).Count) directory roles" + + $roles = foreach ($role in $directoryRoles) { + # Get members for each role + $members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -ErrorAction Stop + + if (-not $members) { + [PSCustomObject]@{ + AdminGroupName = $role.DisplayName + Members = "No Members" + MemberType = "None" # Added member type for better analysis + MemberId = $null + } + } + else { + foreach ($member in $members) { + # Determine member type and get appropriate properties + if ($member.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.user") { + [PSCustomObject]@{ + AdminGroupName = $role.DisplayName + Members = $member.AdditionalProperties.userPrincipalName + MemberType = "User" + MemberId = $member.Id + } + } + else { + # Groups or Service Principals + [PSCustomObject]@{ + AdminGroupName = $role.DisplayName + Members = $member.AdditionalProperties.displayName + MemberType = ($member.AdditionalProperties.'@odata.type' -replace '#microsoft.graph.', '') + MemberId = $member.Id + } + } + } + } + } + + if ($roles) { + $roles | Out-MultipleFileType -FilePrefix "AzureADAdministrators" -csv -json + Out-LogFile "Successfully exported Azure AD Administrators data" + } + else { + Out-LogFile "No administrator roles found or accessible" -notice + } + } + catch { + Out-LogFile "Error retrieving Azure AD Administrators: $($_.Exception.Message)" -notice + Write-Error -ErrorRecord $_ -ErrorAction Continue + } + } + + END { + Out-LogFile "Completed exporting Azure AD Admins" + } + } \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAZAdmins.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAZAdmins.ps1 deleted file mode 100644 index 9d33a92..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantAZAdmins.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -Function Get-HawkTenantAZAdmins{ -<# -.SYNOPSIS - Tenant Azure Active Directory Administrator export. Must be connected to Azure-AD using the Connect-AzureAD cmdlet -.DESCRIPTION - Tenant Azure Active Directory Administrator export. Reviewing administrator access is key to knowing who can make changes - to the tenant and conduct other administrative actions to users and applications. -.EXAMPLE - Get-HawkTenantAZAdmins - Gets all Azure AD Admins -.OUTPUTS - AzureADAdministrators.csv -.LINK - https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureaddirectoryrolemember?view=azureadps-2.0 -.NOTES -#> -BEGIN{ - #Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { - Initialize-HawkGlobalObject - } - Out-LogFile "Gathering Azure AD Administrators" - - Test-AzureADConnection -} -PROCESS{ - $roles = foreach ($role in Get-MgDirectoryRole){ - $admins = (Get-MGDirectoryRoleMember -DirectoryRoleId $role.id) - if ([string]::IsNullOrWhiteSpace($admins)) { - [PSCustomObject]@{ - AdminGroupName = $role.DisplayName - Members = "No Members" - } - } - foreach ($admin in $admins){ - if($admin.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.user"){ - [PSCustomObject]@{ - AdminGroupName = $role.DisplayName - Members = $admin.AdditionalProperties.userPrincipalName - } - } - else{ - [PSCustomObject]@{ - AdminGroupName = $role.DisplayName - Members = $admin.AdditionalProperties.displayName - } - } - } - } - $roles | Out-MultipleFileType -FilePrefix "AzureADAdministrators" -csv -json - -} -END{ - Out-LogFile "Completed exporting Azure AD Admins" -} -}#End Function \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 new file mode 100644 index 0000000..248addb --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetail.ps1 @@ -0,0 +1,150 @@ +Function Get-HawkTenantAppAndSPNCredentialDetail { + <# + .SYNOPSIS + Tenant Azure Active Directory Applications and Service Principal Credential details export using Microsoft Graph. + .DESCRIPTION + Tenant Azure Active Directory Applications and Service Principal Credential details export. Credential details can be used to + review when credentials were created for an Application or Service Principal. If a malicious user created a certificate or password + used to access corporate data, then knowing the key creation time will be instrumental to determining the time frame of when an attacker + had access to data. + .EXAMPLE + Get-HawkTenantAppAndSPNCredentialDetail + Gets all Tenant Application and Service Principal Details + .OUTPUTS + SPNCertsAndSecrets.csv + ApplicationCertsAndSecrets + .LINK + https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list + https://learn.microsoft.com/en-us/graph/api/application-list + .NOTES + Updated to use Microsoft Graph API instead of AzureAD module + #> + [CmdletBinding()] + param() + + BEGIN { + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + + # Create Tenant folder path if it doesn't exist + $tenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant" + if (-not (Test-Path -Path $tenantPath)) { + New-Item -Path $tenantPath -ItemType Directory -Force | Out-Null + } + + Test-GraphConnection + Send-AIEvent -Event "CmdRun" + + # Initialize arrays to collect all results + $spnResults = @() + $appResults = @() + + Out-LogFile "Collecting Azure AD Service Principals" + try { + $spns = Get-MgServicePrincipal -All | Sort-Object -Property DisplayName + Out-LogFile "Collecting Azure AD Registered Applications" + $apps = Get-MgApplication -All | Sort-Object -Property DisplayName + } + catch { + Out-LogFile "Error retrieving Service Principals or Applications: $($_.Exception.Message)" -Notice + Write-Error -ErrorRecord $_ -ErrorAction Continue + } + } + + PROCESS { + try { + Out-LogFile "Exporting Service Principal Certificate and Password details" + foreach ($spn in $spns) { + # Process key credentials + foreach ($key in $spn.KeyCredentials) { + $newapp = [PSCustomObject]@{ + AppName = $spn.DisplayName + AppObjectID = $spn.Id + KeyID = $key.KeyId + StartDate = $key.StartDateTime + EndDate = $key.EndDateTime + KeyType = $key.Type + CredType = "X509Certificate" + } + # Add to array for JSON output + $spnResults += $newapp + # Output individual record to CSV + $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -append + } + + # Process password credentials + foreach ($pass in $spn.PasswordCredentials) { + $newapp = [PSCustomObject]@{ + AppName = $spn.DisplayName + AppObjectID = $spn.Id + KeyID = $pass.KeyId + StartDate = $pass.StartDateTime + EndDate = $pass.EndDateTime + KeyType = $null + CredType = "PasswordSecret" + } + # Add to array for JSON output + $spnResults += $newapp + # Output individual record to CSV + $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -append + } + } + + # Output complete SPN results array as single JSON + if ($spnResults.Count -gt 0) { + $spnResults | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $tenantPath -ChildPath "SPNCertsAndSecrets.json") + } + + Out-LogFile "Exporting Registered Applications Certificate and Password details" + foreach ($app in $apps) { + # Process key credentials + foreach ($key in $app.KeyCredentials) { + $newapp = [PSCustomObject]@{ + AppName = $app.DisplayName + AppObjectID = $app.Id + KeyID = $key.KeyId + StartDate = $key.StartDateTime + EndDate = $key.EndDateTime + KeyType = $key.Type + CredType = "X509Certificate" + } + # Add to array for JSON output + $appResults += $newapp + # Output individual record to CSV + $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -append + } + + # Process password credentials + foreach ($pass in $app.PasswordCredentials) { + $newapp = [PSCustomObject]@{ + AppName = $app.DisplayName + AppObjectID = $app.Id + KeyID = $pass.KeyId + StartDate = $pass.StartDateTime + EndDate = $pass.EndDateTime + KeyType = $pass.Type + CredType = "PasswordSecret" + } + # Add to array for JSON output + $appResults += $newapp + # Output individual record to CSV + $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -append + } + } + + # Output complete application results array as single JSON + if ($appResults.Count -gt 0) { + $appResults | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $tenantPath -ChildPath "ApplicationCertsAndSecrets.json") + } + } + catch { + Out-LogFile "Error processing credentials: $($_.Exception.Message)" -Notice + Write-Error -ErrorRecord $_ -ErrorAction Continue + } + } + + END { + Out-Logfile "Completed exporting Azure AD Service Principal and App Registration Certificate and Password Details" + } +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetails.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetails.ps1 deleted file mode 100644 index 596ca15..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantAppAndSPNCredentialDetails.ps1 +++ /dev/null @@ -1,111 +0,0 @@ -Function Get-HawkTenantAppAndSPNCredentialDetails { -<# -.SYNOPSIS - Tenant Azure Active Directory Applications and Service Principal Credential details export. Must be connected to Azure-AD using the Connect-AzureAD cmdlet -.DESCRIPTION - Tenant Azure Active Directory Applications and Service Principal Credential details export. Credential details can be used to - review when credentials were created for an Application or Service Principal. If a malicious user created a certificat or password - used to access corporate data, then knowing the key creation time will intrumental to determing the time frame of when an attacker - had access to data. -.EXAMPLE - Get-HawkTenantAppAndSPNCredentialDetails - Gets all Tenant Application and Service Principal Details -.OUTPUTS - SPNCertsAndSecrets.csv - ApplicationCertsAndSecrets -.LINK - https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals - https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureadapplicationkeycredential?view=azureadps-2.0 -.NOTES -#> -BEGIN{ - #Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { - Initialize-HawkGlobalObject - } - Test-AzureADConnection - - Out-LogFile "Collecting Azure AD Service Principals" - $spns = Get-MgServicePrincipal -all | Sort-Object -Property DisplayName - Out-LogFile "Collecting Azure AD Registered Applications" - $apps = Get-MgApplication -all $true | Sort-Object -Property DisplayName -} - -PROCESS{ - Out-LogFile "Exporting Service Principal Certificate and Password details" - foreach ($spn in $spns) { - $keys = $spn.keycredentials - foreach ($key in $keys){ - $newapp = [PSCustomObject]@{ - AppName = $spn.DisplayName - AppObjectID = $spn.ObjectID - KeyID = $key.KeyID - StartDate = $key.startdate - EndDate = $key.endDate - KeyType = $Key.Type - CredType = "X509Certificate" - - } - $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -json -append - - } - } - foreach ($spn in $spns) { - $passwords = $spn.PasswordCredentials - foreach ($pass in $passwords){ - $newapp = [PSCustomObject]@{ - AppName = $spn.DisplayName - AppObjectID = $spn.ObjectID - KeyID = $pass.KeyID - StartDate = $pass.startdate - EndDate = $pass.endDate - KeyType = $null - CredType = "PasswordSecret" - } - $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -json -append - - } - - } - Out-LogFile "Exporting Registered Applications Certificate and Password details" - foreach ($app in $apps) { - $keys = $app.keycredentials - foreach ($key in $keys){ - $newapp = [PSCustomObject]@{ - AppName = $app.DisplayName - AppObjectID = $app.ObjectID - KeyID = $key.KeyID - StartDate = $key.startdate - EndDate = $key.endDate - KeyType = $Key.Type - CredType = "X509Certificate" - - } - $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -json -append - - } - - } - foreach ($app in $apps) { - $passwords = $app.PasswordCredentials - foreach ($pass in $passwords){ - $newapp = [PSCustomObject]@{ - AppName = $app.DisplayName - AppObjectID = $app.ObjectID - KeyID = $pass.KeyID - StartDate = $pass.startdate - EndDate = $pass.endDate - KeyType = $pass.Type - CredType = "PasswordSecret" - - } - $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -json -append - - } - } -}#End Process -END{ - Out-Logfile "Completed exporting Azure AD Service Principal and App Registration Certificate and Password Details" -} #End End - -}#End Function diff --git a/Hawk/functions/Tenant/Get-HawkTenantAzureADUsers.ps1 b/Hawk/functions/Tenant/Get-HawkTenantAzureADUsers.ps1 deleted file mode 100644 index 9bb156b..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantAzureADUsers.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -Function Get-HawkTenantAzureADUsers{ -<# -.SYNOPSIS - This function will export all the Azure Active Directory users. -.DESCRIPTION - This function will export all the Azure Active Directory users to .csv file. This data can be used - as a reference for user presence and details about the user for additional context at a later time. This is a point - in time users enumeration. Date created can be of help when determining account creation date. -.EXAMPLE - PS C:\>Get-HawkTenantAzureADUsers - Exports all Azure AD users to .csv -.OUTPUTS - AzureADUPNS.csv -.LINK - https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureaduser?view=azureadps-2.0 -.NOTES -#> -BEGIN{ - #Initializing Hawk Object if not present - if ([string]::IsNullOrEmpty($Hawk.FilePath)) { - Initialize-HawkGlobalObject - } - Out-LogFile "Gathering Azure AD Users" - - Test-AzureADConnection - -}#End BEGIN -PROCESS{ - $users = foreach ($user in (Get-MGUser -All $True)){ - $userproperties = $user | Select-Object userprincipalname, id, usertype, CreatedDateTime, AccountEnabled - foreach ($properties in $userproperties){ - [PSCustomObject]@{ - UserPrincipalname = $userproperties.userprincipalname - ObjectID = $userproperties.id - UserType = $userproperties.UserType - DateCreated = $userproperties.createdDateTime - AccountEnabled = $userproperties.AccountEnabled - } - } - } - $users | Sort-Object -property UserPrincipalname | Out-MultipleFileType -FilePrefix "AzureADUsers" -csv -json -}#End PROCESS -END{ - Out-Logfile "Completed exporting Azure AD users" -}#End END - - -}#End Function - diff --git a/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 b/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 new file mode 100644 index 0000000..c7b3613 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantConsentGrant.ps1 @@ -0,0 +1,56 @@ +Function Get-HawkTenantConsentGrant { + <# +.SYNOPSIS + Gathers application grants using Microsoft Graph + +.DESCRIPTION + Uses Microsoft Graph to gather information about application and delegate grants. + Attempts to detect high risk grants for review. This function is used to identify + potentially risky application permissions and consent grants in your tenant. + +.EXAMPLE + Get-HawkTenantConsentGrant + Gathers and analyzes all OAuth grants in the tenant. + +.OUTPUTS + File: Consent_Grants.csv + Path: \Tenant + Description: Output of all consent grants with details about permissions and access + +.NOTES + This function requires the following Microsoft Graph permissions: + - Application.Read.All + - Directory.Read.All +#> + [CmdletBinding()] + param() + + Out-LogFile "Gathering OAuth / Application Grants" + + Test-GraphConnection + + # Gather the grants using the internal Graph-based implementation + [array]$Grants = Get-AzureADPSPermission -ShowProgress + [bool]$flag = $false + + # Search the Grants for the listed bad grants that we can detect + if ($Grants.ConsentType -contains 'AllPrincipals') { + Out-LogFile "Found at least one 'AllPrincipals' Grant" -notice + $flag = $true + } + if ([bool]($Grants.Permission -match 'all')) { + Out-LogFile "Found at least one 'All' Grant" -notice + $flag = $true + } + + if ($flag) { + Out-LogFile 'Review the information at the following link to understand these results' -notice + Out-LogFile 'https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants' -notice + } + else { + Out-LogFile "To review this data follow:" + Out-LogFile "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants" + } + + $Grants | Out-MultipleFileType -FilePrefix "Consent_Grants" -csv -json +} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantConsentGrants.ps1 b/Hawk/functions/Tenant/Get-HawkTenantConsentGrants.ps1 deleted file mode 100644 index 3c0baa9..0000000 --- a/Hawk/functions/Tenant/Get-HawkTenantConsentGrants.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -Function Get-HawkTenantConsentGrants { -<# -.SYNOPSIS - Gathers application grants -.DESCRIPTION - Used the script from https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants to gather information about - application and delegate grants. Attempts to detect high risk grants for review. -.OUTPUTS - File: Consent_Grants.csv - Path: \Tenant - Description: Output of all consent grants -.EXAMPLE - Get-HawkTenantConsentGrants - - Gathers Grants -#> - - Out-LogFile "Gathering Oauth / Application Grants" - - Test-AzureADConnection - Send-AIEvent -Event "CmdRun" - - # Gather the grants - # Using the script from the article https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants - [array]$Grants = Get-AzureADPSPermissions -ShowProgress - [bool]$flag = $false - - # Search the Grants for the listed bad grants that we can detect - if ($Grants.consenttype -contains 'AllPrinciples') { - Out-LogFile "Found at least one `'AllPrinciples`' Grant" -notice - $flag = $true - } - if ([bool]($Grants.permission -match 'all')){ - Out-LogFile "Found at least one `'All`' Grant" -notice - $flag = $true - } - - if ($flag){ - Out-LogFile 'Review the information at the following link to understand these results' -notice - Out-LogFile 'https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants#inventory-apps-with-access-in-your-organization' -notice - } - else { - Out-LogFile "To review this data follow:" - Out-LogFile "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants#inventory-apps-with-access-in-your-organization" - } - - $Grants | Out-MultipleFileType -FilePrefix "Consent_Grants" -csv -json -} \ No newline at end of file diff --git a/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 b/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 new file mode 100644 index 0000000..d10d0c7 --- /dev/null +++ b/Hawk/functions/Tenant/Get-HawkTenantEntraIDUser.ps1 @@ -0,0 +1,65 @@ +Function Get-HawkTenantEntraIDUser { + <# + .SYNOPSIS + This function will export all the Entra ID users (formerly Azure AD users). + .DESCRIPTION + This function exports all the Entra ID users to a .csv file, focusing on properties + relevant for digital forensics and incident response. Properties include user identity, + account status, and account dates. + + Note: SignInActivity requires additional AuditLog.Read.All permission and is currently commented out. + .EXAMPLE + PS C:\>Get-HawkTenantEntraIDUser + Exports all Entra ID users with DFIR-relevant properties to .csv and .json files. + .OUTPUTS + EntraIDUsers.csv, EntraIDUsers.json + .LINK + https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=powershell + .NOTES + Updated to use Microsoft Graph SDK instead of AzureAD module. + Properties selected for DFIR relevance. + #> + BEGIN { + # Initialize the Hawk environment if not already done + if ([string]::IsNullOrEmpty($Hawk.FilePath)) { + Initialize-HawkGlobalObject + } + Out-LogFile "Gathering Entra ID Users" + + # Ensure we have a valid Graph connection + Test-GraphConnection + } + PROCESS { + # Get all users with specific properties needed for DFIR + # -Property parameter optimizes API call to only retrieve needed fields + $users = Get-MgUser -All -Property UserPrincipalName, # Primary user identifier + DisplayName, # User's display name + Id, # Unique object ID + AccountEnabled, # Account status (active/disabled) + CreatedDateTime, # Account creation timestamp + DeletedDateTime, # Account deletion timestamp (if applicable) + LastPasswordChangeDateTime, # Last password modification + Mail | # Primary email address + Select-Object UserPrincipalName, + DisplayName, + Id, + AccountEnabled, + CreatedDateTime, + DeletedDateTime, + LastPasswordChangeDateTime, + Mail + + # Only process if users were found + if ($users) { + # Sort by UPN and export to both CSV and JSON formats + $users | Sort-Object -Property UserPrincipalName | + Out-MultipleFileType -FilePrefix "EntraIDUsers" -csv -json + } + else { + Out-LogFile "No users found" + } + } + END { + Out-Logfile "Completed exporting Entra ID users" + } + } \ No newline at end of file diff --git a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 index 5c2cb11..2e06ee3 100644 --- a/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 +++ b/Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1 @@ -82,22 +82,22 @@ } if ($PSCmdlet.ShouldProcess("Consent Grants", "Get consent grants")) { - Out-LogFile "Running Get-HawkTenantConsentGrants" -action - Get-HawkTenantConsentGrants + Out-LogFile "Running Get-HawkTenantConsentGrant" -action + Get-HawkTenantConsentGrant } if ($PSCmdlet.ShouldProcess("Azure Admins", "Get Azure admin list")) { - Out-LogFile "Running Get-HawkTenantAZAdmins" -action - Get-HawkTenantAZAdmins + Out-LogFile "Running Get-HawkTenantAZAdmin" -action + Get-HawkTenantAZAdmin } if ($PSCmdlet.ShouldProcess("App and SPN Credentials", "Get credential details")) { - Out-LogFile "Running Get-HawkTenantAppAndSPNCredentialDetails" -action - Get-HawkTenantAppAndSPNCredentialDetails + Out-LogFile "Running Get-HawkTenantAppAndSPNCredentialDetail" -action + Get-HawkTenantAppAndSPNCredentialDetail } - if ($PSCmdlet.ShouldProcess("Azure AD Users", "Get Azure AD user list")) { - Out-LogFile "Running Get-HawkTenantAzureADUsers" -action - Get-HawkTenantAzureADUsers + if ($PSCmdlet.ShouldProcess("Entra ID Users", "Get Entra ID user list")) { + Out-LogFile "Running Get-HawkTenantEntraIDUser" -action + Get-HawkTenantEntraIDUser } } \ No newline at end of file diff --git a/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 b/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 index d9f535d..f18fdeb 100644 --- a/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 +++ b/Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1 @@ -5,5 +5,7 @@ # It is assumed this was done with good reason. 'PSAvoidTrailingWhitespace' 'PSShouldProcess' + # Exclude this as old test rules use Global Vars, will need to fix old tests and re-include this rule + 'PSAvoidGlobalVars' ) } \ No newline at end of file diff --git a/Hawk/internal/functions/Get-AzureADPSPermission.ps1 b/Hawk/internal/functions/Get-AzureADPSPermission.ps1 new file mode 100644 index 0000000..751e9f8 --- /dev/null +++ b/Hawk/internal/functions/Get-AzureADPSPermission.ps1 @@ -0,0 +1,211 @@ +Function Get-AzureADPSPermission { +<# +.SYNOPSIS + Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). + +.DESCRIPTION + Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments) + using Microsoft Graph API. This function retrieves and formats permission information for analysis + of application and delegated permissions in your tenant. + +.PARAMETER DelegatedPermissions + If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions + switch is set, both application and delegated permissions will be returned. + +.PARAMETER ApplicationPermissions + If set, will return application permissions. If neither this switch nor the DelegatedPermissions + switch is set, both application and delegated permissions will be returned. + +.PARAMETER UserProperties + The list of properties of user objects to include in the output. Defaults to DisplayName only. + +.PARAMETER ServicePrincipalProperties + The list of properties of service principals (i.e. apps) to include in the output. + Defaults to DisplayName only. + +.PARAMETER ShowProgress + Whether or not to display a progress bar when retrieving application permissions (which could take some time). + +.PARAMETER PrecacheSize + The number of users to pre-load into a cache. For tenants with over a thousand users, + increasing this may improve performance of the script. + +.EXAMPLE + PS C:\> Get-AzureADPSPermission | Export-Csv -Path "permissions.csv" -NoTypeInformation + Generates a CSV report of all permissions granted to all apps. + +.EXAMPLE + PS C:\> Get-AzureADPSPermission -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" } + Get all apps which have application permissions for Directory.Read.All. + +.EXAMPLE + PS C:\> Get-AzureADPSPermission -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId") + Gets all permissions granted to all apps and includes additional properties for users and service principals. + +.NOTES + This function requires Microsoft.Graph PowerShell module and appropriate permissions: + - Application.Read.All + - Directory.Read.All +#> + [CmdletBinding()] + param( + [switch] $DelegatedPermissions, + [switch] $ApplicationPermissions, + [string[]] $UserProperties = @("DisplayName"), + [string[]] $ServicePrincipalProperties = @("DisplayName"), + [switch] $ShowProgress, + [System.Int32] $PrecacheSize = 999 + ) + + # Verify Graph connection + try { + $tenant_details = Get-MgOrganization + } + catch { + throw "You must call Connect-MgGraph before running this script." + } + Write-Verbose ("TenantId: {0}" -f $tenant_details.Id) + + # Cache objects + $script:ObjectByObjectId = @{} + $script:ObjectByObjectType = @{ + 'ServicePrincipal' = @{} + 'User' = @{} + } + + function CacheObject ($Object, $Type) { + if ($Object) { + $script:ObjectByObjectType[$Type][$Object.Id] = $Object + $script:ObjectByObjectId[$Object.Id] = $Object + } + } + + function GetObjectByObjectId ($ObjectId) { + if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) { + Write-Verbose ("Querying Graph API for object '{0}'" -f $ObjectId) + try { + $object = Get-MgDirectoryObject -DirectoryObjectId $ObjectId + # Determine type from OdataType + $type = $object.AdditionalProperties.'@odata.type'.Split('.')[-1] + CacheObject -Object $object -Type $type + } + catch { + Write-Verbose "Object not found." + } + } + return $script:ObjectByObjectId[$ObjectId] + } + + # Cache all service principals + Write-Verbose "Retrieving all ServicePrincipal objects..." + $servicePrincipals = Get-MgServicePrincipal -All + foreach($sp in $servicePrincipals) { + CacheObject -Object $sp -Type 'ServicePrincipal' + } + $servicePrincipalCount = $servicePrincipals.Count + + # Cache users + Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize) + $users = Get-MgUser -Top $PrecacheSize + foreach($user in $users) { + CacheObject -Object $user -Type 'User' + } + + if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { + Write-Verbose "Retrieving OAuth2PermissionGrants..." + $oauth2Grants = Get-MgOAuth2PermissionGrant -All + + foreach ($grant in $oauth2Grants) { + if ($grant.Scope) { + $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object { + $scope = $_ + + $grantDetails = [ordered]@{ + "PermissionType" = "Delegated" + "ClientObjectId" = $grant.ClientId + "ResourceObjectId" = $grant.ResourceId + "Permission" = $scope + "ConsentType" = $grant.ConsentType + "PrincipalObjectId" = $grant.PrincipalId + } + + # Add service principal properties + if ($ServicePrincipalProperties.Count -gt 0) { + $client = $script:ObjectByObjectId[$grant.ClientId] + $resource = $script:ObjectByObjectId[$grant.ResourceId] + + $insertAtClient = 2 + $insertAtResource = 3 + foreach ($propertyName in $ServicePrincipalProperties) { + $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) + $insertAtResource++ + $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) + $insertAtResource++ + } + } + + # Add user properties + if ($UserProperties.Count -gt 0) { + $principal = if ($grant.PrincipalId) { + $script:ObjectByObjectId[$grant.PrincipalId] + } else { @{} } + + foreach ($propertyName in $UserProperties) { + $grantDetails["Principal$propertyName"] = $principal.$propertyName + } + } + + New-Object PSObject -Property $grantDetails + } + } + } + } + + if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { + Write-Verbose "Retrieving AppRoleAssignments..." + + $i = 0 + foreach ($sp in $servicePrincipals) { + if ($ShowProgress) { + Write-Progress -Activity "Retrieving application permissions..." ` + -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` + -PercentComplete (($i / $servicePrincipalCount) * 100) + } + + $appRoleAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -All + + foreach ($assignment in $appRoleAssignments) { + if ($assignment.PrincipalType -eq "ServicePrincipal") { + $resource = $script:ObjectByObjectId[$assignment.ResourceId] + $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.AppRoleId } + + $grantDetails = [ordered]@{ + "PermissionType" = "Application" + "ClientObjectId" = $assignment.PrincipalId + "ResourceObjectId" = $assignment.ResourceId + "Permission" = $appRole.Value + } + + if ($ServicePrincipalProperties.Count -gt 0) { + $client = $script:ObjectByObjectId[$assignment.PrincipalId] + + $insertAtClient = 2 + $insertAtResource = 3 + foreach ($propertyName in $ServicePrincipalProperties) { + $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) + $insertAtResource++ + $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) + $insertAtResource++ + } + } + + New-Object PSObject -Property $grantDetails + } + } + } + + if ($ShowProgress) { + Write-Progress -Completed -Activity "Retrieving application permissions..." + } + } +} \ No newline at end of file diff --git a/Hawk/internal/functions/Get-AzureADPSPermissions.ps1 b/Hawk/internal/functions/Get-AzureADPSPermissions.ps1 deleted file mode 100644 index 4e12b4f..0000000 --- a/Hawk/internal/functions/Get-AzureADPSPermissions.ps1 +++ /dev/null @@ -1,249 +0,0 @@ -Function Get-AzureADPSPermissions { - - <# - .SYNOPSIS - Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). - .DESCRIPTION - ists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). - .PARAMETER DelegatedPermissions - If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions switch is set, - both application and delegated permissions will be returned. - .PARAMETER ApplicationPermissions - If set, will return application permissions. If neither this switch nor the DelegatedPermissions switch is set, - both application and delegated permissions will be returned. - .PARAMETER UserProperties - The list of properties of user objects to include in the output. Defaults to DisplayName only. - .PARAMETER ServicePrincipalProperties - The list of properties of service principals (i.e. apps) to include in the output. Defaults to DisplayName only. - .PARAMETER ShowProgress - Whether or not to display a progress bar when retrieving application permissions (which could take some time). - .PARAMETER PrecacheSize - The number of users to pre-load into a cache. For tenants with over a thousand users, - increasing this may improve performance of the script. - .EXAMPLE - PS C:\> .\Get-AzureADPSPermissions.ps1 | Export-Csv -Path "permissions.csv" -NoTypeInformation - Generates a CSV report of all permissions granted to all apps. - .EXAMPLE - PS C:\> .\Get-AzureADPSPermissions.ps1 -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" } - Get all apps which have application permissions for Directory.Read.All. - .EXAMPLE - PS C:\> .\Get-AzureADPSPermissions.ps1 -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId") - Gets all permissions granted to all apps and includes additional properties for users and service principals. - - .LINK - https://gist.github.com/psignoret/9d73b00b377002456b24fcb808265c23 - - #> - - [CmdletBinding()] - param( - [switch] $DelegatedPermissions, - - [switch] $ApplicationPermissions, - - [string[]] $UserProperties = @("DisplayName"), - - [string[]] $ServicePrincipalProperties = @("DisplayName"), - - [switch] $ShowProgress, - - [int] $PrecacheSize = 999 - ) - - # Get tenant details to test that Connect-AzureAD has been called - try { - $tenant_details = Get-AzureADTenantDetail - } catch { - throw "You must call Connect-AzureAD before running this script." - } - Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f ` - $tenant_details.ObjectId, ` - ($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name) - - # An in-memory cache of objects by {object ID} andy by {object class, object ID} - $script:ObjectByObjectId = @{} - $script:ObjectByObjectClassId = @{} - - # Function to add an object to the cache - function CacheObject ($Object) { - if ($Object) { - if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) { - $script:ObjectByObjectClassId[$Object.ObjectType] = @{} - } - $script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object - $script:ObjectByObjectId[$Object.ObjectId] = $Object - } - } - - # Function to retrieve an object from the cache (if it's there), or from Azure AD (if not). - function GetObjectByObjectId ($ObjectId) { - if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) { - Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId) - try { - $object = Get-AzureADObjectByObjectId -ObjectId $ObjectId - CacheObject -Object $object - } catch { - Write-Verbose "Object not found." - } - } - return $script:ObjectByObjectId[$ObjectId] - } - - # Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode) - # or by iterating over all ServicePrincipal objects. The latter is required if there are more than - # 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD. - function GetOAuth2PermissionGrants ([switch]$FastMode) { - if ($FastMode) { - Get-AzureADOAuth2PermissionGrant -All $true - } else { - $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } { - if ($ShowProgress) { - Write-Progress -Activity "Retrieving delegated permissions..." ` - -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` - -PercentComplete (($i / $servicePrincipalCount) * 100) - } - - $client = $_.Value - Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId - } - } - } - - $empty = @{} # Used later to avoid null checks - - # Get all ServicePrincipal objects and add to the cache - Write-Verbose "Retrieving all ServicePrincipal objects..." - Get-AzureADServicePrincipal -All $true | ForEach-Object { - CacheObject -Object $_ - } - $servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count - - if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { - - # Get one page of User objects and add to the cache - Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize) - Get-AzureADUser -Top $PrecacheSize | Where-Object { - CacheObject -Object $_ - } - - Write-Verbose "Testing for OAuth2PermissionGrants bug before querying..." - $fastQueryMode = $false - try { - # There's a bug in Azure AD Graph which does not allow for directly listing - # oauth2PermissionGrants if there are more than 999 of them. The following line will - # trigger this bug (if it still exists) and throw an exception. - $null = Get-AzureADOAuth2PermissionGrant -Top 999 - $fastQueryMode = $true - } catch { - if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) { - Write-Verbose ("Fast query for delegated permissions failed, using slow method...") - } else { - throw $_ - } - } - - # Get all existing OAuth2 permission grants, get the client, resource and scope details - Write-Verbose "Retrieving OAuth2PermissionGrants..." - GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object { - $grant = $_ - if ($grant.Scope) { - $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object { - - $scope = $_ - - $grantDetails = [ordered]@{ - "PermissionType" = "Delegated" - "ClientObjectId" = $grant.ClientId - "ResourceObjectId" = $grant.ResourceId - "Permission" = $scope - "ConsentType" = $grant.ConsentType - "PrincipalObjectId" = $grant.PrincipalId - } - - # Add properties for client and resource service principals - if ($ServicePrincipalProperties.Count -gt 0) { - - $client = GetObjectByObjectId -ObjectId $grant.ClientId - $resource = GetObjectByObjectId -ObjectId $grant.ResourceId - - $insertAtClient = 2 - $insertAtResource = 3 - foreach ($propertyName in $ServicePrincipalProperties) { - $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) - $insertAtResource++ - $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) - $insertAtResource ++ - } - } - - # Add properties for principal (will all be null if there's no principal) - if ($UserProperties.Count -gt 0) { - - $principal = $empty - if ($grant.PrincipalId) { - $principal = GetObjectByObjectId -ObjectId $grant.PrincipalId - } - - foreach ($propertyName in $UserProperties) { - $grantDetails["Principal$propertyName"] = $principal.$propertyName - } - } - - Return New-Object PSObject -Property $grantDetails - } - } - } - } - - if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { - - # Iterate over all ServicePrincipal objects and get app permissions - Write-Verbose "Retrieving AppRoleAssignments..." - $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } { - - if ($ShowProgress) { - Write-Progress -Activity "Retrieving application permissions..." ` - -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` - -PercentComplete (($i / $servicePrincipalCount) * 100) - - if ($i -eq $servicePrincipalCount){ - Write-Progress -Completed -Activity "Retrieving application permissions..." ` - } - } - - $sp = $_.Value - - Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true ` - | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object { - $assignment = $_ - - $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId - $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id } - - $grantDetails = [ordered]@{ - "PermissionType" = "Application" - "ClientObjectId" = $assignment.PrincipalId - "ResourceObjectId" = $assignment.ResourceId - "Permission" = $appRole.Value - } - - # Add properties for client and resource service principals - if ($ServicePrincipalProperties.Count -gt 0) { - - $client = GetObjectByObjectId -ObjectId $assignment.PrincipalId - - $insertAtClient = 2 - $insertAtResource = 3 - foreach ($propertyName in $ServicePrincipalProperties) { - $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) - $insertAtResource++ - $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) - $insertAtResource ++ - } - } - - Return New-Object PSObject -Property $grantDetails - } - } - } - } \ No newline at end of file diff --git a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 index 831b357..affed4a 100644 --- a/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 +++ b/Hawk/internal/functions/Initialize-HawkGlobalObject.ps1 @@ -1,5 +1,5 @@ Function Initialize-HawkGlobalObject { -<# + <# .SYNOPSIS Create global variable $Hawk for use by all Hawk cmdlets. .DESCRIPTION @@ -39,13 +39,12 @@ This Command will force the creation of a new $Hawk variable even if one already exists. #> - [CmdletBinding()] + [CmdletBinding()] param ( [switch]$Force, [switch]$IAgreeToTheEula, [switch]$SkipUpdate, - [int]$DaysToLookBack, [DateTime]$StartDate, [DateTime]$EndDate, [string]$FilePath @@ -75,10 +74,11 @@ } Function New-LoggingFolder { + [CmdletBinding(SupportsShouldProcess)] param([string]$RootPath) # Create a folder ID based on date - [string]$TenantName = (Get-MGDomain | Where-Object {$_.isDefault}).ID + [string]$TenantName = (Get-MGDomain | Where-Object { $_.isDefault }).ID [string]$FolderID = "Hawk_" + $TenantName.Substring(0, $TenantName.IndexOf('.')) + "_" + (Get-Date -UFormat %Y%m%d_%H%M).tostring() # Add that ID to the given path @@ -98,6 +98,7 @@ } Function Set-LoggingPath { + [CmdletBinding(SupportsShouldProcess)] param ([string]$Path) # If no value of Path is provided prompt and gather from the user @@ -191,6 +192,8 @@ } Function New-ApplicationInsight { + [CmdletBinding(SupportsShouldProcess)] + param() # Initialize Application Insights client $insightkey = "b69ffd8b-4569-497c-8ee7-b71b8257390e" if ($Null -eq $Client) { @@ -218,8 +221,6 @@ } # Test if we have a connection to Microsoft Graph - $notification = New-Object -ComObject Wscript.Shell - $Output =$notification.Popup("Hawk has been updated to support MGGraph due to MSONLINE deprecation. Please click OK to continue", 0, "Hawk Update", 0x00000040) Write-Information "Testing Graph Connection" Test-GraphConnection @@ -332,7 +333,7 @@ Write-Information "Setting EndDate to today." [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date } - elseif ($EndDate -gt (get-Date).AddDays(2)){ + elseif ($EndDate -gt (get-Date).AddDays(2)) { Write-Information "EndDate to Far in the furture." Write-Information "Setting EndDate to Today." [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date @@ -359,23 +360,23 @@ [bool]$AdvancedAzureLicense = $false } - # Configuration Example, currently not used - #TODO: Implement Configuration system across entire project - Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value $Days -PassThru | Register-PSFConfig - if ($OutputPath) { - Set-PSFConfig -Module 'Hawk' -Name 'FilePath' -Value $OutputPath -PassThru | Register-PSFConfig - } + # Configuration Example, currently not used + #TODO: Implement Configuration system across entire project + Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value $Days -PassThru | Register-PSFConfig + if ($OutputPath) { + Set-PSFConfig -Module 'Hawk' -Name 'FilePath' -Value $OutputPath -PassThru | Register-PSFConfig + } - #TODO: Discard below once migration to configuration is completed + #TODO: Discard below once migration to configuration is completed $Output = [PSCustomObject]@{ - FilePath = $OutputPath - DaysToLookBack = $Days - StartDate = $StartDate - EndDate = $EndDate - AdvancedAzureLicense = $AdvancedAzureLicense - WhenCreated = (Get-Date -Format g) - EULA = $Eula - } + FilePath = $OutputPath + DaysToLookBack = $Days + StartDate = $StartDate + EndDate = $EndDate + AdvancedAzureLicense = $AdvancedAzureLicense + WhenCreated = (Get-Date -Format g) + EULA = $Eula + } # Create the script hawk variable Write-Information "Setting up Script Hawk environment variable`n" @@ -383,7 +384,6 @@ Out-LogFile "Script Variable Configured" Out-LogFile ("*** Version " + (Get-Module Hawk).version + " ***") Out-LogFile $Hawk - #### End of IF } diff --git a/Hawk/internal/functions/Test-AzureADConnection.ps1 b/Hawk/internal/functions/Test-AzureADConnection.ps1 deleted file mode 100644 index b17ea9f..0000000 --- a/Hawk/internal/functions/Test-AzureADConnection.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -<# -.SYNOPSIS - Test if we have a connection with the AzureAD Cmdlets -.DESCRIPTION - Test if we have a connection with the AzureAD Cmdlets -.EXAMPLE - PS C:\> - Explanation of what the example does -.INPUTS - Inputs (if any) -.OUTPUTS - Output (if any) -.NOTES - General notes -#> -Function Test-AzureADConnection { - - $TestModule = Get-Module AzureAD -ListAvailable -ErrorAction SilentlyContinue - $MinimumVersion = New-Object -TypeName Version -ArgumentList "2.0.2.140" - - if ($null -eq $TestModule) { - Out-LogFile "Please Install the AzureAD Module with the following command:" - Out-LogFile "Install-Module AzureAD" - break - } - # Since we are not null pull the highest version - else { - $TestModuleVersion = ($TestModule | Sort-Object -Property Version -Descending)[0].version - } - - # Test the version we need at least 2.0.2.140 - if ($TestModuleVersion -lt $MinimumVersion) { - Out-LogFile ("AzureAD Module Installed Version: " + $TestModuleVersion) - Out-LogFile ("Miniumum Required Version: " + $MinimumVersion) - Out-LogFile "Please update the module with: Update-Module AzureAD" - break - } - # Do nothing - else { } - - try { - $Null = Get-AzureADTenantDetail -ErrorAction Stop - } - catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException] { - #Out-LogFile "Please connect to AzureAD prior to running this cmdlet" - Out-LogFile "Connecting-AzureAD" - Connect-AzureAD - } -} \ No newline at end of file diff --git a/Hawk/internal/functions/Test-GraphConnection.ps1 b/Hawk/internal/functions/Test-GraphConnection.ps1 index 7e44415..f19e94f 100644 --- a/Hawk/internal/functions/Test-GraphConnection.ps1 +++ b/Hawk/internal/functions/Test-GraphConnection.ps1 @@ -26,8 +26,6 @@ Function Test-GraphConnection { else { Out-LogFile "Connecting to MGGraph using MGGraph Module" } - # Connect to the MG Graph. The following scopes allow to retrieve Domain, Organization, and Sku data from the Graph. - Connect-MGGraph -Scopes "User.Read.All", "Directory.Read.All" - Select-MgProfile -Name "v1.0" + Connect-MGGraph } -}#End Function Test-GraphConnection \ No newline at end of file +} \ No newline at end of file diff --git a/Hawk/tests/general/FileIntegrity.Tests.ps1 b/Hawk/tests/general/FileIntegrity.Tests.ps1 index a95dc2e..3446be9 100644 --- a/Hawk/tests/general/FileIntegrity.Tests.ps1 +++ b/Hawk/tests/general/FileIntegrity.Tests.ps1 @@ -1,6 +1,6 @@ $script:moduleRoot = (Resolve-Path "$PSScriptRoot\..").Path -. "$PSScriptRoot\general\FileIntegrity.Exceptions.ps1" +. "$PSScriptRoot\FileIntegrity.Exceptions.ps1" Describe "Verifying integrity of module files" { BeforeAll { @@ -23,7 +23,7 @@ Describe "Verifying integrity of module files" { [string] $Path ) - + process { if ($PSVersionTable.PSVersion.Major -lt 6) { [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path @@ -31,7 +31,7 @@ Describe "Verifying integrity of module files" { else { [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path } - + if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } @@ -43,22 +43,22 @@ Describe "Verifying integrity of module files" { Context "Validating PS1 Script files" { $allFiles = Get-ChildItem -Path $script:moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$script:moduleRoot\tests\*" - + foreach ($file in $allFiles) { $name = $file.FullName.Replace("$script:moduleRoot\", '') - + It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } { Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' } - + $tokens = $null $parseErrors = $null [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) - + It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { $parseErrors | Should -BeNullOrEmpty } - + foreach ($command in $script:BannedCommands) { if ($script:MayContainCommand["$command"] -notcontains $file.Name) { It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } { @@ -68,13 +68,13 @@ Describe "Verifying integrity of module files" { } } } - + Context "Validating help.txt help files" { $allFiles = Get-ChildItem -Path $script:moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$script:moduleRoot\tests\*" - + foreach ($file in $allFiles) { $name = $file.FullName.Replace("$script:moduleRoot\", '') - + It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } { Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' } diff --git a/Hawk/tests/general/Help.Tests.ps1 b/Hawk/tests/general/Help.Tests.ps1 index 28fc10d..43daf9a 100644 --- a/Hawk/tests/general/Help.Tests.ps1 +++ b/Hawk/tests/general/Help.Tests.ps1 @@ -10,7 +10,7 @@ .PARAMETER SkipTest Disables this test. - + .PARAMETER CommandPath List of paths under which the script files are stored. This test assumes that all functions have their own file that is named after themselves. @@ -34,13 +34,13 @@ Param ( [switch] $SkipTest, - + [string[]] $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"), - + [string] $ModuleName = "Hawk", - + [string] $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1" ) @@ -58,60 +58,60 @@ $commands = Get-Command -Module (Get-Module $ModuleName) -CommandType $commandTy foreach ($command in $commands) { $commandName = $command.Name - + # Skip all functions that are on the exclusions list if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } - + # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets $Help = Get-Help $commandName -ErrorAction SilentlyContinue - + Describe "Test help for $commandName" { - + # If help is not found, synopsis in auto-generated help is the syntax diagram It "should not be auto-generated" -TestCases @{ Help = $Help } { $Help.Synopsis | Should -Not -BeLike '*`[``]*' } - + # Should be a description for every function It "gets description for $commandName" -TestCases @{ Help = $Help } { $Help.Description | Should -Not -BeNullOrEmpty } - + # Should be at least one example It "gets example code from $commandName" -TestCases @{ Help = $Help } { ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty } - + # Should be at least one example description It "gets example help from $commandName" -TestCases @{ Help = $Help } { ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty } - + Context "Test parameter help for $commandName" { - + $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' - + $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common $parameterNames = $parameters.Name $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique foreach ($parameter in $parameters) { $parameterName = $parameter.Name $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName - + # Should be a description for every parameter It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } { $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty } - + $codeMandatory = $parameter.IsMandatory.toString() It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { $parameterHelp.Required | Should -Be $codeMandatory } - + if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } - + $codeType = $parameter.ParameterType.Name - + if ($parameter.ParameterType.IsEnum) { # Enumerations often have issues with the typename not being reliably available $names = $parameter.ParameterType::GetNames($parameter.ParameterType) diff --git a/Hawk/tests/general/Test-PreCommitHook.ps1 b/Hawk/tests/general/Test-PreCommitHook.ps1 index e7d3845..942d4b0 100644 --- a/Hawk/tests/general/Test-PreCommitHook.ps1 +++ b/Hawk/tests/general/Test-PreCommitHook.ps1 @@ -1,4 +1,4 @@ -# This file contains examples of both good and bad PowerShell code for testing PSScriptAnalyzer. +# This file contains examples of both good and bad PowerShell code for testing PSScriptAnalyzer. # To test the pre-commit hook or VS Code integration: # 1. Uncomment the "Bad Code Examples" section # 2. Try to commit the changes diff --git a/Hawk/tests/pester.ps1 b/Hawk/tests/pester.ps1 index 94f787b..4acf34b 100644 --- a/Hawk/tests/pester.ps1 +++ b/Hawk/tests/pester.ps1 @@ -1,14 +1,14 @@ param ( $TestGeneral = $true, - + $TestFunctions = $true, - + [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] [Alias('Show')] $Output = "None", - + $Include = "*", - + $Exclude = "" ) @@ -78,7 +78,7 @@ if ($TestFunctions) { if ($file.Name -notlike $Include) { continue } if ($file.Name -like $Exclude) { continue } - + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" $config.Run.Path = $file.FullName