Merge pull request #115 from CriticalSolutionsNetwork/Add-QoL-Features

Add qol features
This commit is contained in:
Doug Rios
2024-06-17 09:42:08 -05:00
committed by GitHub
25 changed files with 788 additions and 624 deletions

1
.gitignore vendored
View File

@@ -16,3 +16,4 @@ markdownissues.txt
node_modules node_modules
package-lock.json package-lock.json
Aligned.xlsx Aligned.xlsx
test-gh1.ps1

View File

@@ -6,6 +6,16 @@ The format is based on and uses the types of changes according to [Keep a Change
### Added ### Added
- Added `Export-M365SecurityAuditTable` public function to export applicable audit results to a table format.
- Added paramter to `Export-M365SecurityAuditTable` to specify output of the original audit results.
- Added `Remove-RowsWithEmptyCSVStatus` public function to remove rows with empty status from the CSV file.
- Added `Get-Action` private function to retrieve the action for the test 6.1.2 and 6.1.3 tests.
- Added output modifications to tests that produce tables to ensure they can be exported with the new `Export-M365SecurityAuditTable` function.
## [0.1.11] - 2024-06-14
### Added
- Added Get-MFAStatus function to help with auditing mfa for conditional access controls. - Added Get-MFAStatus function to help with auditing mfa for conditional access controls.
### Fixed ### Fixed

BIN
README.md

Binary file not shown.

Binary file not shown.

View File

@@ -4,7 +4,7 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1
<# <#
$ver = "v0.1.10" $ver = "v0.1.11"
git checkout main git checkout main
git pull origin main git pull origin main
git tag -a $ver -m "Release version $ver refactor Update" git tag -a $ver -m "Release version $ver refactor Update"
@@ -13,73 +13,3 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1
git push origin $ver git push origin $ver
# git tag -d $ver # git tag -d $ver
#> #>
# Refresh authentication to ensure the correct scopes
gh auth refresh -s project,read:project,write:project,repo
# Create the project
gh project create --owner CriticalSolutionsNetwork --title "Test Validation Project"
$repoOwner = "CriticalSolutionsNetwork"
$repoName = "M365FoundationsCISReport"
$directoryPath = ".\source\tests"
$projectName = "Test Validation Project"
# Function to create GitHub issues
function Create-GitHubIssue {
param (
[string]$title,
[string]$body,
[string]$project
)
# Create the issue and add it to the specified project
$issue = gh issue create --repo "$repoOwner/$repoName" --title "$title" --body "$body" --project "$project"
return $issue
}
# Load test definitions from CSV
$testDefinitionsPath = ".\source\helper\TestDefinitions.csv"
$testDefinitions = Import-Csv -Path $testDefinitionsPath
# Iterate over each .ps1 file in the directory
Get-ChildItem -Path $directoryPath -Filter "*.ps1" | ForEach-Object {
$fileName = $_.Name
$testDefinition = $testDefinitions | Where-Object { $_.TestFileName -eq $fileName }
if ($testDefinition) {
$rec = $testDefinition.Rec
$elevel = $testDefinition.ELevel
$profileLevel = $testDefinition.ProfileLevel
$ig1 = $testDefinition.IG1
$ig2 = $testDefinition.IG2
$ig3 = $testDefinition.IG3
$connection = $testDefinition.Connection
$issueTitle = "Rec: $rec - Validate $fileName, ELevel: $elevel, ProfileLevel: $profileLevel, IG1: $ig1, IG2: $ig2, IG3: $ig3, Connection: $connection"
$issueBody = @"
# Validation for $fileName
## Tasks
- [ ] Validate test for a pass
- Description of passing criteria:
- [ ] Validate test for a fail
- Description of failing criteria:
- [ ] Add notes and observations
- Placeholder for additional notes:
"@
# Create the issue using GitHub CLI
try {
Create-GitHubIssue -title "$issueTitle" -body "$issueBody" -project "$projectName"
Write-Output "Created issue for $fileName"
} catch {
Write-Error "Failed to create issue for $fileName : $_"
}
# Introduce a delay of 2 seconds
Start-Sleep -Seconds 2
} else {
Write-Warning "No matching test definition found for $fileName"
}
}

View File

@@ -1,29 +0,0 @@
function Format-MissingAction {
[CmdletBinding()]
[OutputType([hashtable])]
param (
[array]$missingActions
)
$actionGroups = @{
"Admin" = @()
"Delegate" = @()
"Owner" = @()
}
foreach ($action in $missingActions) {
if ($action -match "(Admin|Delegate|Owner) action '([^']+)' missing") {
$type = $matches[1]
$actionName = $matches[2]
$actionGroups[$type] += $actionName
}
}
$formattedResults = @{
Admin = $actionGroups["Admin"] -join ', '
Delegate = $actionGroups["Delegate"] -join ', '
Owner = $actionGroups["Owner"] -join ', '
}
return $formattedResults
}

View File

@@ -0,0 +1,113 @@
function Get-Action {
[CmdletBinding(DefaultParameterSetName = "GetDictionaries")]
param (
[Parameter(Position = 0, ParameterSetName = "GetDictionaries")]
[switch]$Dictionaries,
[Parameter(Position = 0, ParameterSetName = "ConvertActions")]
[string[]]$Actions,
[Parameter(Position = 1, Mandatory = $true, ParameterSetName = "ConvertActions")]
[ValidateSet("Admin", "Delegate", "Owner")]
[string]$ActionType,
[Parameter(Position = 0, ParameterSetName = "ReverseActions")]
[string[]]$AbbreviatedActions,
[Parameter(Position = 1, Mandatory = $true, ParameterSetName = "ReverseActions")]
[ValidateSet("Admin", "Delegate", "Owner")]
[string]$ReverseActionType
)
$Dictionary = @{
AdminActions = @{
ApplyRecord = 'AR'
Copy = 'CP'
Create = 'CR'
FolderBind = 'FB'
HardDelete = 'HD'
MailItemsAccessed = 'MIA'
Move = 'MV'
MoveToDeletedItems = 'MTDI'
SendAs = 'SA'
SendOnBehalf = 'SOB'
Send = 'SD'
SoftDelete = 'SD'
Update = 'UP'
UpdateCalendarDelegation = 'UCD'
UpdateFolderPermissions = 'UFP'
UpdateInboxRules = 'UIR'
}
DelegateActions = @{
ApplyRecord = 'AR'
Create = 'CR'
FolderBind = 'FB'
HardDelete = 'HD'
MailItemsAccessed = 'MIA'
Move = 'MV'
MoveToDeletedItems = 'MTDI'
SendAs = 'SA'
SendOnBehalf = 'SOB'
SoftDelete = 'SD'
Update = 'UP'
UpdateFolderPermissions = 'UFP'
UpdateInboxRules = 'UIR'
}
OwnerActions = @{
ApplyRecord = 'AR'
Create = 'CR'
HardDelete = 'HD'
MailboxLogin = 'ML'
MailItemsAccessed = 'MIA'
Move = 'MV'
MoveToDeletedItems = 'MTDI'
Send = 'SD'
SoftDelete = 'SD'
Update = 'UP'
UpdateCalendarDelegation = 'UCD'
UpdateFolderPermissions = 'UFP'
UpdateInboxRules = 'UIR'
}
}
switch ($PSCmdlet.ParameterSetName) {
"GetDictionaries" {
return $Dictionary
}
"ConvertActions" {
$actionDictionary = switch ($ActionType) {
"Admin" { $Dictionary.AdminActions }
"Delegate" { $Dictionary.DelegateActions }
"Owner" { $Dictionary.OwnerActions }
}
$abbreviatedActions = @()
foreach ($action in $Actions) {
if ($actionDictionary.ContainsKey($action)) {
$abbreviatedActions += $actionDictionary[$action]
}
}
return $abbreviatedActions
}
"ReverseActions" {
$reverseDictionary = @{}
$originalDictionary = switch ($ReverseActionType) {
"Admin" { $Dictionary.AdminActions }
"Delegate" { $Dictionary.DelegateActions }
"Owner" { $Dictionary.OwnerActions }
}
foreach ($key in $originalDictionary.Keys) {
$reverseDictionary[$originalDictionary[$key]] = $key
}
$fullNames = @()
foreach ($abbrAction in $AbbreviatedActions) {
if ($reverseDictionary.ContainsKey($abbrAction)) {
$fullNames += $reverseDictionary[$abbrAction]
}
}
return $fullNames
}
}
}

View File

@@ -0,0 +1,54 @@
function Get-ExceededLengthResultDetail {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')]
[Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')]
[object[]]$AuditResults,
[Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')]
[Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')]
[string[]]$TestNumbersToCheck,
[Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')]
[string[]]$ExportedTests,
[Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')]
[switch]$ReturnExceedingTestsOnly,
[int]$DetailsLengthLimit = 30000,
[Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')]
[int]$PreviewLineCount = 50
)
$exceedingTests = @()
$updatedResults = @()
for ($i = 0; $i -lt $AuditResults.Count; $i++) {
$auditResult = $AuditResults[$i]
if ($auditResult.Rec -in $TestNumbersToCheck) {
if ($auditResult.Details.Length -gt $DetailsLengthLimit) {
if ($ReturnExceedingTestsOnly) {
$exceedingTests += $auditResult.Rec
} else {
$previewLines = ($auditResult.Details -split '\r?\n' | Select-Object -First $PreviewLineCount) -join "`n"
$message = "The test result is too large to be exported to CSV. Use the audit result and the export function for full output.`n`nPreview:`n$previewLines"
if ($ExportedTests -contains $auditResult.Rec) {
Write-Information "The test result for $($auditResult.Rec) is too large for CSV and was included in the export. Check the exported files."
$auditResult.Details = $message
} else {
$auditResult.Details = $message
}
}
}
}
$updatedResults += $auditResult
}
if ($ReturnExceedingTestsOnly) {
return $exceedingTests
} else {
return $updatedResults
}
}

View File

@@ -0,0 +1,36 @@
<#
.SYNOPSIS
This function generates a large table with the specified number of lines.
.DESCRIPTION
This function generates a large table with the specified number of lines. The table has a header and each line has the same format.
.EXAMPLE
Initialize-LargeTestTable -lineCount 1000
.PARAMETER lineCount
The number of lines to generate.
.INPUTS
System.Int32
.OUTPUTS
System.String
.NOTES
The function is intended for testing purposes.
#>
function Initialize-LargeTestTable {
[cmdletBinding()]
[OutputType([string])]
param(
[Parameter()]
[int]$lineCount = 1000 # Number of lines to generate
)
process {
$header = "UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing"
$lineTemplate = "user{0}@contosonorthwind.net|True|FB,CP,MV|FB,MV|ML,MV,CR"
# Generate the header and lines
$lines = @($header)
for ($i = 1; $i -le $lineCount; $i++) {
$lines += [string]::Format($lineTemplate, $i)
}
$output = $lines -join "`n"
Write-Host "Details character count: $($output.Length)"
return $output
}
}

View File

@@ -0,0 +1,196 @@
<#
.SYNOPSIS
Exports M365 security audit results to a CSV file or outputs a specific test result as an object.
.DESCRIPTION
This function exports M365 security audit results from either an array of CISAuditResult objects or a CSV file.
It can export all results to a specified path or output a specific test result as an object.
.PARAMETER AuditResults
An array of CISAuditResult objects containing the audit results.
.PARAMETER CsvPath
The path to a CSV file containing the audit results.
.PARAMETER OutputTestNumber
The test number to output as an object. Valid values are "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4".
.PARAMETER ExportAllTests
Switch to export all test results.
.PARAMETER ExportPath
The path where the CSV files will be exported.
.PARAMETER ExportOriginalTests
Switch to export the original audit results to a CSV file.
.INPUTS
[CISAuditResult[]], [string]
.OUTPUTS
[PSCustomObject]
.EXAMPLE
# Output object for a single test number from audit results
Export-M365SecurityAuditTable -AuditResults $object -OutputTestNumber 6.1.2
.EXAMPLE
# Export all results from audit results to the specified path
Export-M365SecurityAuditTable -ExportAllTests -AuditResults $object -ExportPath "C:\temp"
.EXAMPLE
# Output object for a single test number from CSV
Export-M365SecurityAuditTable -CsvPath "C:\temp\auditresultstoday1.csv" -OutputTestNumber 6.1.2
.EXAMPLE
# Export all results from CSV to the specified path
Export-M365SecurityAuditTable -ExportAllTests -CsvPath "C:\temp\auditresultstoday1.csv" -ExportPath "C:\temp"
.EXAMPLE
# Export all results from audit results to the specified path along with the original tests
Export-M365SecurityAuditTable -ExportAllTests -AuditResults $object -ExportPath "C:\temp" -ExportOriginalTests
.EXAMPLE
# Export all results from CSV to the specified path along with the original tests
Export-M365SecurityAuditTable -ExportAllTests -CsvPath "C:\temp\auditresultstoday1.csv" -ExportPath "C:\temp" -ExportOriginalTests
.LINK
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Export-M365SecurityAuditTable
#>
function Export-M365SecurityAuditTable {
[CmdletBinding()]
[OutputType([PSCustomObject])]
param (
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = "ExportAllResultsFromAuditResults")]
[Parameter(Mandatory = $true, Position = 2, ParameterSetName = "OutputObjectFromAuditResultsSingle")]
[CISAuditResult[]]$AuditResults,
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = "ExportAllResultsFromCsv")]
[Parameter(Mandatory = $true, Position = 2, ParameterSetName = "OutputObjectFromCsvSingle")]
[ValidateScript({ (Test-Path $_) -and ((Get-Item $_).PSIsContainer -eq $false) })]
[string]$CsvPath,
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = "OutputObjectFromAuditResultsSingle")]
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = "OutputObjectFromCsvSingle")]
[ValidateSet("1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4")]
[string]$OutputTestNumber,
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "ExportAllResultsFromAuditResults")]
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "ExportAllResultsFromCsv")]
[switch]$ExportAllTests,
[Parameter(Mandatory = $true, ParameterSetName = "ExportAllResultsFromAuditResults")]
[Parameter(Mandatory = $true, ParameterSetName = "ExportAllResultsFromCsv")]
[string]$ExportPath,
[Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromAuditResults")]
[Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromCsv")]
[switch]$ExportOriginalTests
)
if ($PSCmdlet.ParameterSetName -like "ExportAllResultsFromCsv" -or $PSCmdlet.ParameterSetName -eq "OutputObjectFromCsvSingle") {
$AuditResults = Import-Csv -Path $CsvPath | ForEach-Object {
$params = @{
Rec = $_.Rec
Result = [bool]$_.Result
Status = $_.Status
Details = $_.Details
FailureReason = $_.FailureReason
}
Initialize-CISAuditResult @params
}
}
if ($ExportAllTests) {
$TestNumbers = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4"
}
$results = @()
$testsToProcess = if ($OutputTestNumber) { @($OutputTestNumber) } else { $TestNumbers }
foreach ($test in $testsToProcess) {
$auditResult = $AuditResults | Where-Object { $_.Rec -eq $test }
if (-not $auditResult) {
Write-Information "No audit results found for the test number $test."
continue
}
switch ($test) {
"6.1.2" {
$details = $auditResult.Details
$csv = $details | ConvertFrom-Csv -Delimiter '|'
if ($null -ne $csv) {
foreach ($row in $csv) {
$row.AdminActionsMissing = (Get-Action -AbbreviatedActions $row.AdminActionsMissing.Split(',') -ReverseActionType Admin | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }) -join ','
$row.DelegateActionsMissing = (Get-Action -AbbreviatedActions $row.DelegateActionsMissing.Split(',') -ReverseActionType Delegate | Where-Object { $_ -notin @("MailItemsAccessed") }) -join ','
$row.OwnerActionsMissing = (Get-Action -AbbreviatedActions $row.OwnerActionsMissing.Split(',') -ReverseActionType Owner | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }) -join ','
}
$newObjectDetails = $csv
}
else {
$newObjectDetails = $details
}
$results += [PSCustomObject]@{ TestNumber = $test; Details = $newObjectDetails }
}
"6.1.3" {
$details = $auditResult.Details
$csv = $details | ConvertFrom-Csv -Delimiter '|'
if ($null -ne $csv) {
foreach ($row in $csv) {
$row.AdminActionsMissing = (Get-Action -AbbreviatedActions $row.AdminActionsMissing.Split(',') -ReverseActionType Admin) -join ','
$row.DelegateActionsMissing = (Get-Action -AbbreviatedActions $row.DelegateActionsMissing.Split(',') -ReverseActionType Delegate) -join ','
$row.OwnerActionsMissing = (Get-Action -AbbreviatedActions $row.OwnerActionsMissing.Split(',') -ReverseActionType Owner) -join ','
}
$newObjectDetails = $csv
}
else {
$newObjectDetails = $details
}
$results += [PSCustomObject]@{ TestNumber = $test; Details = $newObjectDetails }
}
Default {
$details = $auditResult.Details
$csv = $details | ConvertFrom-Csv -Delimiter '|'
$results += [PSCustomObject]@{ TestNumber = $test; Details = $csv }
}
}
}
if ($ExportPath) {
$timestamp = (Get-Date).ToString("yyyy.MM.dd_HH.mm.ss")
$exportedTests = @()
foreach ($result in $results) {
$testDef = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $result.TestNumber }
if ($testDef) {
$fileName = "$ExportPath\$($timestamp)_$($result.TestNumber).$($testDef.TestFileName -replace '\.ps1$').csv"
if ($result.Details.Count -eq 0) {
Write-Information "No results found for test number $($result.TestNumber)." -InformationAction Continue
}
else {
$result.Details | Export-Csv -Path $fileName -NoTypeInformation
$exportedTests += $result.TestNumber
}
}
}
if ($exportedTests.Count -gt 0) {
Write-Information "The following tests were exported: $($exportedTests -join ', ')" -InformationAction Continue
}
else {
if ($ExportOriginalTests) {
Write-Information "No specified tests were included in the export other than the full audit results." -InformationAction Continue
}
else {
Write-Information "No specified tests were included in the export." -InformationAction Continue
}
}
if ($ExportOriginalTests) {
# Define the test numbers to check
$TestNumbersToCheck = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4"
# Check for large details and update the AuditResults array
$updatedAuditResults = Get-ExceededLengthResultDetail -AuditResults $AuditResults -TestNumbersToCheck $TestNumbersToCheck -ExportedTests $exportedTests -DetailsLengthLimit 30000 -PreviewLineCount 25
$originalFileName = "$ExportPath\$timestamp`_M365FoundationsAudit.csv"
$updatedAuditResults | Export-Csv -Path $originalFileName -NoTypeInformation
}
}
elseif ($OutputTestNumber) {
if ($results[0].Details) {
return $results[0].Details
}
else {
Write-Information "No results found for test number $($OutputTestNumber)." -InformationAction Continue
}
}
else {
Write-Error "No valid operation specified. Please provide valid parameters."
}
}

