Skip to content

Commit

Permalink
Fix PSSA Analyzer script issues with plural noun in Get-HawkTenantAZA…
Browse files Browse the repository at this point in the history
…dmins, renaming it to Get-HawkTenantAZAdmin. Added PSSA Exception for global vars as old tests include global vars.
  • Loading branch information
jonnybottles committed Dec 4, 2024
1 parent cec3205 commit c9c2795
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 118 deletions.
2 changes: 1 addition & 1 deletion Hawk/Hawk.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
'Get-HawkUserAutoReply',
'Get-HawkUserMessageTrace',
'Get-HawkUserMobileDevice',
'Get-HawkTenantAZAdmins',
'Get-HawkTenantAZAdmin',
'Get-HawkTenantEXOAdmins',
'Get-HawkTenantMailItemsAccessed',
'Get-HawkTenantAppAndSPNCredentialDetails',
Expand Down
91 changes: 91 additions & 0 deletions Hawk/functions/Tenant/Get-HawkTenantAZAdmin.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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
Send-AIEvent -Event "CmdRun"
}

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"
}
}
56 changes: 0 additions & 56 deletions Hawk/functions/Tenant/Get-HawkTenantAZAdmins.ps1

This file was deleted.

4 changes: 2 additions & 2 deletions Hawk/functions/Tenant/Start-HawkTenantInvestigation.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@
}

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")) {
Expand Down
2 changes: 2 additions & 0 deletions Hawk/internal/configurations/PSScriptAnalyzerSettings.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)
}
186 changes: 146 additions & 40 deletions Hawk/tests/Run-PesterTests.ps1
Original file line number Diff line number Diff line change
@@ -1,41 +1,147 @@
# Load Pester module if not already loaded
Import-Module -Name Pester -ErrorAction Stop

# Log function for consistent output
function Log {
param(
[string]$Message,
[string]$Level = "Info"
)
$timestamp = Get-Date -Format "HH:mm:ss"
Write-Output "[$timestamp][$Level] $Message"
}

# Start of test execution
Log "Starting Tests"

# Define the tests directory
$testDirectory = "$PSScriptRoot"

# Get all test files in the directory, excluding specific ones
$testFiles = Get-ChildItem -Path $testDirectory -Recurse -Include *.Tests.ps1 |
Where-Object { $_.Name -notin @('pester.ps1', 'Run-PesterTests.ps1') }

# Ensure we found test files
if (-not $testFiles) {
Log "No test files found to execute." "Error"
exit 1
}

# Loop through each test file
foreach ($testFile in $testFiles) {
Log "Executing $($testFile.FullName)" "Info"
try {
# Run tests with minimal output
Invoke-Pester -Path $testFile.FullName -Output Minimal -PassThru | Out-Null
} catch {
Log "Error running $($testFile.FullName): $_" "Error"
}
}
<#
.NOTES
The original test this is based upon was written by June Blender.
After several rounds of modifications it stands now as it is, but the honor remains hers.
Thank you June, for all you have done!
.DESCRIPTION
This test evaluates the help for all commands in a module.
.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.
These paths are used to search for commands that should exist and be tested.
Will search recursively and accepts wildcards, make sure only functions are found
.PARAMETER ModuleName
Name of the module to be tested.
The module must already be imported
.PARAMETER ExceptionsFile
File in which exceptions and adjustments are configured.
In it there should be two arrays and a hashtable defined:
$global:FunctionHelpTestExceptions
$global:HelpTestEnumeratedArrays
$global:HelpTestSkipParameterType
These can be used to tweak the tests slightly in cases of need.
See the example file for explanations on each of these usage and effect.
#>
[CmdletBinding()]
Param (
[switch]
$SkipTest,

[string[]]
$CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"),

[string]
$ModuleName = "Hawk",

[string]
$ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1"
)
if ($SkipTest) { return }
. $ExceptionsFile

$includedNames = (Get-ChildItem $CommandPath -Recurse -File | Where-Object Name -like "*.ps1").BaseName
$commandTypes = @('Cmdlet', 'Function')
if ($PSVersionTable.PSEdition -eq 'Desktop' ) { $commandTypes += 'Workflow' }
$commands = Get-Command -Module (Get-Module $ModuleName) -CommandType $commandTypes | Where-Object Name -In $includedNames

## When testing help, remember that help is cached at the beginning of each session.
## To test, restart session.


foreach ($command in $commands) {
$commandName = $command.Name

# Skip all functions that are on the exclusions list
if ($global:FunctionHelpTestExceptions -contains $commandName) { continue }

Log "All tests executed successfully!" "Success"
# 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 '*`[`<CommonParameters`>`]*'
}

# 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)
# Parameter type in Help should match code
It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } {
$parameterHelp.parameterValueGroup.parameterValue | Should -be $names
}
}
elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) {
# Enumerations often have issues with the typename not being reliably available
$names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType)
It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } {
$parameterHelp.parameterValueGroup.parameterValue | Should -be $names
}
}
else {
# To avoid calling Trim method on a null object.
$helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() }
# Parameter type in Help should match code
It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } {
$helpType | Should -be $codeType
}
}
}
foreach ($helpParm in $HelpParameterNames) {
# Shouldn't find extra parameters in help.
It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } {
$helpParm -in $parameterNames | Should -Be $true
}
}
}
}
}
Loading

0 comments on commit c9c2795

Please sign in to comment.