15 Commits

Author SHA1 Message Date
Doug Rios
f85101d0de Merge pull request #108 from CriticalSolutionsNetwork/release-branch
fix: working and verbose confirmation included
2024-06-10 13:00:33 -05:00
DrIOS
f880e566ea fix: working and verbose confirmation included 2024-06-10 12:58:50 -05:00
Doug Rios
7041b0ba52 Merge pull request #107 from CriticalSolutionsNetwork/Bugfix-1.1.1
Bugfix 1.1.1
2024-06-10 12:55:48 -05:00
DrIOS
1161baffad fix: working and verbose confirmation included 2024-06-10 12:31:22 -05:00
DrIOS
032c951e02 fix: working but needs tuning 2024-06-10 11:55:19 -05:00
DrIOS
6ed99dbacf fix: Comments steps 2024-06-10 09:56:42 -05:00
DrIOS
30c848e74d fix: Revert script to oringinal for 1.1.1 2024-06-10 09:42:17 -05:00
DrIOS
40193bd492 docs: Update git issue build 2024-06-09 14:06:34 -05:00
DrIOS
5c868a20fc docs: Fomatting changes 2024-06-09 10:54:34 -05:00
Doug Rios
4db0fd3742 Merge pull request #100 from CriticalSolutionsNetwork/Whatif-Bugfix
fix: whatif
2024-06-09 10:42:00 -05:00
DrIOS
83a8e31aa5 docs: Update CHANGELOG 2024-06-09 10:38:56 -05:00
DrIOS
b9de0638bb add: Output type to functions 2024-06-09 10:36:37 -05:00
DrIOS
5a0475c253 docs: update CHANGELOG.md 2024-06-09 09:50:55 -05:00
DrIOS
312aabc81c fix: whatif output and module install 2024-06-09 09:40:18 -05:00
DrIOS
e6da6d9d47 fix: whatif 2024-06-08 20:42:38 -05:00
26 changed files with 507 additions and 117 deletions

View File