View File

@@ -25,7 +25,6 @@
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Get-AdminRoleUserLicense https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Get-AdminRoleUserLicense
#> #>
function Get-AdminRoleUserLicense { function Get-AdminRoleUserLicense {
# Set output type to System.Collections.ArrayList
[OutputType([System.Collections.ArrayList])] [OutputType([System.Collections.ArrayList])]
[CmdletBinding()] [CmdletBinding()]
param ( param (
@@ -42,33 +41,37 @@ function Get-AdminRoleUserLicense {
$userIds = [System.Collections.ArrayList]::new() $userIds = [System.Collections.ArrayList]::new()
} }
Process { process {
$adminroles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Admin*" } Write-Verbose "Retrieving all admin roles"
$adminRoleNames = (Get-MgDirectoryRole | Where-Object { $null -ne $_.RoleTemplateId }).DisplayName
foreach ($role in $adminroles) { Write-Verbose "Filtering admin roles"
$usersInRole = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'" $adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { ($adminRoleNames -contains $_.DisplayName) -and ($_.DisplayName -ne "Directory Synchronization Accounts") }
foreach ($user in $usersInRole) { foreach ($role in $adminRoles) {
$userDetails = Get-MgUser -UserId $user.PrincipalId -Property "DisplayName, UserPrincipalName, Id, onPremisesSyncEnabled" -ErrorAction SilentlyContinue Write-Verbose "Processing role: $($role.DisplayName)"
$roleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'"
foreach ($assignment in $roleAssignments) {
Write-Verbose "Processing role assignment for principal ID: $($assignment.PrincipalId)"
$userDetails = Get-MgUser -UserId $assignment.PrincipalId -Property "DisplayName, UserPrincipalName, Id, OnPremisesSyncEnabled" -ErrorAction SilentlyContinue
if ($userDetails) { if ($userDetails) {
[void]($userIds.Add($user.PrincipalId)) Write-Verbose "Retrieved user details for: $($userDetails.UserPrincipalName)"
[void]( [void]($userIds.Add($userDetails.Id))
$adminRoleUsers.Add( [void]($adminRoleUsers.Add([PSCustomObject]@{
[PSCustomObject]@{
RoleName = $role.DisplayName RoleName = $role.DisplayName
UserName = $userDetails.DisplayName UserName = $userDetails.DisplayName
UserPrincipalName = $userDetails.UserPrincipalName UserPrincipalName = $userDetails.UserPrincipalName
UserId = $userDetails.Id UserId = $userDetails.Id
HybridUser = $userDetails.onPremisesSyncEnabled HybridUser = [bool]$userDetails.OnPremisesSyncEnabled
Licenses = $null # Initialize as $null Licenses = $null # Initialize as $null
} }))
)
)
} }
} }
} }
Write-Verbose "Retrieving licenses for admin role users"
foreach ($userId in $userIds.ToArray() | Select-Object -Unique) { foreach ($userId in $userIds.ToArray() | Select-Object -Unique) {
$licenses = Get-MgUserLicenseDetail -UserId $userId -ErrorAction SilentlyContinue $licenses = Get-MgUserLicenseDetail -UserId $userId -ErrorAction SilentlyContinue
if ($licenses) { if ($licenses) {
@@ -80,7 +83,7 @@ function Get-AdminRoleUserLicense {
} }
} }
End { end {
Write-Host "Disconnecting from Microsoft Graph..." -ForegroundColor Green Write-Host "Disconnecting from Microsoft Graph..." -ForegroundColor Green
Disconnect-MgGraph | Out-Null Disconnect-MgGraph | Out-Null
return $adminRoleUsers return $adminRoleUsers

View File

@@ -286,6 +286,16 @@ function Invoke-M365SecurityAudit {
# Call the private function to calculate and display results # Call the private function to calculate and display results
Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests
# Return all collected audit results # Return all collected audit results
# Define the test numbers to check
$TestNumbersToCheck = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4"
# Check for large details in the audit results
$exceedingTests = Get-ExceededLengthResultDetail -AuditResults $allAuditResults -TestNumbersToCheck $TestNumbersToCheck -ReturnExceedingTestsOnly -DetailsLengthLimit 30000
if ($exceedingTests.Count -gt 0) {
Write-Information "The following tests exceeded the details length limit: $($exceedingTests -join ', ')" -InformationAction Continue
Write-Host "(Assuming the results were instantiated. Ex: `$object = invoke-M365SecurityAudit) Use the following command and adjust as neccesary to view the full details of the test results:" -ForegroundColor DarkCyan
Write-Host "Export-M365SecurityAuditTable -ExportAllTests -AuditResults `$object -ExportPath `"C:\temp`" -ExportOriginalTests" -ForegroundColor Green
}
return $allAuditResults.ToArray() | Sort-Object -Property Rec return $allAuditResults.ToArray() | Sort-Object -Property Rec
} }
} }

View File

@@ -0,0 +1,34 @@
function Remove-RowsWithEmptyCSVStatus {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$FilePath,
[Parameter(Mandatory = $true)]
[string]$WorksheetName
)
# Import the Excel file
$ExcelData = Import-Excel -Path $FilePath -WorksheetName $WorksheetName
# Check if CSV_Status column exists
if (-not $ExcelData.PSObject.Properties.Match("CSV_Status")) {
throw "CSV_Status column not found in the worksheet."
}
# Filter rows where CSV_Status is not empty
$FilteredData = $ExcelData | Where-Object { $null -ne $_.CSV_Status -and $_.CSV_Status -ne '' }
# Get the original file name and directory
$OriginalFileName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath)
$Directory = [System.IO.Path]::GetDirectoryName($FilePath)
# Create a new file name for the filtered data
$NewFileName = "$OriginalFileName-Filtered.xlsx"
$NewFilePath = Join-Path -Path $Directory -ChildPath $NewFileName
# Export the filtered data to a new Excel file
$FilteredData | Export-Excel -Path $NewFilePath -WorksheetName $WorksheetName -Show
Write-Output "Filtered Excel file created at $NewFilePath"
}

View File

@@ -20,7 +20,12 @@ function Test-AdministrativeAccountCompliance {
try { try {
# Retrieve all admin roles # Retrieve all admin roles
Write-Verbose "Retrieving all admin roles" Write-Verbose "Retrieving all admin roles"
$adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Admin*" } # Get the DisplayNames of all admin roles
$adminRoleNames = (Get-MgDirectoryRole | Where-Object { $null -ne $_.RoleTemplateId }).DisplayName
# Use the DisplayNames to filter the roles in Get-MgRoleManagementDirectoryRoleDefinition
$adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { ($adminRoleNames -contains $_.DisplayName) -and ($_.DisplayName -ne "Directory Synchronization Accounts")}
$adminRoleUsers = @() $adminRoleUsers = @()
# Loop through each admin role to get role assignments and user details # Loop through each admin role to get role assignments and user details
@@ -47,9 +52,10 @@ function Test-AdministrativeAccountCompliance {
# Condition C: Check if the account has no other licenses # Condition C: Check if the account has no other licenses
$hasInvalidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -notcontains $_ } $hasInvalidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -notcontains $_ }
$invalidLicenses = $licenses.SkuPartNumber | Where-Object { $validLicenses -notcontains $_ }
$applicationAssignmentStatus = if ($hasInvalidLicense) { "Fail" } else { "Pass" } $applicationAssignmentStatus = if ($hasInvalidLicense) { "Fail" } else { "Pass" }
Write-Verbose "User: $($userDetails.UserPrincipalName), Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Other Applications Assigned: $applicationAssignmentStatus" Write-Verbose "User: $($userDetails.UserPrincipalName), Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Invalid Licenses: $($invalidLicenses -join ', ')"
# Collect user information # Collect user information
$adminRoleUsers += [PSCustomObject]@{ $adminRoleUsers += [PSCustomObject]@{
@@ -95,13 +101,14 @@ function Test-AdministrativeAccountCompliance {
$failureReasons = $failureReasons -join "`n" $failureReasons = $failureReasons -join "`n"
$failureReason = if ($nonCompliantUsers) { $failureReason = if ($nonCompliantUsers) {
"Non-Compliant Accounts: $($nonCompliantUsers.Count)" "Non-Compliant Accounts: $($nonCompliantUsers.Count)"
} else { }
else {
"Compliant Accounts: $($uniqueAdminRoleUsers.Count)" "Compliant Accounts: $($uniqueAdminRoleUsers.Count)"
} }
$result = $nonCompliantUsers.Count -eq 0 $result = $nonCompliantUsers.Count -eq 0
$status = if ($result) { 'Pass' } else { 'Fail' } $status = if ($result) { 'Pass' } else { 'Fail' }
$details = if ($nonCompliantUsers) { "Non-compliant accounts: `nUsername | Roles | Cloud-Only Status | Entra ID License Status | Other Applications Assigned Status`n$failureReasons" } else { "N/A" } $details = if ($nonCompliantUsers) { "Username | Roles | Cloud-Only Status | EntraID P1/P2 License Status | Other Applications Assigned Status`n$failureReasons" } else { "N/A" }
Write-Verbose "Assessment completed. Result: $status" Write-Verbose "Assessment completed. Result: $status"

View File

@@ -64,7 +64,7 @@ function Test-BlockMailForwarding {
if ($nonCompliantSpamPoliciesArray.Count -gt 0) { if ($nonCompliantSpamPoliciesArray.Count -gt 0) {
# Fail Condition B # Fail Condition B
$failureReasons += "Outbound spam policies allowing automatic forwarding found." $failureReasons += "Outbound spam policies allowing automatic forwarding found."
$details += "Outbound Spam Policies Details:`nPolicy|AutoForwardingMode" $details += "Policy|AutoForwardingMode"
$details += $nonCompliantSpamPoliciesArray | ForEach-Object { $details += $nonCompliantSpamPoliciesArray | ForEach-Object {
"$($_.Name)|$($_.AutoForwardingMode)" "$($_.Name)|$($_.AutoForwardingMode)"
} }

View File

@@ -30,20 +30,21 @@ function Test-MailboxAuditingE3 {
#. .\source\Classes\CISAuditResult.ps1 #. .\source\Classes\CISAuditResult.ps1
$e3SkuPartNumber = "SPE_E3" $e3SkuPartNumber = "SPE_E3"
$AdminActions = @("ApplyRecord", "Copy", "Create", "FolderBind", "HardDelete", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules")
$DelegateActions = @("ApplyRecord", "Create", "FolderBind", "HardDelete", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateFolderPermissions", "UpdateInboxRules") $actionDictionaries = Get-Action -Dictionaries
$OwnerActions = @("ApplyRecord", "Create", "HardDelete", "MailboxLogin", "Move", "MoveToDeletedItems", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules") # E3 specific actions
$AdminActions = $actionDictionaries.AdminActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }
$DelegateActions = $actionDictionaries.DelegateActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed") }
$OwnerActions = $actionDictionaries.OwnerActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }
$allFailures = @() $allFailures = @()
#$allUsers = Get-AzureADUser -All $true
$founde3Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e3SkuPartNumber } $founde3Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e3SkuPartNumber }
$processedUsers = @{} # Dictionary to track processed users $processedUsers = @{} # Dictionary to track processed users
$recnum = "6.1.2" $recnum = "6.1.2"
} }
process { process {
if (($founde3Sku.count)-ne 0) { if ($founde3Sku.Count -ne 0) {
$allUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde3Sku.SkuId) )" -All $allUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde3Sku.SkuId) )" -All
$mailboxes = Get-EXOMailbox -PropertySets Audit $mailboxes = Get-EXOMailbox -PropertySets Audit
try { try {
@@ -53,36 +54,36 @@ function Test-MailboxAuditingE3 {
continue continue
} }
#$licenseDetails = Get-MgUserLicenseDetail -UserId $user.UserPrincipalName
#$hasOfficeE3 = ($licenseDetails | Where-Object { $_.SkuPartNumber -in $e3SkuPartNumbers }).Count -gt 0
#Write-Verbose "Evaluating user $($user.UserPrincipalName) for Office E3 license."
$userUPN = $user.UserPrincipalName $userUPN = $user.UserPrincipalName
$mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $user.UserPrincipalName } $mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $user.UserPrincipalName }
$missingActions = @() $missingAdminActions = @()
$missingDelegateActions = @()
$missingOwnerActions = @()
if ($mailbox.AuditEnabled) { if ($mailbox.AuditEnabled) {
foreach ($action in $AdminActions) { foreach ($action in $AdminActions) {
# Condition B: Checking if the `AuditAdmin` actions include required actions if ($mailbox.AuditAdmin -notcontains $action) {
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" } $missingAdminActions += (Get-Action -Actions $action -ActionType "Admin")
}
} }
foreach ($action in $DelegateActions) { foreach ($action in $DelegateActions) {
# Condition C: Checking if the `AuditDelegate` actions include required actions if ($mailbox.AuditDelegate -notcontains $action) {
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" } $missingDelegateActions += (Get-Action -Actions $action -ActionType "Delegate")
}
} }
foreach ($action in $OwnerActions) { foreach ($action in $OwnerActions) {
# Condition D: Checking if the `AuditOwner` actions include required actions if ($mailbox.AuditOwner -notcontains $action) {
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" } $missingOwnerActions += (Get-Action -Actions $action -ActionType "Owner")
}
} }
if ($missingActions.Count -gt 0) { if ($missingAdminActions.Count -gt 0 -or $missingDelegateActions.Count -gt 0 -or $missingOwnerActions.Count -gt 0) {
$formattedActions = Format-MissingAction -missingActions $missingActions $allFailures += "$userUPN|True|$($missingAdminActions -join ',')|$($missingDelegateActions -join ',')|$($missingOwnerActions -join ',')"
$allFailures += "$userUPN|True|$($formattedActions.Admin)|$($formattedActions.Delegate)|$($formattedActions.Owner)"
} }
} }
else { else {
# Condition A: Checking if mailbox audit logging is enabled $allFailures += "$userUPN|False|||" # Condition A for fail
$allFailures += "$userUPN|False|||"
} }
# Mark the user as processed # Mark the user as processed
@@ -90,7 +91,12 @@ function Test-MailboxAuditingE3 {
} }
# Prepare failure reasons and details based on compliance # Prepare failure reasons and details based on compliance
$failureReasons = if ($allFailures.Count -eq 0) { "N/A" } else { "Audit issues detected." } if ($allFailures.Count -eq 0) {
$failureReasons = "N/A"
}
else {
$failureReasons = "Audit issues detected."
}
$details = if ($allFailures.Count -eq 0) { $details = if ($allFailures.Count -eq 0) {
"All Office E3 users have correct mailbox audit settings." "All Office E3 users have correct mailbox audit settings."
} }
@@ -134,14 +140,13 @@ function Test-MailboxAuditingE3 {
} }
end { end {
#$verbosePreference = 'Continue'
$detailsLength = $details.Length $detailsLength = $details.Length
Write-Verbose "Character count of the details: $detailsLength" Write-Verbose "Character count of the details: $detailsLength"
if ($detailsLength -gt 32767) { if ($detailsLength -gt 32767) {
Write-Verbose "Warning: The character count exceeds the limit for Excel cells." Write-Verbose "Warning: The character count exceeds the limit for Excel cells."
} }
#$verbosePreference = 'SilentlyContinue'
return $auditResult return $auditResult
} }
} }

View File

@@ -28,14 +28,15 @@ function Test-MailboxAuditingE5 {
# - Condition D: AuditOwner actions do not include all of the following: ApplyRecord, HardDelete, MailItemsAccessed, MoveToDeletedItems, Send, SoftDelete, Update, UpdateCalendarDelegation, UpdateFolderPermissions, UpdateInboxRules. # - Condition D: AuditOwner actions do not include all of the following: ApplyRecord, HardDelete, MailItemsAccessed, MoveToDeletedItems, Send, SoftDelete, Update, UpdateCalendarDelegation, UpdateFolderPermissions, UpdateInboxRules.
$e5SkuPartNumber = "SPE_E5" $e5SkuPartNumber = "SPE_E5"
$AdminActions = @("ApplyRecord", "Copy", "Create", "FolderBind", "HardDelete", "MailItemsAccessed", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "Send", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules") $founde5Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber }
$DelegateActions = @("ApplyRecord", "Create", "FolderBind", "HardDelete", "MailItemsAccessed", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateFolderPermissions", "UpdateInboxRules")
$OwnerActions = @("ApplyRecord", "Create", "HardDelete", "MailboxLogin", "Move", "MailItemsAccessed", "MoveToDeletedItems", "Send", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules") $actionDictionaries = Get-Action -Dictionaries
$AdminActions = $actionDictionaries.AdminActions.Keys
$DelegateActions = $actionDictionaries.DelegateActions.Keys
$OwnerActions = $actionDictionaries.OwnerActions.Keys
$allFailures = @() $allFailures = @()
#$allUsers = Get-AzureADUser -All $true $processedUsers = @{}
$founde5Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber }
$processedUsers = @{} # Dictionary to track processed users
$recnum = "6.1.3" $recnum = "6.1.3"
} }
@@ -50,35 +51,39 @@ function Test-MailboxAuditingE5 {
continue continue
} }
#$licenseDetails = Get-MgUserLicenseDetail -UserId $user.UserPrincipalName
#$hasOfficeE5 = ($licenseDetails | Where-Object { $_.SkuPartNumber -in $e5SkuPartNumbers }).Count -gt 0
#Write-Verbose "Evaluating user $($user.UserPrincipalName) for Office E5 license."
$mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $user.UserPrincipalName } $mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $user.UserPrincipalName }
$userUPN = $user.UserPrincipalName $userUPN = $user.UserPrincipalName
#$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit
$missingActions = @() $missingAdminActions = @()
$missingDelegateActions = @()
$missingOwnerActions = @()
if ($mailbox.AuditEnabled) { if ($mailbox.AuditEnabled) {
# Validate Admin actions # Validate Admin actions
foreach ($action in $AdminActions) { foreach ($action in $AdminActions) {
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" } # Condition B if ($mailbox.AuditAdmin -notcontains $action) {
$missingAdminActions += (Get-Action -Actions $action -ActionType "Admin") # Condition B
}
} }
# Validate Delegate actions # Validate Delegate actions
foreach ($action in $DelegateActions) { foreach ($action in $DelegateActions) {
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" } # Condition C if ($mailbox.AuditDelegate -notcontains $action) {
$missingDelegateActions += (Get-Action -Actions $action -ActionType "Delegate") # Condition C
}
} }
# Validate Owner actions # Validate Owner actions
foreach ($action in $OwnerActions) { foreach ($action in $OwnerActions) {
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" } # Condition D if ($mailbox.AuditOwner -notcontains $action) {
$missingOwnerActions += (Get-Action -Actions $action -ActionType "Owner") # Condition D
}
} }
if ($missingActions.Count -gt 0) { if ($missingAdminActions.Count -gt 0 -or $missingDelegateActions.Count -gt 0 -or $missingOwnerActions.Count -gt 0) {
$formattedActions = Format-MissingAction -missingActions $missingActions $allFailures += "$userUPN|True|$($missingAdminActions -join ',')|$($missingDelegateActions -join ',')|$($missingOwnerActions -join ',')"
$allFailures += "$userUPN|True|$($formattedActions.Admin)|$($formattedActions.Delegate)|$($formattedActions.Owner)"
} }
} }
else { else {
$allFailures += "$userUPN|False|||" $allFailures += "$userUPN|False|||" # Condition A for fail
} }
# Mark the user as processed # Mark the user as processed
@@ -86,14 +91,19 @@ function Test-MailboxAuditingE5 {
} }
# Prepare failure reasons and details based on compliance # Prepare failure reasons and details based on compliance
$failureReasons = if ($allFailures.Count -eq 0) { "N/A" } else { "Audit issues detected." } if ($allFailures.Count -eq 0) {
$failureReasons = "N/A"
}
else {
$failureReasons = "Audit issues detected."
}
$details = if ($allFailures.Count -eq 0) { $details = if ($allFailures.Count -eq 0) {
"All Office E5 users have correct mailbox audit settings." # Condition A for pass "All Office E5 users have correct mailbox audit settings." # Condition A for pass
} }
else { else {
"UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing`n" + ($allFailures -join "`n") # Condition A for fail "UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing`n" + ($allFailures -join "`n") # Condition A for fail
} }
# $details = Initialize-LargeTestTable -lineCount 3000 # Adjust the lineCount to exceed 32,000 characters
# Populate the audit result # Populate the audit result
$params = @{ $params = @{
Rec = $recnum Rec = $recnum
@@ -130,14 +140,13 @@ function Test-MailboxAuditingE5 {
} }
end { end {
#$verbosePreference = 'Continue'
$detailsLength = $details.Length $detailsLength = $details.Length
Write-Verbose "Character count of the details: $detailsLength" Write-Verbose "Character count of the details: $detailsLength"
if ($detailsLength -gt 32767) { if ($detailsLength -gt 32767) {
Write-Verbose "Warning: The character count exceeds the limit for Excel cells." Write-Verbose "Warning: The character count exceeds the limit for Excel cells."
} }
#$verbosePreference = 'SilentlyContinue'
return $auditResult return $auditResult
} }
} }

