20 Commits

Author SHA1 Message Date
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
Doug Rios
014c42b3fe Merge pull request #19 from CriticalSolutionsNetwork/Make-tenant-admin-optional
Make tenant admin optional
2024-06-08 19:32:55 -05:00
DrIOS
fbfb5b5986 add: build help for issues 2024-06-08 19:31:29 -05:00
DrIOS
03b5bb47e2 docs: Update HelpE 2024-06-08 18:12:01 -05:00
DrIOS
9dc99636d3 fix: module check included for whatif 2024-06-08 17:57:42 -05:00
DrIOS
afe657ffc0 fix: module check included for whatif 2024-06-08 17:48:43 -05:00
DrIOS
702f557579 fix: module check included for whatif 2024-06-08 17:45:31 -05:00
DrIOS
f855ef7d0b fix: Update supports should process for connection/disconect 2024-06-08 17:44:16 -05:00
DrIOS
270e980a57 docs: Update CHANGELOG 2024-06-08 17:41:23 -05:00
DrIOS
ff90669984 fix: Update supports should process for connection/disconect 2024-06-08 17:41:09 -05:00
DrIOS
f2e799af2f docs: Update HelpE 2024-06-08 17:31:28 -05:00
DrIOS
4a4d200197 fix: throw error if no test definitioins after SPO removal 2024-06-08 17:26:30 -05:00
DrIOS
9199d97fc2 docs: Update Help and README 2024-06-08 17:22:39 -05:00
DrIOS
5d681f3d72 docs: update CHANGELOG 2024-06-08 17:19:39 -05:00
DrIOS
f926c63533 add: tenantadmin url as optional parameter 2024-06-08 17:19:22 -05:00
25 changed files with 383 additions and 94 deletions

View File

@@ -6,6 +6,31 @@ The format is based on and uses the types of changes according to [Keep a Change
### Added ### 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.
### Changed
- Updated `Connect-M365Suite` to make `TenantAdminUrl` an optional parameter.
- Updated `Invoke-M365SecurityAudit` to make `TenantAdminUrl` an optional parameter.
- Improved connection handling and error messaging in `Connect-M365Suite`.
- 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. - Added pipeline support to `Sync-CISExcelAndCsvData` function for `[CISAuditResult[]]` input.
## [0.1.5] - 2024-06-08 ## [0.1.5] - 2024-06-08

BIN
README.md

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -1,33 +1,37 @@
function Assert-ModuleAvailability { function Assert-ModuleAvailability {
[OutputType([void]) ]
param( param(
[string]$ModuleName, [string]$ModuleName,
[string]$RequiredVersion, [string]$RequiredVersion,
[string]$SubModuleName [string[]]$SubModules = @()
) )
try { try {
$module = Get-Module -ListAvailable -Name $ModuleName | Where-Object { $_.Version -ge [version]$RequiredVersion } $module = Get-Module -ListAvailable -Name $ModuleName | Where-Object { $_.Version -ge [version]$RequiredVersion }
if ($null -eq $module) {$auditResult.Profile if ($null -eq $module) {
Write-Host "Installing $ModuleName module..." Write-Information "Installing $ModuleName module..." -InformationAction Continue
Install-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force -AllowClobber -Scope CurrentUser | Out-Null Install-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force -AllowClobber -Scope CurrentUser | Out-Null
} }
elseif ($module.Version -lt [version]$RequiredVersion) { 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 Update-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force | Out-Null
} }
else { 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) { if ($SubModules.Count -gt 0) {
Import-Module -Name "$ModuleName.$SubModuleName" -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null foreach ($subModule in $SubModules) {
} Write-Information "Importing submodule $ModuleName.$subModule..." -InformationAction Continue
else { Import-Module -Name "$ModuleName.$subModule" -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null
}
} else {
Write-Information "Importing module $ModuleName..." -InformationAction Continue
Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null
} }
} }
catch { catch {
Write-Warning "An error occurred with module $ModuleName`: $_" Write-Warning "An error occurred with module $ModuleName`: $_"
} }
} }

View File

