Merge pull request #115 from CriticalSolutionsNetwork/Add-QoL-Features
Add qol features
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ markdownissues.txt
|
||||
node_modules
|
||||
package-lock.json
|
||||
Aligned.xlsx
|
||||
test-gh1.ps1
|
10
CHANGELOG.md
10
CHANGELOG.md
@@ -6,6 +6,16 @@ The format is based on and uses the types of changes according to [Keep a Change
|
||||
|
||||
### 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.
|
||||
|
||||
### Fixed
|
||||
|
BIN
docs/index.html
BIN
docs/index.html
Binary file not shown.
@@ -4,7 +4,7 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1
|
||||
|
||||
|
||||
<#
|
||||
$ver = "v0.1.10"
|
||||
$ver = "v0.1.11"
|
||||
git checkout main
|
||||
git pull origin main
|
||||
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 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"
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
113
source/Private/Get-Action.ps1
Normal file
113
source/Private/Get-Action.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
54
source/Private/Get-ExceededLengthResultDetail.ps1
Normal file
54
source/Private/Get-ExceededLengthResultDetail.ps1
Normal 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
|
||||
}
|
||||
}
|
36
source/Private/Initialize-LargeTestTable.ps1
Normal file
36
source/Private/Initialize-LargeTestTable.ps1
Normal 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
|
||||
}
|
||||
}
|
196
source/Public/Export-M365SecurityAuditTable.ps1
Normal file
196
source/Public/Export-M365SecurityAuditTable.ps1
Normal 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."
|
||||
}
|
||||
}
|
@@ -25,7 +25,6 @@
|
||||
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Get-AdminRoleUserLicense
|
||||
#>
|
||||
function Get-AdminRoleUserLicense {
|
||||
# Set output type to System.Collections.ArrayList
|
||||
[OutputType([System.Collections.ArrayList])]
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
@@ -42,33 +41,37 @@ function Get-AdminRoleUserLicense {
|
||||
$userIds = [System.Collections.ArrayList]::new()
|
||||
}
|
||||
|
||||
Process {
|
||||
$adminroles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Admin*" }
|
||||
process {
|
||||
Write-Verbose "Retrieving all admin roles"
|
||||
$adminRoleNames = (Get-MgDirectoryRole | Where-Object { $null -ne $_.RoleTemplateId }).DisplayName
|
||||
|
||||
foreach ($role in $adminroles) {
|
||||
$usersInRole = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'"
|
||||
Write-Verbose "Filtering admin roles"
|
||||
$adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { ($adminRoleNames -contains $_.DisplayName) -and ($_.DisplayName -ne "Directory Synchronization Accounts") }
|
||||
|
||||
foreach ($user in $usersInRole) {
|
||||
$userDetails = Get-MgUser -UserId $user.PrincipalId -Property "DisplayName, UserPrincipalName, Id, onPremisesSyncEnabled" -ErrorAction SilentlyContinue
|
||||
foreach ($role in $adminRoles) {
|
||||
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) {
|
||||
[void]($userIds.Add($user.PrincipalId))
|
||||
[void](
|
||||
$adminRoleUsers.Add(
|
||||
[PSCustomObject]@{
|
||||
Write-Verbose "Retrieved user details for: $($userDetails.UserPrincipalName)"
|
||||
[void]($userIds.Add($userDetails.Id))
|
||||
[void]($adminRoleUsers.Add([PSCustomObject]@{
|
||||
RoleName = $role.DisplayName
|
||||
UserName = $userDetails.DisplayName
|
||||
UserPrincipalName = $userDetails.UserPrincipalName
|
||||
UserId = $userDetails.Id
|
||||
HybridUser = $userDetails.onPremisesSyncEnabled
|
||||
HybridUser = [bool]$userDetails.OnPremisesSyncEnabled
|
||||
Licenses = $null # Initialize as $null
|
||||
}
|
||||
)
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Retrieving licenses for admin role users"
|
||||
foreach ($userId in $userIds.ToArray() | Select-Object -Unique) {
|
||||
$licenses = Get-MgUserLicenseDetail -UserId $userId -ErrorAction SilentlyContinue
|
||||
if ($licenses) {
|
||||
@@ -80,7 +83,7 @@ function Get-AdminRoleUserLicense {
|
||||
}
|
||||
}
|
||||
|
||||
End {
|
||||
end {
|
||||
Write-Host "Disconnecting from Microsoft Graph..." -ForegroundColor Green
|
||||
Disconnect-MgGraph | Out-Null
|
||||
return $adminRoleUsers
|
||||
|
@@ -286,6 +286,16 @@ function Invoke-M365SecurityAudit {
|
||||
# Call the private function to calculate and display results
|
||||
Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
34
source/Public/Remove-RowsWithEmptyCSVStatus.ps1
Normal file
34
source/Public/Remove-RowsWithEmptyCSVStatus.ps1
Normal 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"
|
||||
}
|
@@ -20,7 +20,12 @@ function Test-AdministrativeAccountCompliance {
|
||||
try {
|
||||
# Retrieve 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 = @()
|
||||
|
||||
# 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
|
||||
$hasInvalidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -notcontains $_ }
|
||||
$invalidLicenses = $licenses.SkuPartNumber | Where-Object { $validLicenses -notcontains $_ }
|
||||
$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
|
||||
$adminRoleUsers += [PSCustomObject]@{
|
||||
@@ -95,13 +101,14 @@ function Test-AdministrativeAccountCompliance {
|
||||
$failureReasons = $failureReasons -join "`n"
|
||||
$failureReason = if ($nonCompliantUsers) {
|
||||
"Non-Compliant Accounts: $($nonCompliantUsers.Count)"
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
"Compliant Accounts: $($uniqueAdminRoleUsers.Count)"
|
||||
}
|
||||
|
||||
$result = $nonCompliantUsers.Count -eq 0
|
||||
$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"
|
||||
|
||||
|
@@ -64,7 +64,7 @@ function Test-BlockMailForwarding {
|
||||
if ($nonCompliantSpamPoliciesArray.Count -gt 0) {
|
||||
# Fail Condition B
|
||||
$failureReasons += "Outbound spam policies allowing automatic forwarding found."
|
||||
$details += "Outbound Spam Policies Details:`nPolicy|AutoForwardingMode"
|
||||
$details += "Policy|AutoForwardingMode"
|
||||
$details += $nonCompliantSpamPoliciesArray | ForEach-Object {
|
||||
"$($_.Name)|$($_.AutoForwardingMode)"
|
||||
}
|
||||
|
@@ -30,20 +30,21 @@ function Test-MailboxAuditingE3 {
|
||||
#. .\source\Classes\CISAuditResult.ps1
|
||||
|
||||
$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")
|
||||
$OwnerActions = @("ApplyRecord", "Create", "HardDelete", "MailboxLogin", "Move", "MoveToDeletedItems", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules")
|
||||
|
||||
$actionDictionaries = Get-Action -Dictionaries
|
||||
# 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 = @()
|
||||
#$allUsers = Get-AzureADUser -All $true
|
||||
$founde3Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e3SkuPartNumber }
|
||||
$processedUsers = @{} # Dictionary to track processed users
|
||||
$recnum = "6.1.2"
|
||||
}
|
||||
|
||||
|
||||
process {
|
||||
if (($founde3Sku.count)-ne 0) {
|
||||
if ($founde3Sku.Count -ne 0) {
|
||||
$allUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde3Sku.SkuId) )" -All
|
||||
$mailboxes = Get-EXOMailbox -PropertySets Audit
|
||||
try {
|
||||
@@ -53,36 +54,36 @@ function Test-MailboxAuditingE3 {
|
||||
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
|
||||
$mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $user.UserPrincipalName }
|
||||
|
||||
$missingActions = @()
|
||||
$missingAdminActions = @()
|
||||
$missingDelegateActions = @()
|
||||
$missingOwnerActions = @()
|
||||
|
||||
if ($mailbox.AuditEnabled) {
|
||||
foreach ($action in $AdminActions) {
|
||||
# Condition B: Checking if the `AuditAdmin` actions include required actions
|
||||
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" }
|
||||
if ($mailbox.AuditAdmin -notcontains $action) {
|
||||
$missingAdminActions += (Get-Action -Actions $action -ActionType "Admin")
|
||||
}
|
||||
}
|
||||
foreach ($action in $DelegateActions) {
|
||||
# Condition C: Checking if the `AuditDelegate` actions include required actions
|
||||
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" }
|
||||
if ($mailbox.AuditDelegate -notcontains $action) {
|
||||
$missingDelegateActions += (Get-Action -Actions $action -ActionType "Delegate")
|
||||
}
|
||||
}
|
||||
foreach ($action in $OwnerActions) {
|
||||
# Condition D: Checking if the `AuditOwner` actions include required actions
|
||||
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" }
|
||||
if ($mailbox.AuditOwner -notcontains $action) {
|
||||
$missingOwnerActions += (Get-Action -Actions $action -ActionType "Owner")
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingActions.Count -gt 0) {
|
||||
$formattedActions = Format-MissingAction -missingActions $missingActions
|
||||
$allFailures += "$userUPN|True|$($formattedActions.Admin)|$($formattedActions.Delegate)|$($formattedActions.Owner)"
|
||||
if ($missingAdminActions.Count -gt 0 -or $missingDelegateActions.Count -gt 0 -or $missingOwnerActions.Count -gt 0) {
|
||||
$allFailures += "$userUPN|True|$($missingAdminActions -join ',')|$($missingDelegateActions -join ',')|$($missingOwnerActions -join ',')"
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Condition A: Checking if mailbox audit logging is enabled
|
||||
$allFailures += "$userUPN|False|||"
|
||||
$allFailures += "$userUPN|False|||" # Condition A for fail
|
||||
}
|
||||
|
||||
# Mark the user as processed
|
||||
@@ -90,7 +91,12 @@ function Test-MailboxAuditingE3 {
|
||||
}
|
||||
|
||||
# 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) {
|
||||
"All Office E3 users have correct mailbox audit settings."
|
||||
}
|
||||
@@ -134,14 +140,13 @@ function Test-MailboxAuditingE3 {
|
||||
}
|
||||
|
||||
end {
|
||||
#$verbosePreference = 'Continue'
|
||||
$detailsLength = $details.Length
|
||||
Write-Verbose "Character count of the details: $detailsLength"
|
||||
|
||||
if ($detailsLength -gt 32767) {
|
||||
Write-Verbose "Warning: The character count exceeds the limit for Excel cells."
|
||||
}
|
||||
#$verbosePreference = 'SilentlyContinue'
|
||||
|
||||
return $auditResult
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
|
||||
$e5SkuPartNumber = "SPE_E5"
|
||||
$AdminActions = @("ApplyRecord", "Copy", "Create", "FolderBind", "HardDelete", "MailItemsAccessed", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "Send", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules")
|
||||
$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")
|
||||
$founde5Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber }
|
||||
|
||||
$actionDictionaries = Get-Action -Dictionaries
|
||||
$AdminActions = $actionDictionaries.AdminActions.Keys
|
||||
$DelegateActions = $actionDictionaries.DelegateActions.Keys
|
||||
$OwnerActions = $actionDictionaries.OwnerActions.Keys
|
||||
|
||||
$allFailures = @()
|
||||
#$allUsers = Get-AzureADUser -All $true
|
||||
$founde5Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber }
|
||||
$processedUsers = @{} # Dictionary to track processed users
|
||||
$processedUsers = @{}
|
||||
$recnum = "6.1.3"
|
||||
}
|
||||
|
||||
@@ -50,35 +51,39 @@ function Test-MailboxAuditingE5 {
|
||||
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 }
|
||||
$userUPN = $user.UserPrincipalName
|
||||
#$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit
|
||||
|
||||
$missingActions = @()
|
||||
$missingAdminActions = @()
|
||||
$missingDelegateActions = @()
|
||||
$missingOwnerActions = @()
|
||||
|
||||
if ($mailbox.AuditEnabled) {
|
||||
# Validate Admin actions
|
||||
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
|
||||
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
|
||||
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) {
|
||||
$formattedActions = Format-MissingAction -missingActions $missingActions
|
||||
$allFailures += "$userUPN|True|$($formattedActions.Admin)|$($formattedActions.Delegate)|$($formattedActions.Owner)"
|
||||
if ($missingAdminActions.Count -gt 0 -or $missingDelegateActions.Count -gt 0 -or $missingOwnerActions.Count -gt 0) {
|
||||
$allFailures += "$userUPN|True|$($missingAdminActions -join ',')|$($missingDelegateActions -join ',')|$($missingOwnerActions -join ',')"
|
||||
}
|
||||
}
|
||||
else {
|
||||
$allFailures += "$userUPN|False|||"
|
||||
$allFailures += "$userUPN|False|||" # Condition A for fail
|
||||
}
|
||||
|
||||
# Mark the user as processed
|
||||
@@ -86,14 +91,19 @@ function Test-MailboxAuditingE5 {
|
||||
}
|
||||
|
||||
# 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) {
|
||||
"All Office E5 users have correct mailbox audit settings." # Condition A for pass
|
||||
}
|
||||
else {
|
||||
"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
|
||||
$params = @{
|
||||
Rec = $recnum
|
||||
@@ -130,14 +140,13 @@ function Test-MailboxAuditingE5 {
|
||||
}
|
||||
|
||||
end {
|
||||
#$verbosePreference = 'Continue'
|
||||
$detailsLength = $details.Length
|
||||
Write-Verbose "Character count of the details: $detailsLength"
|
||||
|
||||
if ($detailsLength -gt 32767) {
|
||||
Write-Verbose "Warning: The character count exceeds the limit for Excel cells."
|
||||
}
|
||||
#$verbosePreference = 'SilentlyContinue'
|
||||
|
||||
return $auditResult
|
||||
}
|
||||
}
|
@@ -4,10 +4,6 @@ function Test-SafeAttachmentsPolicy {
|
||||
param ()
|
||||
|
||||
begin {
|
||||
# Dot source the class script if necessary
|
||||
#. .\source\Classes\CISAuditResult.ps1
|
||||
|
||||
# Initialization code, if needed
|
||||
$recnum = "2.1.4"
|
||||
|
||||
<#
|
||||
@@ -71,8 +67,10 @@ function Test-SafeAttachmentsPolicy {
|
||||
# The result is a pass if there are no failure reasons
|
||||
$result = $failureReasons.Count -eq 0
|
||||
|
||||
# Format details for output
|
||||
$detailsString = $details | Format-Table -AutoSize | Out-String
|
||||
# Format details for output manually
|
||||
$detailsString = "Policy|Enabled|Action|Failed`n" + ($details |
|
||||
ForEach-Object {"$($_.Policy)|$($_.Enabled)|$($_.Action)|$($_.Failed)`n"}
|
||||
)
|
||||
$failureReasonsString = ($failureReasons | ForEach-Object { $_ }) -join ' '
|
||||
|
||||
# Create and populate the CISAuditResult object
|
||||
@@ -103,8 +101,8 @@ function Test-SafeAttachmentsPolicy {
|
||||
Rec = $recnum
|
||||
Result = $false
|
||||
Status = "Fail"
|
||||
Details = "No M365 E5 licenses found."
|
||||
FailureReason = "The audit is for M365 E5 licenses and the required EXO commands will not be available otherwise."
|
||||
Details = "No Safe Attachments policies found."
|
||||
FailureReason = "The audit needs Safe Attachment features available or required EXO commands will not be available otherwise."
|
||||
}
|
||||
$auditResult = Initialize-CISAuditResult @params
|
||||
}
|
||||
|
405
test-gh.ps1
405
test-gh.ps1
@@ -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 "----------------------------"
|
||||
}
|
||||
}
|
27
tests/Unit/Private/Get-Action.tests.ps1
Normal file
27
tests/Unit/Private/Get-Action.tests.ps1
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
27
tests/Unit/Private/Get-ExceededLengthResultDetail.tests.ps1
Normal file
27
tests/Unit/Private/Get-ExceededLengthResultDetail.tests.ps1
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
27
tests/Unit/Private/Initialize-LargeTestTable.tests.ps1
Normal file
27
tests/Unit/Private/Initialize-LargeTestTable.tests.ps1
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
71
tests/Unit/Public/Export-M365SecurityAuditTable.tests.ps1
Normal file
71
tests/Unit/Public/Export-M365SecurityAuditTable.tests.ps1
Normal 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
71
tests/Unit/Public/Remove-RowsWithEmptyCSVStatus.tests.ps1
Normal file
71
tests/Unit/Public/Remove-RowsWithEmptyCSVStatus.tests.ps1
Normal 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user