View File

@@ -4,10 +4,6 @@ function Test-SafeAttachmentsPolicy {
param () param ()
begin { begin {
# Dot source the class script if necessary
#. .\source\Classes\CISAuditResult.ps1
# Initialization code, if needed
$recnum = "2.1.4" $recnum = "2.1.4"
<# <#
@@ -71,8 +67,10 @@ function Test-SafeAttachmentsPolicy {
# The result is a pass if there are no failure reasons # The result is a pass if there are no failure reasons
$result = $failureReasons.Count -eq 0 $result = $failureReasons.Count -eq 0
# Format details for output # Format details for output manually
$detailsString = $details | Format-Table -AutoSize | Out-String $detailsString = "Policy|Enabled|Action|Failed`n" + ($details |
ForEach-Object {"$($_.Policy)|$($_.Enabled)|$($_.Action)|$($_.Failed)`n"}
)
$failureReasonsString = ($failureReasons | ForEach-Object { $_ }) -join ' ' $failureReasonsString = ($failureReasons | ForEach-Object { $_ }) -join ' '
# Create and populate the CISAuditResult object # Create and populate the CISAuditResult object
@@ -103,8 +101,8 @@ function Test-SafeAttachmentsPolicy {
Rec = $recnum Rec = $recnum
Result = $false Result = $false
Status = "Fail" Status = "Fail"
Details = "No M365 E5 licenses found." Details = "No Safe Attachments policies found."
FailureReason = "The audit is for M365 E5 licenses and the required EXO commands will not be available otherwise." FailureReason = "The audit needs Safe Attachment features available or required EXO commands will not be available otherwise."
} }
$auditResult = Initialize-CISAuditResult @params $auditResult = Initialize-CISAuditResult @params
} }

View File

@@ -1,405 +0,0 @@
$repoOwner = "CriticalSolutionsNetwork"
$repoName = "M365FoundationsCISReport"
$directoryPath = ".\source\tests"
$projectName = "Test Validation Project"
# Function to create GitHub issues
function Create-GitHubIssue {
param (
[string]$title,
[string]$body,
[string]$project
)
# Create the issue and add it to the specified project
$issue = gh issue create --repo "$repoOwner/$repoName" --title "$title" --body "$body" --project "$project"
return $issue
}
# Load test definitions from CSV
$testDefinitionsPath = ".\source\helper\TestDefinitions.csv"
$testDefinitions = Import-Csv -Path $testDefinitionsPath
# Iterate over each .ps1 file in the directory
Get-ChildItem -Path $directoryPath -Filter "*.ps1" | ForEach-Object {
$fileName = $_.Name
$testDefinition = $testDefinitions | Where-Object { $_.TestFileName -eq $fileName }
if ($testDefinition) {
$rec = $testDefinition.Rec
$elevel = $testDefinition.ELevel
$profileLevel = $testDefinition.ProfileLevel
$ig1 = $testDefinition.IG1
$ig2 = $testDefinition.IG2
$ig3 = $testDefinition.IG3
$connection = $testDefinition.Connection
$issueTitle = "Rec: $rec - Validate $fileName, ELevel: $elevel, ProfileLevel: $profileLevel, IG1: $ig1, IG2: $ig2, IG3: $ig3, Connection: $connection"
$issueBody = @"
# Validation for $fileName
## Tasks
- [ ] Validate test for a pass
- Description of passing criteria:
- [ ] Validate test for a fail
- Description of failing criteria:
- [ ] Add notes and observations
- Placeholder for additional notes:
"@
# Create the issue using GitHub CLI
try {
Create-GitHubIssue -title "$issueTitle" -body "$issueBody" -project "$projectName"
Write-Output "Created issue for $fileName"
}
catch {
Write-Error "Failed to create issue for $fileName`: $_"
}
# Introduce a delay of 2 seconds
Start-Sleep -Seconds 2
}
else {
Write-Warning "No matching test definition found for $fileName"
}
}
######################################
$repoOwner = "CriticalSolutionsNetwork"
$repoName = "M365FoundationsCISReport"
# Function to update GitHub issue
function Update-GitHubTIssue {
param (
[int]$issueNumber,
[string]$title,
[string]$body,
[string]$owner,
[string]$repositoryName
)
# Update the issue using Set-GitHubIssue
Set-GitHubIssue -OwnerName $owner -RepositoryName $repositoryName -Issue $issueNumber -Title $title -Body $body -Label @("documentation", "help wanted", "question") -Confirm:$false
}
# Load test definitions from CSV
$testDefinitionsPath = ".\source\helper\TestDefinitions.csv"
$testDefinitions = Import-Csv -Path $testDefinitionsPath
# Fetch existing issues that start with "Rec:"
$existingIssues = Get-GitHubIssue -OwnerName 'CriticalSolutionsNetwork' -RepositoryName 'M365FoundationsCISReport'
# Create a list to hold matched issues
$matchedIssues = @()
$warnings = @()
# Iterate over each existing issue
$existingIssues | ForEach-Object {
$issueNumber = $_.Number
$issueTitle = $_.Title
$issueBody = $_.Body
# Extract the rec number from the issue title
if ($issueTitle -match "Rec: (\d+\.\d+\.\d+)") {
$rec = $matches[1]
# Find the matching test definition based on rec number
$testDefinition = $testDefinitions | Where-Object { $_.Rec -eq $rec }
if ($testDefinition) {
# Create the new issue body
$newIssueBody = @"
# Validation for $($testDefinition.TestFileName)
## Recommendation Details
- **Recommendation**: $($testDefinition.Rec)
- **Description**: $($testDefinition.RecDescription)
- **ELevel**: $($testDefinition.ELevel)
- **Profile Level**: $($testDefinition.ProfileLevel)
- **CIS Control**: $($testDefinition.CISControl)
- **CIS Description**: $($testDefinition.CISDescription)
- **Implementation Group 1**: $($testDefinition.IG1)
- **Implementation Group 2**: $($testDefinition.IG2)
- **Implementation Group 3**: $($testDefinition.IG3)
- **Automated**: $($testDefinition.Automated)
- **Connection**: $($testDefinition.Connection)
## [$($testDefinition.TestFileName)](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/blob/main/source/tests/$($testDefinition.TestFileName))
## Tasks
### Validate recommendation details
- [ ] Confirm that the recommendation details are accurate and complete as per the CIS benchmark.
### Validate test for a pass
- [ ] Confirm that the automated test results align with the manual audit steps outlined in the CIS benchmark.
- Specific conditions to check:
- Condition A: (Detail about what constitutes Condition A)
- Condition B: (Detail about what constitutes Condition B)
- Condition C: (Detail about what constitutes Condition C)
### Validate test for a fail
- [ ] Confirm that the failure conditions in the automated test are consistent with the manual audit results.
- Specific conditions to check:
- Condition A: (Detail about what constitutes Condition A)
- Condition B: (Detail about what constitutes Condition B)
- Condition C: (Detail about what constitutes Condition C)
### Add notes and observations
- [ ] Compare the automated audit results with the manual audit steps and provide detailed observations.
- Automated audit produced info consistent with the manual audit test results? (Yes/No)
- Without disclosing any sensitive information, document any discrepancies between the actual output and the expected output.
- Document any error messages, removing any sensitive information before submitting.
- Identify the specific function, line, or section of the script that failed, if known.
- Provide any additional context or observations that might help in troubleshooting.
If needed, the helpers folder in .\source\helpers contains a CSV to assist with locating the test definition.
"@
# Add to matched issues list
$matchedIssues += [PSCustomObject]@{
IssueNumber = $issueNumber
Title = $issueTitle
NewBody = $newIssueBody
}
} else {
$warnings += "No matching test definition found for Rec: $rec"
}
} else {
$warnings += "No matching rec number found in issue title #$issueNumber"
}
}
# Display matched issues for confirmation
if ($matchedIssues.Count -gt 0) {
Write-Output "Matched Issues:"
$matchedIssues | ForEach-Object {
Write-Output $_.Title
}
$confirmation = Read-Host "Do you want to proceed with updating these issues? (yes/no)"
if ($confirmation -eq 'yes') {
# Update the issues
$matchedIssues | ForEach-Object {
try {
Update-GitHubTIssue -issueNumber $_.IssueNumber -title $_.Title -body $_.NewBody -owner $repoOwner -repositoryName $repoName
Write-Output "Updated issue #$($_.IssueNumber)"
} catch {
Write-Error "Failed to update issue #$($_.IssueNumber): $_"
}
# Introduce a delay of 2 seconds
Start-Sleep -Seconds 2
}
} else {
Write-Output "Update canceled by user."
}
} else {
Write-Output "No matched issues found to update."
}
# Display any warnings that were captured
if ($warnings.Count -gt 0) {
Write-Output "Warnings:"
$warnings | ForEach-Object {
Write-Output $_
}
}
# Test command to verify GitHub access
Get-GitHubRepository -OwnerName 'CriticalSolutionsNetwork' -RepositoryName 'M365FoundationsCISReport'
#########################################################################################
connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -NoWelcome
# Retrieve the subscribed SKUs
$sub = Get-MgSubscribedSku -All
# Define the product array
$ProductArray = @(
"Microsoft_Cloud_App_Security_App_Governance_Add_On",
"Defender_Threat_Intelligence",
"THREAT_INTELLIGENCE",
"WIN_DEF_ATP",
"Microsoft_Defender_for_Endpoint_F2",
"DEFENDER_ENDPOINT_P1",
"DEFENDER_ENDPOINT_P1_EDU",
"MDATP_XPLAT",
"MDATP_Server",
"ATP_ENTERPRISE_FACULTY",
"ATA",
"ATP_ENTERPRISE_GOV",
"ATP_ENTERPRISE_USGOV_GCCHIGH",
"THREAT_INTELLIGENCE_GOV",
"TVM_Premium_Standalone",
"TVM_Premium_Add_on",
"ATP_ENTERPRISE",
"Azure_Information_Protection_Premium_P1",
"Azure_Information_Protection_Premium_P2",
"Microsoft_Application_Protection_and_Governance",
"Exchange_Online_Protection",
"Microsoft_365_Defender",
"Cloud_App_Security_Discovery"
)
# Define the hashtable
$ProductHashTable = @{
"App governance add-on to Microsoft Defender for Cloud Apps" = "Microsoft_Cloud_App_Security_App_Governance_Add_On"
"Defender Threat Intelligence" = "Defender_Threat_Intelligence"
"Microsoft Defender for Office 365 (Plan 2)" = "THREAT_INTELLIGENCE"
"Microsoft Defender for Endpoint" = "WIN_DEF_ATP"
"Microsoft Defender for Endpoint F2" = "Microsoft_Defender_for_Endpoint_F2"
"Microsoft Defender for Endpoint P1" = "DEFENDER_ENDPOINT_P1"
"Microsoft Defender for Endpoint P1 for EDU" = "DEFENDER_ENDPOINT_P1_EDU"
"Microsoft Defender for Endpoint P2_XPLAT" = "MDATP_XPLAT"
"Microsoft Defender for Endpoint Server" = "MDATP_Server"
"Microsoft Defender for Office 365 (Plan 1) Faculty" = "ATP_ENTERPRISE_FACULTY"
"Microsoft Defender for Identity" = "ATA"
"Microsoft Defender for Office 365 (Plan 1) GCC" = "ATP_ENTERPRISE_GOV"
"Microsoft Defender for Office 365 (Plan 1)_USGOV_GCCHIGH" = "ATP_ENTERPRISE_USGOV_GCCHIGH"
"Microsoft Defender for Office 365 (Plan 2) GCC" = "THREAT_INTELLIGENCE_GOV"
"Microsoft Defender Vulnerability Management" = "TVM_Premium_Standalone"
"Microsoft Defender Vulnerability Management Add-on" = "TVM_Premium_Add_on"
"Microsoft Defender for Office 365 (Plan 1)" = "ATP_ENTERPRISE"
"Azure Information Protection Premium P1" = "Azure_Information_Protection_Premium_P1"
"Azure Information Protection Premium P2" = "Azure_Information_Protection_Premium_P2"
"Microsoft Application Protection and Governance" = "Microsoft_Application_Protection_and_Governance"
"Exchange Online Protection" = "Exchange_Online_Protection"
"Microsoft 365 Defender" = "Microsoft_365_Defender"
"Cloud App Security Discovery" = "Cloud_App_Security_Discovery"
}
# Reverse the hashtable
$ReverseProductHashTable = @{}
foreach ($key in $ProductHashTable.Keys) {
$ReverseProductHashTable[$ProductHashTable[$key]] = $key
}
# Loop through each SKU and get the enabled security features
$securityFeatures = foreach ($sku in $sub) {
if ($sku.SkuPartNumber -eq "MDATP_XPLAT_EDU") {
Write-Host "the SKU is: `n$($sku | gm)"
[PSCustomObject]@{
Skupartnumber = $sku.skupartnumber
AppliesTo = $sku.AppliesTo
ProvisioningStatus = $sku.ProvisioningStatus
ServicePlanId = $sku.ServicePlanId
ServicePlanName = $sku.ServicePlanName
FriendlyName = "Defender P2 for EDU"
}
}
else {
$sku.serviceplans | Where-Object { $_.serviceplanname -in $ProductArray } | ForEach-Object {
$friendlyName = $ReverseProductHashTable[$_.ServicePlanName]
[PSCustomObject]@{
Skupartnumber = $sku.skupartnumber
AppliesTo = $_.AppliesTo
ProvisioningStatus = $_.ProvisioningStatus
ServicePlanId = $_.ServicePlanId
ServicePlanName = $_.ServicePlanName
FriendlyName = $friendlyName
}
}
}
}
# Output the security features
$securityFeatures | Format-Table -AutoSize
##########
# Ensure the ImportExcel module is available
# Ensure the ImportExcel module is available
if (-not (Get-Module -ListAvailable -Name ImportExcel)) {
Install-Module -Name ImportExcel -Force -Scope CurrentUser
}
# Function to wait until the file is available
function Wait-ForFile {
param (
[string]$FilePath
)
while (Test-Path -Path $FilePath -PathType Leaf -and -not (Get-Content $FilePath -ErrorAction SilentlyContinue)) {
Start-Sleep -Seconds 1
}
}
# Path to the Excel file
$excelFilePath = "C:\Users\dougrios\OneDrive - CRITICALSOLUTIONS NET LLC\Documents\_Tools\Benchies\SKUs.xlsx"
# Wait for the file to be available
# Import the Excel file
$excelData = Import-Excel -Path $excelFilePath
# Retrieve the subscribed SKUs
$subscribedSkus = Get-MgSubscribedSku -All
# Define the hashtable with security-related product names
$ProductHashTable = @{
"App governance add-on to Microsoft Defender for Cloud Apps" = "Microsoft_Cloud_App_Security_App_Governance_Add_On"
"Defender Threat Intelligence" = "Defender_Threat_Intelligence"
"Microsoft Defender for Office 365 (Plan 2)" = "THREAT_INTELLIGENCE"
"Microsoft Defender for Endpoint" = "WIN_DEF_ATP"
"Microsoft Defender for Endpoint F2" = "Microsoft_Defender_for_Endpoint_F2"
"Microsoft Defender for Endpoint P1" = "DEFENDER_ENDPOINT_P1"
"Microsoft Defender for Endpoint P1 for EDU" = "DEFENDER_ENDPOINT_P1_EDU"
"Microsoft Defender for Endpoint P2_XPLAT" = "MDATP_XPLAT"
"Microsoft Defender for Endpoint Server" = "MDATP_Server"
"Microsoft Defender for Office 365 (Plan 1) Faculty" = "ATP_ENTERPRISE_FACULTY"
"Microsoft Defender for Identity" = "ATA"
"Microsoft Defender for Office 365 (Plan 1) GCC" = "ATP_ENTERPRISE_GOV"
"Microsoft Defender for Office 365 (Plan 1)_USGOV_GCCHIGH" = "ATP_ENTERPRISE_USGOV_GCCHIGH"
"Microsoft Defender for Office 365 (Plan 2) GCC" = "THREAT_INTELLIGENCE_GOV"
"Microsoft Defender Vulnerability Management" = "TVM_Premium_Standalone"
"Microsoft Defender Vulnerability Management Add-on" = "TVM_Premium_Add_on"
"Microsoft Defender for Office 365 (Plan 1)" = "ATP_ENTERPRISE"
"Azure Information Protection Premium P1" = "Azure_Information_Protection_Premium_P1"
"Azure Information Protection Premium P2" = "Azure_Information_Protection_Premium_P2"
"Microsoft Application Protection and Governance" = "Microsoft_Application_Protection_and_Governance"
"Exchange Online Protection" = "Exchange_Online_Protection"
"Microsoft 365 Defender" = "Microsoft_365_Defender"
"Cloud App Security Discovery" = "Cloud_App_Security_Discovery"
}
# Create a hashtable to store the SKU part numbers and their associated security features
$skuSecurityFeatures = @{}
# Populate the hashtable with data from the Excel file
foreach ($row in $excelData) {
if ($null -ne $row.'String ID' -and $null -ne $row.'Service plans included (friendly names)') {
$skuSecurityFeatures[$row.'String ID'] = $row.'Service plans included (friendly names)'
}
}
# Display the SKU part numbers and their associated security features
foreach ($sku in $subscribedSkus) {
$skuPartNumber = $sku.SkuPartNumber
if ($skuSecurityFeatures.ContainsKey($skuPartNumber)) {
$securityFeatures = $skuSecurityFeatures[$skuPartNumber]
# Check if the security feature is in the hashtable
$isSecurityFeature = $ProductHashTable.ContainsKey($securityFeatures)
if ($isSecurityFeature) {
Write-Output "SKU Part Number: $skuPartNumber"
Write-Output "Security Features: $securityFeatures (Security-related)"
} else {
Write-Output "SKU Part Number: $skuPartNumber"
Write-Output "Security Features: $securityFeatures"
}
Write-Output "----------------------------"
} else {
Write-Output "SKU Part Number: $skuPartNumber"
Write-Output "Security Features: Not Found in Excel"
Write-Output "----------------------------"
}
}

View File

@@ -0,0 +1,27 @@
$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
$(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
}).BaseName
Import-Module $ProjectName
InModuleScope $ProjectName {
Describe Get-PrivateFunction {
Context 'Default' {
BeforeEach {
$return = Get-PrivateFunction -PrivateData 'string'
}
It 'Returns a single object' {
($return | Measure-Object).Count | Should -Be 1
}
It 'Returns a string based on the parameter PrivateData' {
$return | Should -Be 'string'
}
}
}
}