@@ -4,6 +4,27 @@ The format is based on and uses the types of changes according to [Keep a Change
## [Unreleased]
### Fixed
- Fixed bug in 1.1.1 that caused the test to fail/pass incorrectly. Added verbose output.
### Docs
- Updated helper csv formatting for one cis control.
## [0.1.8] - 2024-06-09
### Added
- Added output type to functions.
### Fixed
- Whatif support for `Invoke-M365SecurityAudit`.
- Whatif module output and module install process.
## [0.1.7] - 2024-06-08
### Added
- Added pipeline support to `Sync-CISExcelAndCsvData` function for `[CISAuditResult[]]` input.
@@ -16,6 +37,12 @@ The format is based on and uses the types of changes according to [Keep a Change
- Enhanced `Invoke-M365SecurityAudit` to allow flexible inclusion and exclusion of specific recommendations, IG filters, and profile levels.
- SupportsShoudProcess to also bypass connection checks in `Invoke-M365SecurityAudit` as well as Disconnect-M365Suite.
## [0.1.6] - 2024-06-08
### Added
- Added pipeline support to `Sync-CISExcelAndCsvData` function for `[CISAuditResult[]]` input.
## [0.1.5] - 2024-06-08
### Added

View File

@@ -4,7 +4,7 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1
<#
$ver = "v0.1.6"
$ver = "v0.1.8"
git checkout main
git pull origin main
git tag -a $ver -m "Release version $ver refactor Update"

View File

@@ -1,29 +1,33 @@
function Assert-ModuleAvailability {
[OutputType([void]) ]
param(
[string]$ModuleName,
[string]$RequiredVersion,
[string]$SubModuleName
[string[]]$SubModules = @()
)
try {
$module = Get-Module -ListAvailable -Name $ModuleName | Where-Object { $_.Version -ge [version]$RequiredVersion }
if ($null -eq $module) {$auditResult.Profile
Write-Host "Installing $ModuleName module..."
if ($null -eq $module) {
Write-Information "Installing $ModuleName module..." -InformationAction Continue
Install-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force -AllowClobber -Scope CurrentUser | Out-Null
}
elseif ($module.Version -lt [version]$RequiredVersion) {
Write-Host "Updating $ModuleName module to required version..."
Write-Information "Updating $ModuleName module to required version..." -InformationAction Continue
Update-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force | Out-Null
}
else {
Write-Host "$ModuleName module is already at required version or newer."
Write-Information "$ModuleName module is already at required version or newer." -InformationAction Continue
}
if ($SubModuleName) {
Import-Module -Name "$ModuleName.$SubModuleName" -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null
if ($SubModules.Count -gt 0) {
foreach ($subModule in $SubModules) {
Write-Information "Importing submodule $ModuleName.$subModule..." -InformationAction Continue
Import-Module -Name "$ModuleName.$subModule" -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null
}
else {
} else {
Write-Information "Importing module $ModuleName..." -InformationAction Continue
Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null
}
}

View File

@@ -1,4 +1,5 @@
function Connect-M365Suite {
[OutputType([void])]
[CmdletBinding()]
param (
[Parameter(Mandatory=$false)]

View File

@@ -1,4 +1,5 @@
function Disconnect-M365Suite {
[OutputType([void])]
param (
[Parameter(Mandatory)]
[string[]]$RequiredConnections

View File

@@ -1,5 +1,9 @@
function Format-MissingAction {
param ([array]$missingActions)
[CmdletBinding()]
[OutputType([hashtable])]
param (
[array]$missingActions
)
$actionGroups = @{
"Admin" = @()

View File

@@ -0,0 +1,19 @@
function Format-RequiredModuleList {
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(Mandatory = $true)]
[System.Object[]]$RequiredModules
)
$requiredModulesFormatted = ""
foreach ($module in $RequiredModules) {
if ($module.SubModules -and $module.SubModules.Count -gt 0) {
$subModulesFormatted = $module.SubModules -join ', '
$requiredModulesFormatted += "$($module.ModuleName) (SubModules: $subModulesFormatted), "
} else {
$requiredModulesFormatted += "$($module.ModuleName), "
}
}
return $requiredModulesFormatted.TrimEnd(", ")
}

View File

@@ -1,4 +1,6 @@
function Get-MostCommonWord {
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(Mandatory = $true)]
[string[]]$InputStrings

View File

@@ -12,22 +12,16 @@ function Get-RequiredModule {
switch ($PSCmdlet.ParameterSetName) {
'AuditFunction' {
return @(
@{ ModuleName = "ExchangeOnlineManagement"; RequiredVersion = "3.3.0" },
@{ ModuleName = "AzureAD"; RequiredVersion = "2.0.2.182" },
@{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModuleName = "Authentication" },
@{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModuleName = "Users" },
@{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModuleName = "Groups" },
@{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModuleName = "DirectoryObjects" },
@{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModuleName = "Domains" },
@{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModuleName = "Reports" },
@{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModuleName = "Mail" },
@{ ModuleName = "Microsoft.Online.SharePoint.PowerShell"; RequiredVersion = "16.0.24009.12000" },
@{ ModuleName = "MicrosoftTeams"; RequiredVersion = "5.5.0" }
@{ ModuleName = "ExchangeOnlineManagement"; RequiredVersion = "3.3.0"; SubModules = @() },
@{ ModuleName = "AzureAD"; RequiredVersion = "2.0.2.182"; SubModules = @() },
@{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModules = @("Groups", "DeviceManagement", "Users", "Identity.DirectoryManagement", "Identity.SignIns") },
@{ ModuleName = "Microsoft.Online.SharePoint.PowerShell"; RequiredVersion = "16.0.24009.12000"; SubModules = @() },
@{ ModuleName = "MicrosoftTeams"; RequiredVersion = "5.5.0"; SubModules = @() }
)
}
'SyncFunction' {
return @(
@{ ModuleName = "ImportExcel"; RequiredVersion = "7.8.9" }
@{ ModuleName = "ImportExcel"; RequiredVersion = "7.8.9"; SubModules = @() }
)
}
default {

View File

@@ -1,4 +1,6 @@
function Get-TestDefinitionsObject {
[CmdletBinding()]
[OutputType([object[]])]
param (
[Parameter(Mandatory = $true)]
[object[]]$TestDefinitions,

View File

@@ -0,0 +1,28 @@
function Get-UniqueConnection {
[CmdletBinding()]
[OutputType([string[]])]
param (
[Parameter(Mandatory = $true)]
[string[]]$Connections
)
$uniqueConnections = @()
if ($Connections -contains "AzureAD" -or $Connections -contains "AzureAD | EXO" -or $Connections -contains "AzureAD | EXO | Microsoft Graph") {
$uniqueConnections += "AzureAD"
}
if ($Connections -contains "Microsoft Graph" -or $Connections -contains "AzureAD | EXO | Microsoft Graph") {
$uniqueConnections += "Microsoft Graph"
}
if ($Connections -contains "EXO" -or $Connections -contains "AzureAD | EXO" -or $Connections -contains "Microsoft Teams | EXO" -or $Connections -contains "AzureAD | EXO | Microsoft Graph") {
$uniqueConnections += "EXO"
}
if ($Connections -contains "SPO") {
$uniqueConnections += "SPO"
}
if ($Connections -contains "Microsoft Teams" -or $Connections -contains "Microsoft Teams | EXO") {
$uniqueConnections += "Microsoft Teams"
}
return $uniqueConnections | Sort-Object -Unique
}

View File

@@ -1,5 +1,6 @@
function Initialize-CISAuditResult {
[CmdletBinding()]
[OutputType([CISAuditResult])]
param (
[Parameter(Mandatory = $true)]
[string]$Rec,

View File

@@ -1,4 +1,5 @@
function Invoke-TestFunction {
[OutputType([CISAuditResult[]])]
param (
[Parameter(Mandatory = $true)]
[PSObject]$FunctionFile,

View File

@@ -1,4 +1,5 @@
function Measure-AuditResult {
[OutputType([void])]
param (
[Parameter(Mandatory = $true)]
[System.Collections.ArrayList]$AllAuditResults,

View File

@@ -1,5 +1,6 @@
function Merge-CISExcelAndCsvData {
[CmdletBinding(DefaultParameterSetName = 'CsvInput')]
[OutputType([PSCustomObject[]])]
param (
[Parameter(Mandatory = $true)]
[string]$ExcelPath,

View File

@@ -1,4 +1,6 @@
function New-MergedObject {
[CmdletBinding()]
[OutputType([PSCustomObject])]
param (
[Parameter(Mandatory = $true)]
[psobject]$ExcelItem,

View File

@@ -1,4 +1,5 @@
function Update-CISExcelWorksheet {
[OutputType([void])]
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]

View File

@@ -1,4 +1,5 @@
function Update-WorksheetCell {
function Update-WorksheetCell {
[OutputType([void])]
param (
$Worksheet,
$Data,
@@ -25,4 +26,4 @@
}
$rowIndex++
}
}
}

View File

@@ -114,7 +114,6 @@
.LINK
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Invoke-M365SecurityAudit
#>
function Invoke-M365SecurityAudit {
[CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Default')]
[OutputType([CISAuditResult[]])]
@@ -183,12 +182,18 @@ function Invoke-M365SecurityAudit {
$script:MaximumFunctionCount = 8192
}
# Ensure required modules are installed
if (!($NoModuleCheck) -and $PSCmdlet.ShouldProcess("Check for required modules", "Check")) {
$requiredModules = Get-RequiredModule -AuditFunction
# Format the required modules list
$requiredModulesFormatted = Format-RequiredModuleList -RequiredModules $requiredModules
# Check and install required modules if necessary
if (!($NoModuleCheck) -and $PSCmdlet.ShouldProcess("Check for required modules: $requiredModulesFormatted", "Check")) {
foreach ($module in $requiredModules) {
Assert-ModuleAvailability -ModuleName $module.ModuleName -RequiredVersion $module.RequiredVersion -SubModuleName $module.SubModuleName
Assert-ModuleAvailability -ModuleName $module.ModuleName -RequiredVersion $module.RequiredVersion -SubModules $module.SubModules
}
}
# Load test definitions from CSV
$testDefinitionsPath = Join-Path -Path $PSScriptRoot -ChildPath "helper\TestDefinitions.csv"
$testDefinitions = Import-Csv -Path $testDefinitionsPath
@@ -207,7 +212,7 @@ function Invoke-M365SecurityAudit {
$testDefinitions = Get-TestDefinitionsObject @params
# Extract unique connections needed
$requiredConnections = $testDefinitions.Connection | Sort-Object -Unique
if ($requiredConnections -contains 'SPO'){
if ($requiredConnections -contains 'SPO') {
if (-not $TenantAdminUrl) {
$requiredConnections = $requiredConnections | Where-Object { $_ -ne 'SPO' }
$testDefinitions = $testDefinitions | Where-Object { $_.Connection -ne 'SPO' }
@@ -235,10 +240,14 @@ function Invoke-M365SecurityAudit {
$currentTestIndex = 0
# Establishing connections if required
if (!($DoNotConnect) -and $PSCmdlet.ShouldProcess("Establish connections to Microsoft 365 services", "Connect")) {
$actualUniqueConnections = Get-UniqueConnection -Connections $requiredConnections
if (!($DoNotConnect) -and $PSCmdlet.ShouldProcess("Establish connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Connect")) {
Write-Information "Establishing connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')" -InformationAction Continue
Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections
}
Write-Information "A total of $($totalTests) tests were selected to run..." -InformationAction Continue
# Import the test functions
$testFiles | ForEach-Object {
$currentTestIndex++
@@ -269,11 +278,11 @@ function Invoke-M365SecurityAudit {
}
End {
if (!($DoNotDisconnect) -and $PSCmdlet.ShouldProcess("Disconnect from Microsoft 365 services", "Disconnect")) {
if (!($DoNotDisconnect) -and $PSCmdlet.ShouldProcess("Disconnect from Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Disconnect")) {
# Clean up sessions
Disconnect-M365Suite -RequiredConnections $requiredConnections
}
if ($PSCmdlet.ShouldProcess("Measure and display audit results", "Measure")) {
if ($PSCmdlet.ShouldProcess("Measure and display audit results for $($totalTests) tests", "Measure")) {
# Call the private function to calculate and display results
Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests
# Return all collected audit results
@@ -281,3 +290,4 @@ function Invoke-M365SecurityAudit {
}
}
}

View File

@@ -44,6 +44,7 @@ If the SkipUpdate switch is used, the function returns an array of custom object
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Sync-CISExcelAndCsvData
#>
function Sync-CISExcelAndCsvData {
[OutputType([void], [PSCustomObject[]])]
[CmdletBinding(DefaultParameterSetName = 'CsvInput')]
param (
[Parameter(Mandatory = $true)]

View File

@@ -10,7 +10,7 @@
9,Test-CommonAttachmentFilter.ps1,2.1.2,Ensure the Common Attachment Types Filter is enabled,E3,L1,9.6,Block Unnecessary File Types,FALSE,TRUE,TRUE,TRUE,EXO
10,Test-NotifyMalwareInternal.ps1,2.1.3,Ensure notifications for internal users sending malware is Enabled,E3,L1,17.5,Assign Key Roles and Responsibilities,FALSE,TRUE,TRUE,TRUE,EXO
11,Test-SafeAttachmentsPolicy.ps1,2.1.4,Ensure Safe Attachments policy is enabled,E5,L2,9.7,Deploy and Maintain Email Server Anti-Malware Protections,FALSE,FALSE,TRUE,TRUE,EXO
12,Test-SafeAttachmentsTeams.ps1,2.1.5,"Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled",E5,L2,"9.7, 10.1","Deploy and Maintain Email Server Anti-Malware Protections, Deploy and Maintain Anti-Malware Software",TRUE,TRUE,TRUE,TRUE,EXO
12,Test-SafeAttachmentsTeams.ps1,2.1.5,"Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled",E5,L2,"9.7,10.1","Deploy and Maintain Email Server Anti-Malware Protections, Deploy and Maintain Anti-Malware Software",TRUE,TRUE,TRUE,TRUE,EXO
13,Test-SpamPolicyAdminNotify.ps1,2.1.6,Ensure Exchange Online Spam Policies are set to notify administrators,E3,L1,17.5,Assign Key Roles and Responsibilities,FALSE,TRUE,TRUE,TRUE,EXO
14,Test-AntiPhishingPolicy.ps1,2.1.7,Ensure that an anti-phishing policy has been created,E5,L1,9.7,Deploy and Maintain Email Server Anti-Malware Protections,FALSE,FALSE,TRUE,TRUE,EXO
15,Test-EnableDKIM.ps1,2.1.9,Ensure that DKIM is enabled for all Exchange Online Domains,E3,L1,9.5,Implement DMARC,FALSE,TRUE,TRUE,TRUE,EXO
1 Index TestFileName Rec RecDescription ELevel ProfileLevel CISControl CISDescription IG1 IG2 IG3 Automated Connection
10 9 Test-CommonAttachmentFilter.ps1 2.1.2 Ensure the Common Attachment Types Filter is enabled E3 L1 9.6 Block Unnecessary File Types FALSE TRUE TRUE TRUE EXO
11 10 Test-NotifyMalwareInternal.ps1 2.1.3 Ensure notifications for internal users sending malware is Enabled E3 L1 17.5 Assign Key Roles and Responsibilities FALSE TRUE TRUE TRUE EXO
12 11 Test-SafeAttachmentsPolicy.ps1 2.1.4 Ensure Safe Attachments policy is enabled E5 L2 9.7 Deploy and Maintain Email Server Anti-Malware Protections FALSE FALSE TRUE TRUE EXO
13 12 Test-SafeAttachmentsTeams.ps1 2.1.5 Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled E5 L2 9.7, 10.1 9.7,10.1 Deploy and Maintain Email Server Anti-Malware Protections, Deploy and Maintain Anti-Malware Software TRUE TRUE TRUE TRUE EXO
14 13 Test-SpamPolicyAdminNotify.ps1 2.1.6 Ensure Exchange Online Spam Policies are set to notify administrators E3 L1 17.5 Assign Key Roles and Responsibilities FALSE TRUE TRUE TRUE EXO
15 14 Test-AntiPhishingPolicy.ps1 2.1.7 Ensure that an anti-phishing policy has been created E5 L1 9.7 Deploy and Maintain Email Server Anti-Malware Protections FALSE FALSE TRUE TRUE EXO
16 15 Test-EnableDKIM.ps1 2.1.9 Ensure that DKIM is enabled for all Exchange Online Domains E3 L1 9.5 Implement DMARC FALSE TRUE TRUE TRUE EXO

View File

@@ -1,91 +1,111 @@
function Test-AdministrativeAccountCompliance {
[CmdletBinding()]
[OutputType([CISAuditResult])]
param (
# Aligned
# Parameters can be added if needed
)
begin {
# The following conditions are checked:
# Condition A: The administrative account is cloud-only (not synced).
# Condition B: The account is assigned a valid license (e.g., Microsoft Entra ID P1 or P2).
# Condition C: The administrative account does not have any other application assignments (only valid licenses).
$validLicenses = @('AAD_PREMIUM', 'AAD_PREMIUM_P2')
$recnum = "1.1.1"
Write-Verbose "Starting Test-AdministrativeAccountCompliance with Rec: $recnum"
}
process {
try {
# Retrieve all necessary data outside the loops
# Retrieve all admin roles
Write-Verbose "Retrieving all admin roles"
$adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Admin*" }
$roleAssignments = Get-MgRoleManagementDirectoryRoleAssignment
$principalIds = $roleAssignments.PrincipalId | Select-Object -Unique
# Fetch user details using filter
$userDetailsList = @{}
$licensesList = @{}
$userDetails = Get-MgUser -Filter "id in ('$($principalIds -join "','")')" -Property "DisplayName, UserPrincipalName, Id, OnPremisesSyncEnabled" -ErrorAction SilentlyContinue
foreach ($user in $userDetails) {
$userDetailsList[$user.Id] = $user
}
# Fetch user licenses for each unique principal ID
foreach ($principalId in $principalIds) {
$licensesList[$principalId] = Get-MgUserLicenseDetail -UserId $principalId -ErrorAction SilentlyContinue
}
$adminRoleUsers = @()
# Loop through each admin role to get role assignments and user details
foreach ($role in $adminRoles) {
foreach ($assignment in $roleAssignments | Where-Object { $_.RoleDefinitionId -eq $role.Id }) {
$userDetails = $userDetailsList[$assignment.PrincipalId]
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)"
# Get user details for each principal ID
$userDetails = Get-MgUser -UserId $assignment.PrincipalId -Property "DisplayName, UserPrincipalName, Id, OnPremisesSyncEnabled" -ErrorAction SilentlyContinue
if ($userDetails) {
$licenses = $licensesList[$assignment.PrincipalId]
Write-Verbose "Retrieved user details for: $($userDetails.UserPrincipalName)"
# Get user license details
$licenses = Get-MgUserLicenseDetail -UserId $assignment.PrincipalId -ErrorAction SilentlyContinue
$licenseString = if ($licenses) { ($licenses.SkuPartNumber -join '|') } else { "No Licenses Found" }
# Condition A: Check if the account is cloud-only
$cloudOnlyStatus = if ($userDetails.OnPremisesSyncEnabled) { "Fail" } else { "Pass" }
# Condition B: Check if the account has valid licenses
$hasValidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -contains $_ }
$validLicensesStatus = if ($hasValidLicense) { "Pass" } else { "Fail" }
# Condition C: Check if the account has no other licenses
$hasInvalidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -notcontains $_ }
$applicationAssignmentStatus = if ($hasInvalidLicense) { "Fail" } else { "Pass" }
Write-Verbose "User: $($userDetails.UserPrincipalName), Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Other Applications Assigned: $applicationAssignmentStatus"
# Collect user information
$adminRoleUsers += [PSCustomObject]@{
UserName = $userDetails.UserPrincipalName
RoleName = $role.DisplayName
UserId = $userDetails.Id
HybridUser = $userDetails.OnPremisesSyncEnabled
Licenses = $licenseString
CloudOnlyStatus = $cloudOnlyStatus
ValidLicensesStatus = $validLicensesStatus
ApplicationAssignmentStatus = $applicationAssignmentStatus
}
}
else {
Write-Verbose "No user details found for principal ID: $($assignment.PrincipalId)"
}
}
}
# Group admin role users by UserName and collect unique roles and licenses
Write-Verbose "Grouping admin role users by UserName"
$uniqueAdminRoleUsers = $adminRoleUsers | Group-Object -Property UserName | ForEach-Object {
$first = $_.Group | Select-Object -First 1
$roles = ($_.Group.RoleName -join ', ')
$licenses = (($_.Group | Select-Object -ExpandProperty Licenses) -join ',').Split(',') | Select-Object -Unique
$first | Select-Object UserName, UserId, HybridUser, @{Name = 'Roles'; Expression = { $roles } }, @{Name = 'Licenses'; Expression = { $licenses -join '|' } }
$first | Select-Object UserName, UserId, HybridUser, @{Name = 'Roles'; Expression = { $roles } }, @{Name = 'Licenses'; Expression = { $licenses -join '|' } }, CloudOnlyStatus, ValidLicensesStatus, ApplicationAssignmentStatus
}
# Identify non-compliant users based on conditions A, B, and C
Write-Verbose "Identifying non-compliant users based on conditions"
$nonCompliantUsers = $uniqueAdminRoleUsers | Where-Object {
$_.HybridUser -or
-not ($_.Licenses -split '\|' | Where-Object { $validLicenses -contains $_ })
$_.HybridUser -or # Fails Condition A
$_.ValidLicensesStatus -eq "Fail" -or # Fails Condition B
$_.ApplicationAssignmentStatus -eq "Fail" # Fails Condition C
}
# Generate failure reasons
Write-Verbose "Generating failure reasons for non-compliant users"
$failureReasons = $nonCompliantUsers | ForEach-Object {
$accountType = if ($_.HybridUser) { "Hybrid" } else { "Cloud-Only" }
$missingLicenses = $validLicenses | Where-Object { $_ -notin ($_.Licenses -split '\|') }
"$($_.UserName)|$($_.Roles)|$accountType|$($missingLicenses -join ',')"
"$($_.UserName)|$($_.Roles)|$($_.CloudOnlyStatus)|$($_.ValidLicensesStatus)|$($_.ApplicationAssignmentStatus)"
}
$failureReasons = $failureReasons -join "`n"
$details = if ($nonCompliantUsers) {
"Non-compliant accounts: `nUsername | Roles | HybridStatus | Missing Licence`n$failureReasons"
$failureReason = if ($nonCompliantUsers) {
"Non-Compliant Accounts: $($nonCompliantUsers.Count)"
} else {
"Compliant Accounts: $($uniqueAdminRoleUsers.Count)"
}
$failureReason = if ($nonCompliantUsers) {
"Non-Compliant Accounts: $($nonCompliantUsers.Count)`nDetails:`n" + ($nonCompliantUsers | ForEach-Object { $_.UserName }) -join "`n"
} else {
"N/A"
}
$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" }
Write-Verbose "Assessment completed. Result: $status"
# Create the parameter splat
$params = @{
Rec = $recnum
Result = $result
@@ -99,6 +119,7 @@ function Test-AdministrativeAccountCompliance {
catch {
Write-Error "An error occurred during the test: $_"
# Handle the error and create a failure result
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
@@ -107,7 +128,9 @@ function Test-AdministrativeAccountCompliance {
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
}
}
end {
# Output the result
return $auditResult
}
}

View File

@@ -42,7 +42,7 @@ function Test-PasswordNeverExpirePolicy {
$failureReasons = if ($isCompliant) {
"N/A"
} else {
"Password expiration is not set to never expire for domain $domainName. Run the following command to remediate: `nUpdate-MgDomain -DomainId $domainName -PasswordValidityPeriodInDays 2147483647 -PasswordNotificationWindowInDays 30"
"Password expiration is not set to never expire for domain $domainName. Run the following command to remediate: `nUpdate-MgDomain -DomainId $domainName -PasswordValidityPeriodInDays 2147483647 -PasswordNotificationWindowInDays 30`n"
}
$details = "$domainName|$passwordPolicy days|$isDefault"

212
test-gh.ps1 Normal file
View File

@@ -0,0 +1,212 @@
$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'

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'
}
}
}
}