@@ -1,7 +1,8 @@
function Connect-M365Suite { function Connect-M365Suite {
[OutputType([void])]
[CmdletBinding()] [CmdletBinding()]
param ( param (
[Parameter(Mandatory)] [Parameter(Mandatory=$false)]
[string]$TenantAdminUrl, [string]$TenantAdminUrl,
[Parameter(Mandatory)] [Parameter(Mandatory)]

View File

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

View File

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

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 { function Get-MostCommonWord {
[CmdletBinding()]
[OutputType([string])]
param ( param (
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string[]]$InputStrings [string[]]$InputStrings
@@ -19,4 +21,4 @@ function Get-MostCommonWord {
} else { } else {
return $null return $null
} }
} }

View File

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

View File

@@ -1,4 +1,6 @@
function Get-TestDefinitionsObject { function Get-TestDefinitionsObject {
[CmdletBinding()]
[OutputType([object[]])]
param ( param (
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[object[]]$TestDefinitions, [object[]]$TestDefinitions,
@@ -60,4 +62,4 @@ function Get-TestDefinitionsObject {
Write-Verbose "Filtered test definitions count: $($TestDefinitions.Count)" Write-Verbose "Filtered test definitions count: $($TestDefinitions.Count)"
return $TestDefinitions return $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 { function Initialize-CISAuditResult {
[CmdletBinding()] [CmdletBinding()]
[OutputType([CISAuditResult])]
param ( param (
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$Rec, [string]$Rec,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,29 @@
function Update-WorksheetCell { function Update-WorksheetCell {
param ( [OutputType([void])]
$Worksheet, param (
$Data, $Worksheet,
$StartingRowIndex $Data,
) $StartingRowIndex
)
# Check and set headers # Check and set headers
$firstItem = $Data[0] $firstItem = $Data[0]
$colIndex = 1 $colIndex = 1
foreach ($property in $firstItem.PSObject.Properties) { foreach ($property in $firstItem.PSObject.Properties) {
if ($StartingRowIndex -eq 2 -and $Worksheet.Cells[1, $colIndex].Value -eq $null) { if ($StartingRowIndex -eq 2 -and $Worksheet.Cells[1, $colIndex].Value -eq $null) {
$Worksheet.Cells[1, $colIndex].Value = $property.Name $Worksheet.Cells[1, $colIndex].Value = $property.Name
}
$colIndex++
}
# Iterate over each row in the data and update cells
$rowIndex = $StartingRowIndex
foreach ($item in $Data) {
$colIndex = 1
foreach ($property in $item.PSObject.Properties) {
$Worksheet.Cells[$rowIndex, $colIndex].Value = $property.Value
$colIndex++
}
$rowIndex++
}
} }
$colIndex++
}
# Iterate over each row in the data and update cells
$rowIndex = $StartingRowIndex
foreach ($item in $Data) {
$colIndex = 1
foreach ($property in $item.PSObject.Properties) {
$Worksheet.Cells[$rowIndex, $colIndex].Value = $property.Value
$colIndex++
}
$rowIndex++
}
}

View File

@@ -4,7 +4,7 @@
.DESCRIPTION .DESCRIPTION
The Invoke-M365SecurityAudit cmdlet performs a comprehensive security audit based on the specified parameters. It allows auditing of various configurations and settings within a Microsoft 365 environment, such as compliance with CIS benchmarks. The Invoke-M365SecurityAudit cmdlet performs a comprehensive security audit based on the specified parameters. It allows auditing of various configurations and settings within a Microsoft 365 environment, such as compliance with CIS benchmarks.
.PARAMETER TenantAdminUrl .PARAMETER TenantAdminUrl
The URL of the tenant admin. This parameter is mandatory. The URL of the tenant admin. If not specified, none of the SharePoint Online tests will run.
.PARAMETER M365DomainForPWPolicyTest .PARAMETER M365DomainForPWPolicyTest
The domain name of the Microsoft 365 environment to test. This parameter is not mandatory and by default it will pass/fail all found domains as a group if a specific domain is not specified. The domain name of the Microsoft 365 environment to test. This parameter is not mandatory and by default it will pass/fail all found domains as a group if a specific domain is not specified.
.PARAMETER ELevel .PARAMETER ELevel
@@ -28,34 +28,89 @@
.PARAMETER NoModuleCheck .PARAMETER NoModuleCheck
If specified, the cmdlet will not check for the presence of required modules. If specified, the cmdlet will not check for the presence of required modules.
.EXAMPLE .EXAMPLE
PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -ELevel "E5" -ProfileLevel "L1" PS> Invoke-M365SecurityAudit
Performs a security audit using default parameters.
Output:
Status : Fail
ELevel : E3
ProfileLevel: L1
Connection : Microsoft Graph
Rec : 1.1.1
Result : False
Details : Non-compliant accounts:
Username | Roles | HybridStatus | Missing Licence
user1@domain.com| Global Administrator | Cloud-Only | AAD_PREMIUM
user2@domain.com| Global Administrator | Hybrid | AAD_PREMIUM, AAD_PREMIUM_P2
FailureReason: Non-Compliant Accounts: 2
.EXAMPLE
PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -M365DomainForPWPolicyTest "contoso.com" -ELevel "E5" -ProfileLevel "L1"
Performs a security audit for the E5 level and L1 profile in the specified Microsoft 365 environment. Performs a security audit for the E5 level and L1 profile in the specified Microsoft 365 environment.
Output:
Status : Fail
ELevel : E5
ProfileLevel: L1
Connection : Microsoft Graph
Rec : 1.1.1
Result : False
Details : Non-compliant accounts:
Username | Roles | HybridStatus | Missing Licence
user1@domain.com| Global Administrator | Cloud-Only | AAD_PREMIUM
user2@domain.com| Global Administrator | Hybrid | AAD_PREMIUM, AAD_PREMIUM_P2
FailureReason: Non-Compliant Accounts: 2
.EXAMPLE .EXAMPLE
PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -IncludeIG1 PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -M365DomainForPWPolicyTest "contoso.com" -IncludeIG1
Performs an audit including all tests where IG1 is true. Performs an audit including all tests where IG1 is true.
Output:
Status : Fail
ELevel : E3
ProfileLevel: L1
Connection : Microsoft Graph
Rec : 1.1.1
Result : False
Details : Non-compliant accounts:
Username | Roles | HybridStatus | Missing Licence
user1@domain.com| Global Administrator | Cloud-Only | AAD_PREMIUM
user2@domain.com| Global Administrator | Hybrid | AAD_PREMIUM, AAD_PREMIUM_P2
FailureReason: Non-Compliant Accounts: 2
.EXAMPLE .EXAMPLE
PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -SkipRecommendation '1.1.3', '2.1.1' PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -M365DomainForPWPolicyTest "contoso.com" -SkipRecommendation '1.1.3', '2.1.1'
Performs an audit while excluding specific recommendations 1.1.3 and 2.1.1. Performs an audit while excluding specific recommendations 1.1.3 and 2.1.1.
Output:
Status : Fail
ELevel : E3
ProfileLevel: L1
Connection : Microsoft Graph
Rec : 1.1.1
Result : False
Details : Non-compliant accounts:
Username | Roles | HybridStatus | Missing Licence
user1@domain.com| Global Administrator | Cloud-Only | AAD_PREMIUM
user2@domain.com| Global Administrator | Hybrid | AAD_PREMIUM, AAD_PREMIUM_P2
FailureReason: Non-Compliant Accounts: 2
.EXAMPLE .EXAMPLE
PS> $auditResults = Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" PS> $auditResults = Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -M365DomainForPWPolicyTest "contoso.com"
PS> $auditResults | Export-Csv -Path "auditResults.csv" -NoTypeInformation PS> $auditResults | Export-Csv -Path "auditResults.csv" -NoTypeInformation
Captures the audit results into a variable and exports them to a CSV file. Captures the audit results into a variable and exports them to a CSV file.
Output:
CISAuditResult[]
auditResults.csv
.EXAMPLE
PS> Invoke-M365SecurityAudit -WhatIf
Displays what would happen if the cmdlet is run without actually performing the audit.
Output:
What if: Performing the operation "Invoke-M365SecurityAudit" on target "Microsoft 365 environment".
.INPUTS .INPUTS
None. You cannot pipe objects to Invoke-M365SecurityAudit. None. You cannot pipe objects to Invoke-M365SecurityAudit.
.OUTPUTS .OUTPUTS
CISAuditResult[] CISAuditResult[]
The cmdlet returns an array of CISAuditResult objects representing the results of the security audit. The cmdlet returns an array of CISAuditResult objects representing the results of the security audit.
.NOTES .NOTES
- This module is based on CIS benchmarks. - This module is based on CIS benchmarks.
- Governed by the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. - Governed by the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
- Commercial use is not permitted. This module cannot be sold or used for commercial purposes. - Commercial use is not permitted. This module cannot be sold or used for commercial purposes.
- Modifications and sharing are allowed under the same license. - Modifications and sharing are allowed under the same license.
- For full license details, visit: https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en - For full license details, visit: https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en
- Register for CIS Benchmarks at: https://www.cisecurity.org/cis-benchmarks - Register for CIS Benchmarks at: https://www.cisecurity.org/cis-benchmarks
.LINK .LINK
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Invoke-M365SecurityAudit https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Invoke-M365SecurityAudit
#> #>
@@ -63,7 +118,7 @@ function Invoke-M365SecurityAudit {
[CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Default')] [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Default')]
[OutputType([CISAuditResult[]])] [OutputType([CISAuditResult[]])]
param ( param (
[Parameter(Mandatory = $true, HelpMessage = "The SharePoint tenant admin URL, which should end with '-admin.sharepoint.com'.")] [Parameter(Mandatory = $false, HelpMessage = "The SharePoint tenant admin URL, which should end with '-admin.sharepoint.com'. If not specified none of the Sharepoint Online tests will run.")]
[ValidatePattern('^https://[a-zA-Z0-9-]+-admin\.sharepoint\.com$')] [ValidatePattern('^https://[a-zA-Z0-9-]+-admin\.sharepoint\.com$')]
[string]$TenantAdminUrl, [string]$TenantAdminUrl,
@@ -94,12 +149,12 @@ function Invoke-M365SecurityAudit {
[Parameter(Mandatory = $true, ParameterSetName = 'RecFilter')] [Parameter(Mandatory = $true, ParameterSetName = 'RecFilter')]
[ValidateSet( [ValidateSet(
'1.1.1', '1.1.3', '1.2.1', '1.2.2', '1.3.1', '1.3.3', '1.3.6', '2.1.1', '2.1.2', ` '1.1.1', '1.1.3', '1.2.1', '1.2.2', '1.3.1', '1.3.3', '1.3.6', '2.1.1', '2.1.2', `
'2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.1.7', '2.1.9', '3.1.1', '5.1.2.3', ` '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.1.7', '2.1.9', '3.1.1', '5.1.2.3', `
'5.1.8.1', '6.1.1', '6.1.2', '6.1.3', '6.2.1', '6.2.2', '6.2.3', '6.3.1', ` '5.1.8.1', '6.1.1', '6.1.2', '6.1.3', '6.2.1', '6.2.2', '6.2.3', '6.3.1', `
'6.5.1', '6.5.2', '6.5.3', '7.2.1', '7.2.10', '7.2.2', '7.2.3', '7.2.4', ` '6.5.1', '6.5.2', '6.5.3', '7.2.1', '7.2.10', '7.2.2', '7.2.3', '7.2.4', `
'7.2.5', '7.2.6', '7.2.7', '7.2.9', '7.3.1', '7.3.2', '7.3.4', '8.1.1', ` '7.2.5', '7.2.6', '7.2.7', '7.2.9', '7.3.1', '7.3.2', '7.3.4', '8.1.1', `
'8.1.2', '8.2.1', '8.5.1', '8.5.2', '8.5.3', '8.5.4', '8.5.5', '8.5.6', ` '8.1.2', '8.2.1', '8.5.1', '8.5.2', '8.5.3', '8.5.4', '8.5.5', '8.5.6', `
'8.5.7', '8.6.1' '8.5.7', '8.6.1'
)] )]
[string[]]$IncludeRecommendation, [string[]]$IncludeRecommendation,
@@ -107,12 +162,12 @@ function Invoke-M365SecurityAudit {
[Parameter(Mandatory = $true, ParameterSetName = 'SkipRecFilter')] [Parameter(Mandatory = $true, ParameterSetName = 'SkipRecFilter')]
[ValidateSet( [ValidateSet(
'1.1.1', '1.1.3', '1.2.1', '1.2.2', '1.3.1', '1.3.3', '1.3.6', '2.1.1', '2.1.2', ` '1.1.1', '1.1.3', '1.2.1', '1.2.2', '1.3.1', '1.3.3', '1.3.6', '2.1.1', '2.1.2', `
'2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.1.7', '2.1.9', '3.1.1', '5.1.2.3', ` '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.1.7', '2.1.9', '3.1.1', '5.1.2.3', `
'5.1.8.1', '6.1.1', '6.1.2', '6.1.3', '6.2.1', '6.2.2', '6.2.3', '6.3.1', ` '5.1.8.1', '6.1.1', '6.1.2', '6.1.3', '6.2.1', '6.2.2', '6.2.3', '6.3.1', `
'6.5.1', '6.5.2', '6.5.3', '7.2.1', '7.2.10', '7.2.2', '7.2.3', '7.2.4', ` '6.5.1', '6.5.2', '6.5.3', '7.2.1', '7.2.10', '7.2.2', '7.2.3', '7.2.4', `
'7.2.5', '7.2.6', '7.2.7', '7.2.9', '7.3.1', '7.3.2', '7.3.4', '8.1.1', ` '7.2.5', '7.2.6', '7.2.7', '7.2.9', '7.3.1', '7.3.2', '7.3.4', '8.1.1', `
'8.1.2', '8.2.1', '8.5.1', '8.5.2', '8.5.3', '8.5.4', '8.5.5', '8.5.6', ` '8.1.2', '8.2.1', '8.5.1', '8.5.2', '8.5.3', '8.5.4', '8.5.5', '8.5.6', `
'8.5.7', '8.6.1' '8.5.7', '8.6.1'
)] )]
[string[]]$SkipRecommendation, [string[]]$SkipRecommendation,
@@ -127,12 +182,18 @@ function Invoke-M365SecurityAudit {
$script:MaximumFunctionCount = 8192 $script:MaximumFunctionCount = 8192
} }
# Ensure required modules are installed # Ensure required modules are installed
if (!($NoModuleCheck)) { $requiredModules = Get-RequiredModule -AuditFunction
$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) { 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 # Load test definitions from CSV
$testDefinitionsPath = Join-Path -Path $PSScriptRoot -ChildPath "helper\TestDefinitions.csv" $testDefinitionsPath = Join-Path -Path $PSScriptRoot -ChildPath "helper\TestDefinitions.csv"
$testDefinitions = Import-Csv -Path $testDefinitionsPath $testDefinitions = Import-Csv -Path $testDefinitionsPath
@@ -151,9 +212,14 @@ function Invoke-M365SecurityAudit {
$testDefinitions = Get-TestDefinitionsObject @params $testDefinitions = Get-TestDefinitionsObject @params
# Extract unique connections needed # Extract unique connections needed
$requiredConnections = $testDefinitions.Connection | Sort-Object -Unique $requiredConnections = $testDefinitions.Connection | Sort-Object -Unique
# Establishing connections if required if ($requiredConnections -contains 'SPO') {
if (!($DoNotConnect)) { if (-not $TenantAdminUrl) {
Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections $requiredConnections = $requiredConnections | Where-Object { $_ -ne 'SPO' }
$testDefinitions = $testDefinitions | Where-Object { $_.Connection -ne 'SPO' }
if ($null -eq $testDefinitions) {
throw "No tests to run as no SharePoint Online tests are available."
}
}
} }
# Determine which test files to load based on filtering # Determine which test files to load based on filtering
$testsToLoad = $testDefinitions.TestFileName | ForEach-Object { $_ -replace '.ps1$', '' } $testsToLoad = $testDefinitions.TestFileName | ForEach-Object { $_ -replace '.ps1$', '' }
@@ -162,6 +228,7 @@ function Invoke-M365SecurityAudit {
# Initialize a collection to hold failed test details # Initialize a collection to hold failed test details
$script:FailedTests = [System.Collections.ArrayList]::new() $script:FailedTests = [System.Collections.ArrayList]::new()
} # End Begin } # End Begin
Process { Process {
$allAuditResults = [System.Collections.ArrayList]::new() # Initialize a collection to hold all results $allAuditResults = [System.Collections.ArrayList]::new() # Initialize a collection to hold all results
# Dynamically dot-source the test scripts # Dynamically dot-source the test scripts
@@ -172,6 +239,15 @@ function Invoke-M365SecurityAudit {
$totalTests = $testFiles.Count $totalTests = $testFiles.Count
$currentTestIndex = 0 $currentTestIndex = 0
# Establishing connections if required
$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 # Import the test functions
$testFiles | ForEach-Object { $testFiles | ForEach-Object {
$currentTestIndex++ $currentTestIndex++
@@ -202,14 +278,16 @@ function Invoke-M365SecurityAudit {
} }
End { End {
if (!($DoNotDisconnect)) { if (!($DoNotDisconnect) -and $PSCmdlet.ShouldProcess("Disconnect from Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Disconnect")) {
# Clean up sessions # Clean up sessions
Disconnect-M365Suite -RequiredConnections $requiredConnections Disconnect-M365Suite -RequiredConnections $requiredConnections
} }
# Call the private function to calculate and display results if ($PSCmdlet.ShouldProcess("Measure and display audit results for $($totalTests) tests", "Measure")) {
Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests # Call the private function to calculate and display results
# Return all collected audit results Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests
return $allAuditResults.ToArray() | Sort-Object -Property Rec # Return all collected audit results
return $allAuditResults.ToArray() | Sort-Object -Property Rec
}
} }
} }

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 https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Sync-CISExcelAndCsvData
#> #>
function Sync-CISExcelAndCsvData { function Sync-CISExcelAndCsvData {
[OutputType([void], [PSCustomObject[]])]
[CmdletBinding(DefaultParameterSetName = 'CsvInput')] [CmdletBinding(DefaultParameterSetName = 'CsvInput')]
param ( param (
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]

View File

@@ -42,7 +42,7 @@ function Test-PasswordNeverExpirePolicy {
$failureReasons = if ($isCompliant) { $failureReasons = if ($isCompliant) {
"N/A" "N/A"
} else { } 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" $details = "$domainName|$passwordPolicy days|$isDefault"

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