View File

@@ -0,0 +1,27 @@
$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
$(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
}).BaseName
Import-Module $ProjectName
InModuleScope $ProjectName {
Describe Get-PrivateFunction {
Context 'Default' {
BeforeEach {
$return = Get-PrivateFunction -PrivateData 'string'
}
It 'Returns a single object' {
($return | Measure-Object).Count | Should -Be 1
}
It 'Returns a string based on the parameter PrivateData' {
$return | Should -Be 'string'
}
}
}
}

View File

@@ -1,31 +0,0 @@
BeforeAll {
$script:dscModuleName = 'M365FoundationsCISReport'
Import-Module -Name $script:dscModuleName
}
AfterAll {
# Unload the module being tested so that it doesn't impact any other tests.
Get-Module -Name $script:dscModuleName -All | Remove-Module -Force
}
Describe Get-PrivateFunction {
Context 'When calling the function with string value' {
It 'Should return a single object' {
InModuleScope -ModuleName $dscModuleName {
$return = Get-PrivateFunction -PrivateData 'string'
($return | Measure-Object).Count | Should -Be 1
}
}
It 'Should return a string based on the parameter PrivateData' {
InModuleScope -ModuleName $dscModuleName {
$return = Get-PrivateFunction -PrivateData 'string'
$return | Should -Be 'string'
}
}
}
}

View File

@@ -0,0 +1,27 @@
$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
$(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
}).BaseName
Import-Module $ProjectName
InModuleScope $ProjectName {
Describe Get-PrivateFunction {
Context 'Default' {
BeforeEach {
$return = Get-PrivateFunction -PrivateData 'string'
}
It 'Returns a single object' {
($return | Measure-Object).Count | Should -Be 1
}
It 'Returns a string based on the parameter PrivateData' {
$return | Should -Be 'string'
}
}
}
}

View File

@@ -0,0 +1,71 @@
BeforeAll {
$script:moduleName = '<% $PLASTER_PARAM_ModuleName %>'
# If the module is not found, run the build task 'noop'.
if (-not (Get-Module -Name $script:moduleName -ListAvailable))
{
# Redirect all streams to $null, except the error stream (stream 2)
& "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null
}
# Re-import the module using force to get any code changes between runs.
Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop'
$PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName
$PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName
$PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName
}
AfterAll {
$PSDefaultParameterValues.Remove('Mock:ModuleName')
$PSDefaultParameterValues.Remove('InModuleScope:ModuleName')
$PSDefaultParameterValues.Remove('Should:ModuleName')
Remove-Module -Name $script:moduleName
}
Describe Get-Something {
Context 'Return values' {
BeforeEach {
$return = Get-Something -Data 'value'
}
It 'Returns a single object' {
($return | Measure-Object).Count | Should -Be 1
}
}
Context 'Pipeline' {
It 'Accepts values from the pipeline by value' {
$return = 'value1', 'value2' | Get-Something
$return[0] | Should -Be 'value1'
$return[1] | Should -Be 'value2'
}
It 'Accepts value from the pipeline by property name' {
$return = 'value1', 'value2' | ForEach-Object {
[PSCustomObject]@{
Data = $_
OtherProperty = 'other'
}
} | Get-Something
$return[0] | Should -Be 'value1'
$return[1] | Should -Be 'value2'
}
}
Context 'ShouldProcess' {
It 'Supports WhatIf' {
(Get-Command Get-Something).Parameters.ContainsKey('WhatIf') | Should -Be $true
{ Get-Something -Data 'value' -WhatIf } | Should -Not -Throw
}
}
}

View File

@@ -0,0 +1,71 @@
BeforeAll {
$script:moduleName = '<% $PLASTER_PARAM_ModuleName %>'
# If the module is not found, run the build task 'noop'.
if (-not (Get-Module -Name $script:moduleName -ListAvailable))
{
# Redirect all streams to $null, except the error stream (stream 2)
& "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null
}
# Re-import the module using force to get any code changes between runs.
Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop'
$PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName
$PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName
$PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName
}
AfterAll {
$PSDefaultParameterValues.Remove('Mock:ModuleName')
$PSDefaultParameterValues.Remove('InModuleScope:ModuleName')
$PSDefaultParameterValues.Remove('Should:ModuleName')
Remove-Module -Name $script:moduleName
}
Describe Get-Something {
Context 'Return values' {
BeforeEach {
$return = Get-Something -Data 'value'
}
It 'Returns a single object' {
($return | Measure-Object).Count | Should -Be 1
}
}
Context 'Pipeline' {
It 'Accepts values from the pipeline by value' {
$return = 'value1', 'value2' | Get-Something
$return[0] | Should -Be 'value1'
$return[1] | Should -Be 'value2'
}
It 'Accepts value from the pipeline by property name' {
$return = 'value1', 'value2' | ForEach-Object {
[PSCustomObject]@{
Data = $_
OtherProperty = 'other'
}
} | Get-Something
$return[0] | Should -Be 'value1'
$return[1] | Should -Be 'value2'
}
}
Context 'ShouldProcess' {
It 'Supports WhatIf' {
(Get-Command Get-Something).Parameters.ContainsKey('WhatIf') | Should -Be $true
{ Get-Something -Data 'value' -WhatIf } | Should -Not -Throw
}
}
}