Initial Commit

This commit is contained in:
DrIOS
2024-03-25 08:34:43 -05:00
commit 0226b293b5
137 changed files with 9448 additions and 0 deletions

15
.gitattributes vendored Normal file
View File

@@ -0,0 +1,15 @@
# Needed for publishing of examples, build worker defaults to core.autocrlf=input.
* text eol=autocrlf
*.mof text eol=crlf
*.sh text eol=lf
*.svg eol=lf
# Ensure any exe files are treated as binary
*.exe binary
*.jpg binary
*.xl* binary
*.pfx binary
*.png binary
*.dll binary
*.so binary

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
output/
**.bak
*.local.*
!**/README.md
.kitchen/
*.nupkg
*.suo
*.user
*.coverage
.vs
.psproj
.sln
markdownissues.txt
node_modules
package-lock.json

125
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,125 @@
{
"version": "2.0.0",
"_runner": "terminal",
"windows": {
"options": {
"shell": {
"executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command"
]
}
}
},
"linux": {
"options": {
"shell": {
"executable": "/usr/bin/pwsh",
"args": [
"-NoProfile",
"-Command"
]
}
}
},
"osx": {
"options": {
"shell": {
"executable": "/usr/local/bin/pwsh",
"args": [
"-NoProfile",
"-Command"
]
}
}
},
"tasks": [
{
"label": "build",
"type": "shell",
"command": "&${cwd}/build.ps1",
"args": [],
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "new",
"clear": false
},
"runOptions": {
"runOn": "default"
},
"problemMatcher": [
{
"owner": "powershell",
"fileLocation": [
"absolute"
],
"severity": "error",
"pattern": [
{
"regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$",
"message": 1
},
{
"regexp": "(.*)",
"code": 1
},
{
"regexp": ""
},
{
"regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*",
"file": 1,
"line": 2
}
]
}
]
},
{
"label": "test",
"type": "shell",
"command": "&${cwd}/build.ps1",
"args": ["-AutoRestore","-Tasks","test"],
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false
},
"problemMatcher": [
{
"owner": "powershell",
"fileLocation": [
"absolute"
],
"severity": "error",
"pattern": [
{
"regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$",
"message": 1
},
{
"regexp": "(.*)",
"code": 1
},
{
"regexp": ""
},
{
"regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*",
"file": 1,
"line": 2
}
]
}
]
}
]
}

17
CHANGELOG.md Normal file
View File

@@ -0,0 +1,17 @@
# Changelog for M365FoundationsCISReport
The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Initial release of the M365FoundationsCISReport PowerShell module.
- Function `Invoke-M365SecurityAudit` for conducting a comprehensive security audit in Microsoft 365 environments.
- Support for multiple parameter sets including ELevelFilter, IGFilters, RecFilter, and SkipRecFilter to cater to diverse audit requirements.
- Implementation of `-NoModuleCheck`, `-DoNotConnect`, and `-DoNotDisconnect` switches for enhanced control over module behavior.
- Integration with required modules like ExchangeOnlineManagement, AzureAD, Microsoft.Graph, Microsoft.Online.SharePoint.PowerShell, and MicrosoftTeams.
- A dynamic test loading system based on CSV input for flexibility in defining audit tests.
- Comprehensive verbose logging to detail the steps being performed during an audit.
- Comment-help documentation for the `Invoke-M365SecurityAudit` function with examples and usage details.
- Attribution to CIS and licensing information under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License in the README.

40
GitVersion.yml Normal file
View File

@@ -0,0 +1,40 @@
mode: ContinuousDelivery
next-version: 0.0.1
major-version-bump-message: '(breaking\schange|breaking|major)\b'
minor-version-bump-message: '(adds?|features?|minor)\b'
patch-version-bump-message: '\s?(fix|patch)'
no-bump-message: '\+semver:\s?(none|skip)'
assembly-informational-format: '{NuGetVersionV2}+Sha.{Sha}.Date.{CommitDate}'
branches:
master:
tag: preview
regex: ^main$
pull-request:
tag: PR
feature:
tag: useBranchName
increment: Minor
regex: f(eature(s)?)?[\/-]
source-branches: ['master']
hotfix:
tag: fix
increment: Patch
regex: (hot)?fix(es)?[\/-]
source-branches: ['master']
ignore:
sha: []
merge-message-formats: {}
# feature:
# tag: useBranchName
# increment: Minor
# regex: f(eature(s)?)?[/-]
# source-branches: ['master']
# hotfix:
# tag: fix
# increment: Patch
# regex: (hot)?fix(es)?[/-]
# source-branches: ['master']

25
LICENSE.md Normal file
View File

@@ -0,0 +1,25 @@
# License
## Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit [CC BY-NC-SA 4.0 license](https://creativecommons.org/licenses/by-nc-sa/4.0/).
### You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
The licensor cannot revoke these freedoms as long as you follow the license terms.
### Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
- **NonCommercial** — You may not use the material for commercial purposes.
- **ShareAlike** — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
- **No additional restrictions** — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
### Notices:
You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation.
No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
# M365FoundationsCISReport Module
## License
This PowerShell module is based on CIS benchmarks and is distributed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. This means:
- **Non-commercial**: You may not use the material for commercial purposes.
- **ShareAlike**: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
- **Attribution**: Appropriate credit must be given, provide a link to the license, and indicate if changes were made.
For full license details, please visit [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en).
## Invoke-M365SecurityAudit
### Synopsis
Invokes a security audit for Microsoft 365 environments.
### Syntax
```powershell
Invoke-M365SecurityAudit -TenantAdminUrl <String> -DomainName <String> [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [<CommonParameters>]
Invoke-M365SecurityAudit -TenantAdminUrl <String> -DomainName <String> [-ELevel <String>] [-ProfileLevel <String>] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [<CommonParameters>]
Invoke-M365SecurityAudit -TenantAdminUrl <String> -DomainName <String> [-IncludeIG1] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [<CommonParameters>]
Invoke-M365SecurityAudit -TenantAdminUrl <String> -DomainName <String> [-IncludeIG2] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [<CommonParameters>]
Invoke-M365SecurityAudit -TenantAdminUrl <String> -DomainName <String> [-IncludeIG3] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [<CommonParameters>]
Invoke-M365SecurityAudit -TenantAdminUrl <String> -DomainName <String> [-IncludeRecommendation <String[]>] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [<CommonParameters>]
Invoke-M365SecurityAudit -TenantAdminUrl <String> -DomainName <String> [-SkipRecommendation <String[]>] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [<CommonParameters>]
```
### Parameters
| Name | Alias | Description | Required? | Pipeline Input | Default Value |
| - | - | - | - | - | - |
| <nobr>TenantAdminUrl</nobr> | | The URL of the tenant admin. This parameter is mandatory. | true | false | |
| <nobr>DomainName</nobr> | | The domain name of the Microsoft 365 environment. This parameter is mandatory. | true | false | |
| <nobr>ELevel</nobr> | | Specifies the E-Level \(E3 or E5\) for the audit. This parameter is optional and can be combined with the ProfileLevel parameter. | false | false | |
| <nobr>ProfileLevel</nobr> | | Specifies the profile level \(L1 or L2\) for the audit. This parameter is optional and can be combined with the ELevel parameter. | false | false | |
| <nobr>IncludeIG1</nobr> | | If specified, includes tests where IG1 is true. | false | false | False |
| <nobr>IncludeIG2</nobr> | | If specified, includes tests where IG2 is true. | false | false | False |
| <nobr>IncludeIG3</nobr> | | If specified, includes tests where IG3 is true. | false | false | False |
| <nobr>IncludeRecommendation</nobr> | | Specifies specific recommendations to include in the audit. Accepts an array of recommendation numbers. | false | false | |
| <nobr>SkipRecommendation</nobr> | | Specifies specific recommendations to exclude from the audit. Accepts an array of recommendation numbers. | false | false | |
| <nobr>DoNotConnect</nobr> | | If specified, the cmdlet will not establish a connection to Microsoft 365 services. | false | false | False |
| <nobr>DoNotDisconnect</nobr> | | If specified, the cmdlet will not disconnect from Microsoft 365 services after execution. | false | false | False |
| <nobr>NoModuleCheck</nobr> | | If specified, the cmdlet will not check for the presence of required modules. | false | false | False |
| <nobr>WhatIf</nobr> | wi | | false | false | |
| <nobr>Confirm</nobr> | cf | | false | false | |
### Inputs
- None. You cannot pipe objects to Invoke-M365SecurityAudit.
### Outputs
- CISAuditResult\\[\] The cmdlet returns an array of CISAuditResult objects representing the results of the security audit.
### Note
This module is based on CIS benchmarks and is governed by the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. For more details, visit: https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en
### Examples
**EXAMPLE 1**
```powershell
Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -ELevel "E5" -ProfileLevel "L1"
```
Performs a security audit for the E5 level and L1 profile in the specified Microsoft 365 environment.
**EXAMPLE 2**
```powershell
Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -IncludeIG1
```
Performs an audit including all tests where IG1 is true.
**EXAMPLE 3**
```powershell
Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -SkipRecommendation '1.1.3', '2.1.1'
```
Performs an audit while excluding specific recommendations 1.1.3 and 2.1.1.
**EXAMPLE 4**
```powershell
$auditResults = Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com"
PS> $auditResults | Export-Csv -Path "auditResults.csv" -NoTypeInformation
```
Captures the audit results into a variable and exports them to a CSV file.
### Links
- [Online Version: [GitHub Repository URL]](#Online Version: [GitHub Repository URL])

20
RequiredModules.psd1 Normal file
View File

@@ -0,0 +1,20 @@
@{
PSDependOptions = @{
AddToPath = $true
Target = 'output\RequiredModules'
Parameters = @{
Repository = 'PSGallery'
}
}
InvokeBuild = 'latest'
PSScriptAnalyzer = 'latest'
Pester = 'latest'
ModuleBuilder = 'latest'
ChangelogManagement = 'latest'
Sampler = 'latest'
'Sampler.GitHubTasks' = 'latest'
}

919
Resolve-Dependency.ps1 Normal file
View File

@@ -0,0 +1,919 @@
<#
.DESCRIPTION
Bootstrap script for PSDepend.
.PARAMETER DependencyFile
Specifies the configuration file for the this script. The default value is
'RequiredModules.psd1' relative to this script's path.
.PARAMETER PSDependTarget
Path for PSDepend to be bootstrapped and save other dependencies.
Can also be CurrentUser or AllUsers if you wish to install the modules in
such scope. The default value is 'output/RequiredModules' relative to
this script's path.
.PARAMETER Proxy
Specifies the URI to use for Proxy when attempting to bootstrap
PackageProvider and PowerShellGet.
.PARAMETER ProxyCredential
Specifies the credential to contact the Proxy when provided.
.PARAMETER Scope
Specifies the scope to bootstrap the PackageProvider and PSGet if not available.
THe default value is 'CurrentUser'.
.PARAMETER Gallery
Specifies the gallery to use when bootstrapping PackageProvider, PSGet and
when calling PSDepend (can be overridden in Dependency files). The default
value is 'PSGallery'.
.PARAMETER GalleryCredential
Specifies the credentials to use with the Gallery specified above.
.PARAMETER AllowOldPowerShellGetModule
Allow you to use a locally installed version of PowerShellGet older than
1.6.0 (not recommended). Default it will install the latest PowerShellGet
if an older version than 2.0 is detected.
.PARAMETER MinimumPSDependVersion
Allow you to specify a minimum version fo PSDepend, if you're after specific
features.
.PARAMETER AllowPrerelease
Not yet written.
.PARAMETER WithYAML
Not yet written.
.PARAMETER UseModuleFast
Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies
faster.
.PARAMETER PSResourceGet
Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies
faster.
.NOTES
Load defaults for parameters values from Resolve-Dependency.psd1 if not
provided as parameter.
#>
[CmdletBinding()]
param
(
[Parameter()]
[System.String]
$DependencyFile = 'RequiredModules.psd1',
[Parameter()]
[System.String]
$PSDependTarget = (Join-Path -Path $PSScriptRoot -ChildPath 'output/RequiredModules'),
[Parameter()]
[System.Uri]
$Proxy,
[Parameter()]
[System.Management.Automation.PSCredential]
$ProxyCredential,
[Parameter()]
[ValidateSet('CurrentUser', 'AllUsers')]
[System.String]
$Scope = 'CurrentUser',
[Parameter()]
[System.String]
$Gallery = 'PSGallery',
[Parameter()]
[System.Management.Automation.PSCredential]
$GalleryCredential,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$AllowOldPowerShellGetModule,
[Parameter()]
[System.String]
$MinimumPSDependVersion,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$AllowPrerelease,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$WithYAML,
[Parameter()]
[System.Collections.Hashtable]
$RegisterGallery,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$UseModuleFast,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$UsePSResourceGet,
[Parameter()]
[System.String]
$PSResourceGetVersion
)
try
{
if ($PSVersionTable.PSVersion.Major -le 5)
{
if (-not (Get-Command -Name 'Import-PowerShellDataFile' -ErrorAction 'SilentlyContinue'))
{
Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion '3.1.0.0'
}
}
Write-Verbose -Message 'Importing Bootstrap default parameters from ''$PSScriptRoot/Resolve-Dependency.psd1''.'
$resolveDependencyConfigPath = Join-Path -Path $PSScriptRoot -ChildPath '.\Resolve-Dependency.psd1' -Resolve -ErrorAction 'Stop'
$resolveDependencyDefaults = Import-PowerShellDataFile -Path $resolveDependencyConfigPath
$parameterToDefault = $MyInvocation.MyCommand.ParameterSets.Where{ $_.Name -eq $PSCmdlet.ParameterSetName }.Parameters.Keys
if ($parameterToDefault.Count -eq 0)
{
$parameterToDefault = $MyInvocation.MyCommand.Parameters.Keys
}
# Set the parameters available in the Parameter Set, or it's not possible to choose yet, so all parameters are an option.
foreach ($parameterName in $parameterToDefault)
{
if (-not $PSBoundParameters.Keys.Contains($parameterName) -and $resolveDependencyDefaults.ContainsKey($parameterName))
{
Write-Verbose -Message "Setting parameter '$parameterName' to value '$($resolveDependencyDefaults[$parameterName])'."
try
{
$variableValue = $resolveDependencyDefaults[$parameterName]
if ($variableValue -is [System.String])
{
$variableValue = $ExecutionContext.InvokeCommand.ExpandString($variableValue)
}
$PSBoundParameters.Add($parameterName, $variableValue)
Set-Variable -Name $parameterName -Value $variableValue -Force -ErrorAction 'SilentlyContinue'
}
catch
{
Write-Verbose -Message "Error adding default for $parameterName : $($_.Exception.Message)."
}
}
}
}
catch
{
Write-Warning -Message "Error attempting to import Bootstrap's default parameters from '$resolveDependencyConfigPath': $($_.Exception.Message)."
}
# Handles when both ModuleFast and PSResourceGet is configured or/and passed as parameter.
if ($UseModuleFast -and $UsePSResourceGet)
{
Write-Information -MessageData 'Both ModuleFast and PSResourceGet is configured or/and passed as parameter.' -InformationAction 'Continue'
if ($PSVersionTable.PSVersion -ge '7.2')
{
$UsePSResourceGet = $false
Write-Information -MessageData 'PowerShell 7.2 or higher being used, prefer ModuleFast over PSResourceGet.' -InformationAction 'Continue'
}
else
{
$UseModuleFast = $false
Write-Information -MessageData 'Older PowerShell or Windows PowerShell being used, prefer PSResourceGet since ModuleFast is not supported on this version of PowerShell.' -InformationAction 'Continue'
}
}
if ($UseModuleFast)
{
try
{
$invokeWebRequestParameters = @{
Uri = 'bit.ly/modulefast' # cSpell: disable-line
ErrorAction = 'Stop'
}
$moduleFastBootstrapScript = Invoke-WebRequest @invokeWebRequestParameters
<#
Using this method instead of the one mentioned in the instructions from
https://github.com/JustinGrote/ModuleFast to avoid the PSScriptAnalyzer
rule PSAvoidUsingInvokeExpression.
#>
$moduleFastBootstrapScriptBlock = [ScriptBlock]::Create($moduleFastBootstrapScript)
<#
We could pass parameters to the bootstrap script when calling Invoke().
But currently the default parameter values works just fine.
#>
$moduleFastBootstrapScriptBlock.Invoke()
}
catch
{
Write-Warning -Message ('ModuleFast could not be bootstrapped. Reverting to PowerShellGet. Error: {0}' -f $_.Exception.Message)
$UseModuleFast = $false
}
}
if ($UsePSResourceGet)
{
$psResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet'
# If PSResourceGet was used prior it will be locked and we can't replace it.
if ((Test-Path -Path "$PSDependTarget/$psResourceGetModuleName" -PathType 'Container') -and (Get-Module -Name $psResourceGetModuleName))
{
Write-Information -MessageData ('{0} is already saved and loaded into the session. To refresh the module open a new session and resolve dependencies again.' -f $psResourceGetModuleName) -InformationAction 'Continue'
}
else
{
Write-Debug -Message ('{0} do not exist, saving the module to RequiredModules.' -f $psResourceGetModuleName)
$psResourceGetDownloaded = $false
try
{
if (-not $PSResourceGetVersion)
{
# Default version to use if non is specified in parameter or in configuration.
$PSResourceGetVersion = '0.9.0-rc1'
}
$invokeWebRequestParameters = @{
# TODO: This should be hardcoded to a stable release in the future.
# TODO: Should support proxy parameters passed to the script.
Uri = "https://www.powershellgallery.com/api/v2/package/$psResourceGetModuleName/$PSResourceGetVersion"
OutFile = "$PSDependTarget/$psResourceGetModuleName.nupkg" # cSpell: ignore nupkg
ErrorAction = 'Stop'
}
$previousProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
# Bootstrapping Microsoft.PowerShell.PSResourceGet.
Invoke-WebRequest @invokeWebRequestParameters
$ProgressPreference = $previousProgressPreference
$psResourceGetDownloaded = $true
}
catch
{
Write-Warning -Message ('{0} could not be bootstrapped. Reverting to PowerShellGet. Error: {1}' -f $psResourceGetModuleName, $_.Exception.Message)
}
$UsePSResourceGet = $false
if ($psResourceGetDownloaded)
{
# On Windows PowerShell the command Expand-Archive do not like .nupkg as a zip archive extension.
$zipFileName = ((Split-Path -Path $invokeWebRequestParameters.OutFile -Leaf) -replace 'nupkg', 'zip')
$renameItemParameters = @{
Path = $invokeWebRequestParameters.OutFile
NewName = $zipFileName
Force = $true
}
Rename-Item @renameItemParameters
$psResourceGetZipArchivePath = Join-Path -Path (Split-Path -Path $invokeWebRequestParameters.OutFile -Parent) -ChildPath $zipFileName
$expandArchiveParameters = @{
Path = $psResourceGetZipArchivePath
DestinationPath = "$PSDependTarget/$psResourceGetModuleName"
Force = $true
}
Expand-Archive @expandArchiveParameters
Remove-Item -Path $psResourceGetZipArchivePath
Import-Module -Name $expandArchiveParameters.DestinationPath -Force
# Successfully bootstrapped PSResourceGet, so let's use it.
$UsePSResourceGet = $true
}
}
if ($UsePSResourceGet)
{
$psResourceGetModule = Get-Module -Name $psResourceGetModuleName
$psResourceGetModuleVersion = $psResourceGetModule.Version.ToString()
if ($psResourceGetModule.PrivateData.PSData.Prerelease)
{
$psResourceGetModuleVersion += '-{0}' -f $psResourceGetModule.PrivateData.PSData.Prerelease
}
Write-Information -MessageData ('Using {0} v{1}.' -f $psResourceGetModuleName, $psResourceGetModuleVersion) -InformationAction 'Continue'
if ($UsePowerShellGetCompatibilityModule)
{
$savePowerShellGetParameters = @{
Name = 'PowerShellGet'
Path = $PSDependTarget
Repository = 'PSGallery'
TrustRepository = $true
}
if ($UsePowerShellGetCompatibilityModuleVersion)
{
$savePowerShellGetParameters.Version = $UsePowerShellGetCompatibilityModuleVersion
# Check if the version is a prerelease.
if ($UsePowerShellGetCompatibilityModuleVersion -match '\d+\.\d+\.\d+-.*')
{
$savePowerShellGetParameters.Prerelease = $true
}
}
Save-PSResource @savePowerShellGetParameters
Import-Module -Name "$PSDependTarget/PowerShellGet"
}
}
}
if (-not ($UseModuleFast -or $UsePSResourceGet))
{
if ($PSVersionTable.PSVersion.Major -le 5)
{
<#
Making sure the imported PackageManagement module is not from PS7 module
path. The VSCode PS extension is changing the $env:PSModulePath and
prioritize the PS7 path. This is an issue with PowerShellGet because
it loads an old version if available (or fail to load latest).
#>
Get-Module -ListAvailable PackageManagement |
Where-Object -Property 'ModuleBase' -NotMatch 'powershell.7' |
Select-Object -First 1 |
Import-Module -Force
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 0 -CurrentOperation 'NuGet Bootstrap'
$importModuleParameters = @{
Name = 'PowerShellGet'
MinimumVersion = '2.0'
MaximumVersion = '2.8.999'
ErrorAction = 'SilentlyContinue'
PassThru = $true
}
if ($AllowOldPowerShellGetModule)
{
$importModuleParameters.Remove('MinimumVersion')
}
$powerShellGetModule = Import-Module @importModuleParameters
# Install the package provider if it is not available.
$nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable -ErrorAction 'SilentlyContinue' |
Select-Object -First 1
if (-not $powerShellGetModule -and -not $nuGetProvider)
{
$providerBootstrapParameters = @{
Name = 'NuGet'
Force = $true
ForceBootstrap = $true
ErrorAction = 'Stop'
Scope = $Scope
}
switch ($PSBoundParameters.Keys)
{
'Proxy'
{
$providerBootstrapParameters.Add('Proxy', $Proxy)
}
'ProxyCredential'
{
$providerBootstrapParameters.Add('ProxyCredential', $ProxyCredential)
}
'AllowPrerelease'
{
$providerBootstrapParameters.Add('AllowPrerelease', $AllowPrerelease)
}
}
Write-Information -MessageData 'Bootstrap: Installing NuGet Package Provider from the web (Make sure Microsoft addresses/ranges are allowed).'
$null = Install-PackageProvider @providerBootstrapParameters
$nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable | Select-Object -First 1
$nuGetProviderVersion = $nuGetProvider.Version.ToString()
Write-Information -MessageData "Bootstrap: Importing NuGet Package Provider version $nuGetProviderVersion to current session."
$Null = Import-PackageProvider -Name 'NuGet' -RequiredVersion $nuGetProviderVersion -Force
}
if ($RegisterGallery)
{
if ($RegisterGallery.ContainsKey('Name') -and -not [System.String]::IsNullOrEmpty($RegisterGallery.Name))
{
$Gallery = $RegisterGallery.Name
}
else
{
$RegisterGallery.Name = $Gallery
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 7 -CurrentOperation "Verifying private package repository '$Gallery'" -Completed
$previousRegisteredRepository = Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue'
if ($previousRegisteredRepository.SourceLocation -ne $RegisterGallery.SourceLocation)
{
if ($previousRegisteredRepository)
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Re-registrering private package repository '$Gallery'" -Completed
Unregister-PSRepository -Name $Gallery
$unregisteredPreviousRepository = $true
}
else
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Registering private package repository '$Gallery'" -Completed
}
Register-PSRepository @RegisterGallery
}
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 10 -CurrentOperation "Ensuring Gallery $Gallery is trusted"
# Fail if the given PSGallery is not registered.
$previousGalleryInstallationPolicy = (Get-PSRepository -Name $Gallery -ErrorAction 'Stop').Trusted
if ($previousGalleryInstallationPolicy -ne $true)
{
# Only change policy if the repository is not trusted
Set-PSRepository -Name $Gallery -InstallationPolicy 'Trusted' -ErrorAction 'Ignore'
}
}
try
{
if (-not ($UseModuleFast -or $UsePSResourceGet))
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 25 -CurrentOperation 'Checking PowerShellGet'
# Ensure the module is loaded and retrieve the version you have.
$powerShellGetVersion = (Import-Module -Name 'PowerShellGet' -PassThru -ErrorAction 'SilentlyContinue').Version
Write-Verbose -Message "Bootstrap: The PowerShellGet version is $powerShellGetVersion"
# Versions below 2.0 are considered old, unreliable & not recommended
if (-not $powerShellGetVersion -or ($powerShellGetVersion -lt [System.Version] '2.0' -and -not $AllowOldPowerShellGetModule))
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 40 -CurrentOperation 'Fetching newer version of PowerShellGet'
# PowerShellGet module not found, installing or saving it.
if ($PSDependTarget -in 'CurrentUser', 'AllUsers')
{
Write-Debug -Message "PowerShellGet module not found. Attempting to install from Gallery $Gallery."
Write-Warning -Message "Installing PowerShellGet in $PSDependTarget Scope."
$installPowerShellGetParameters = @{
Name = 'PowerShellGet'
Force = $true
SkipPublisherCheck = $true
AllowClobber = $true
Scope = $Scope
Repository = $Gallery
MaximumVersion = '2.8.999'
}
switch ($PSBoundParameters.Keys)
{
'Proxy'
{
$installPowerShellGetParameters.Add('Proxy', $Proxy)
}
'ProxyCredential'
{
$installPowerShellGetParameters.Add('ProxyCredential', $ProxyCredential)
}
'GalleryCredential'
{
$installPowerShellGetParameters.Add('Credential', $GalleryCredential)
}
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation 'Installing newer version of PowerShellGet'
Install-Module @installPowerShellGetParameters
}
else
{
Write-Debug -Message "PowerShellGet module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget"
$saveModuleParameters = @{
Name = 'PowerShellGet'
Repository = $Gallery
Path = $PSDependTarget
Force = $true
MaximumVersion = '2.8.999'
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation "Saving PowerShellGet from $Gallery to $Scope"
Save-Module @saveModuleParameters
}
Write-Debug -Message 'Removing previous versions of PowerShellGet and PackageManagement from session'
Get-Module -Name 'PowerShellGet' -All | Remove-Module -Force -ErrorAction 'SilentlyContinue'
Get-Module -Name 'PackageManagement' -All | Remove-Module -Force
Write-Progress -Activity 'Bootstrap:' -PercentComplete 65 -CurrentOperation 'Loading latest version of PowerShellGet'
Write-Debug -Message 'Importing latest PowerShellGet and PackageManagement versions into session'
if ($AllowOldPowerShellGetModule)
{
$powerShellGetModule = Import-Module -Name 'PowerShellGet' -Force -PassThru
}
else
{
Import-Module -Name 'PackageManagement' -MinimumVersion '1.4.8.1' -Force
$powerShellGetModule = Import-Module -Name 'PowerShellGet' -MinimumVersion '2.2.5' -Force -PassThru
}
$powerShellGetVersion = $powerShellGetModule.Version.ToString()
Write-Information -MessageData "Bootstrap: PowerShellGet version loaded is $powerShellGetVersion"
}
# Try to import the PSDepend module from the available modules.
$getModuleParameters = @{
Name = 'PSDepend'
ListAvailable = $true
}
$psDependModule = Get-Module @getModuleParameters
if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion'))
{
try
{
$psDependModule = $psDependModule | Where-Object -FilterScript { $_.Version -ge $MinimumPSDependVersion }
}
catch
{
throw ('There was a problem finding the minimum version of PSDepend. Error: {0}' -f $_)
}
}
if (-not $psDependModule)
{
Write-Debug -Message 'PSDepend module not found.'
# PSDepend module not found, installing or saving it.
if ($PSDependTarget -in 'CurrentUser', 'AllUsers')
{
Write-Debug -Message "Attempting to install from Gallery '$Gallery'."
Write-Warning -Message "Installing PSDepend in $PSDependTarget Scope."
$installPSDependParameters = @{
Name = 'PSDepend'
Repository = $Gallery
Force = $true
Scope = $PSDependTarget
SkipPublisherCheck = $true
AllowClobber = $true
}
if ($MinimumPSDependVersion)
{
$installPSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion)
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Installing PSDepend from $Gallery"
Install-Module @installPSDependParameters
}
else
{
Write-Debug -Message "Attempting to Save from Gallery $Gallery to $PSDependTarget"
$saveModuleParameters = @{
Name = 'PSDepend'
Repository = $Gallery
Path = $PSDependTarget
Force = $true
}
if ($MinimumPSDependVersion)
{
$saveModuleParameters.add('MinimumVersion', $MinimumPSDependVersion)
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Saving PSDepend from $Gallery to $PSDependTarget"
Save-Module @saveModuleParameters
}
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 80 -CurrentOperation 'Importing PSDepend'
$importModulePSDependParameters = @{
Name = 'PSDepend'
ErrorAction = 'Stop'
Force = $true
}
if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion'))
{
$importModulePSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion)
}
# We should have successfully bootstrapped PSDepend. Fail if not available.
$null = Import-Module @importModulePSDependParameters
Write-Progress -Activity 'Bootstrap:' -PercentComplete 81 -CurrentOperation 'Invoke PSDepend'
if ($WithYAML)
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 82 -CurrentOperation 'Verifying PowerShell module PowerShell-Yaml'
if (-not (Get-Module -ListAvailable -Name 'PowerShell-Yaml'))
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 85 -CurrentOperation 'Installing PowerShell module PowerShell-Yaml'
Write-Verbose -Message "PowerShell-Yaml module not found. Attempting to Save from Gallery '$Gallery' to '$PSDependTarget'."
$SaveModuleParam = @{
Name = 'PowerShell-Yaml'
Repository = $Gallery
Path = $PSDependTarget
Force = $true
}
Save-Module @SaveModuleParam
}
else
{
Write-Verbose -Message 'PowerShell-Yaml is already available'
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 88 -CurrentOperation 'Importing PowerShell module PowerShell-Yaml'
}
}
if (Test-Path -Path $DependencyFile)
{
if ($UseModuleFast -or $UsePSResourceGet)
{
$requiredModules = Import-PowerShellDataFile -Path $DependencyFile
$requiredModules = $requiredModules.GetEnumerator() |
Where-Object -FilterScript { $_.Name -ne 'PSDependOptions' }
$modulesToSave = @(
'PSDepend' # Always include PSDepend for backward compatibility.
)
foreach ($requiredModule in $requiredModules)
{
# If the RequiredModules.psd1 entry is an Hashtable then special handling is needed.
if ($requiredModule.Value -is [System.Collections.Hashtable])
{
$saveModuleHashtable = @{
ModuleName = $requiredModule.Name
}
if ($requiredModule.Value.Version -and $requiredModule.Value.Version -ne 'latest')
{
$saveModuleHashtable.RequiredVersion = $requiredModule.Value.Version
}
# ModuleFast does no support preview releases yet.
if ($UsePSResourceGet)
{
if ($requiredModule.Value.Parameters.AllowPrerelease -eq $true)
{
$saveModuleHashtable.Prerelease = $true
}
}
$modulesToSave += $saveModuleHashtable
}
else
{
if ($requiredModule.Value -eq 'latest')
{
$modulesToSave += $requiredModule.Name
}
else
{
$modulesToSave += @{
ModuleName = $requiredModule.Name
RequiredVersion = $requiredModule.Value
}
}
}
}
if ($WithYAML)
{
$modulesToSave += 'PowerShell-Yaml'
}
if ($UseModuleFast)
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking ModuleFast'
Write-Progress -Activity 'ModuleFast:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies'
$moduleFastPlan = $modulesToSave | Get-ModuleFastPlan
if ($moduleFastPlan)
{
# Clear all modules in plan from the current session so they can be fetched again.
$moduleFastPlan.Name | Get-Module | Remove-Module -Force
$installModuleFastParameters = @{
ModulesToInstall = $moduleFastPlan
Destination = $PSDependTarget
NoPSModulePathUpdate = $true
NoProfileUpdate = $true
Update = $true
Confirm = $false
}
Install-ModuleFast @installModuleFastParameters
}
else
{
Write-Verbose -Message 'All modules were already up to date'
}
Write-Progress -Activity 'ModuleFast:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed
}
if ($UsePSResourceGet)
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSResourceGet'
$progressPercentage = 0
Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies'
$percentagePerModule = [System.Math]::Floor(100 / $modulesToSave.Length)
foreach ($currentModule in $modulesToSave)
{
Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Saving module {0}' -f $savePSResourceParameters.Name)
$savePSResourceParameters = @{
Path = $PSDependTarget
TrustRepository = $true
Confirm = $false
}
if ($currentModule -is [System.Collections.Hashtable])
{
$savePSResourceParameters.Name = $currentModule.ModuleName
if ($currentModule.RequiredVersion)
{
$savePSResourceParameters.Version = $currentModule.RequiredVersion
}
if ($currentModule.Prerelease)
{
$savePSResourceParameters.Prerelease = $currentModule.Prerelease
}
}
else
{
$savePSResourceParameters.Name = $currentModule
}
# Modules that Sampler depend on that cannot be refreshed without a new session.
$skipModule = @('powershell-yaml')
if ($savePSResourceParameters.Name -in $skipModule -and (Get-Module -Name 'powershell-yaml'))
{
Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Skipping module {0}' -f $savePSResourceParameters.Name)
Write-Information -MessageData ('Skipping the module {0} since it cannot be refresh while loaded into the session. To refresh the module open a new session and resolve dependencies again.' -f $savePSResourceParameters.Name) -InformationAction 'Continue'
}
else
{
# Clear all module from the current session so any new version fetched will be re-imported.
Get-Module -Name $savePSResourceParameters.Name | Remove-Module -Force
Save-PSResource @savePSResourceParameters -ErrorVariable 'savePSResourceError'
if ($savePSResourceError)
{
Write-Warning -Message 'Save-PSResource could not save (replace) one or more dependencies. This can be due to the module is loaded into the session (and referencing assemblies). Close the current session and open a new session and try again.'
}
}
$progressPercentage += $percentagePerModule
}
Write-Progress -Activity 'PSResourceGet:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed
}
}
else
{
Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSDepend'
Write-Progress -Activity 'PSDepend:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies'
$psDependParameters = @{
Force = $true
Path = $DependencyFile
}
# TODO: Handle when the Dependency file is in YAML, and -WithYAML is specified.
Invoke-PSDepend @psDependParameters
Write-Progress -Activity 'PSDepend:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed
}
}
Write-Progress -Activity 'Bootstrap:' -PercentComplete 100 -CurrentOperation 'Bootstrap complete' -Completed
}
finally
{
if ($RegisterGallery)
{
Write-Verbose -Message "Removing private package repository '$Gallery'."
Unregister-PSRepository -Name $Gallery
}
if ($unregisteredPreviousRepository)
{
Write-Verbose -Message "Reverting private package repository '$Gallery' to previous location URI:s."
$registerPSRepositoryParameters = @{
Name = $previousRegisteredRepository.Name
InstallationPolicy = $previousRegisteredRepository.InstallationPolicy
}
if ($previousRegisteredRepository.SourceLocation)
{
$registerPSRepositoryParameters.SourceLocation = $previousRegisteredRepository.SourceLocation
}
if ($previousRegisteredRepository.PublishLocation)
{
$registerPSRepositoryParameters.PublishLocation = $previousRegisteredRepository.PublishLocation
}
if ($previousRegisteredRepository.ScriptSourceLocation)
{
$registerPSRepositoryParameters.ScriptSourceLocation = $previousRegisteredRepository.ScriptSourceLocation
}
if ($previousRegisteredRepository.ScriptPublishLocation)
{
$registerPSRepositoryParameters.ScriptPublishLocation = $previousRegisteredRepository.ScriptPublishLocation
}
Register-PSRepository @registerPSRepositoryParameters
}
# Only try to revert installation policy if the repository exist
if ((Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue'))
{
if ($previousGalleryInstallationPolicy -ne $true)
{
# Reverting the Installation Policy for the given gallery if it was not already trusted
Set-PSRepository -Name $Gallery -InstallationPolicy 'Untrusted'
}
}
Write-Verbose -Message 'Project Bootstrapped, returning to Invoke-Build.'
}

49
Resolve-Dependency.psd1 Normal file
View File

@@ -0,0 +1,49 @@
@{
<#
Default parameter values to be loaded by the Resolve-Dependency.ps1 script (unless set in bound parameters
when calling the script).
#>
#PSDependTarget = './output/modules'
#Proxy = ''
#ProxyCredential = '$MyCredentialVariable' #TODO: find a way to support credentials in build (resolve variable)
Gallery = 'PSGallery'
# To use a private nuget repository change the following to your own feed. The locations must be a Nuget v2 feed due
# to limitation in PowerShellGet v2.x. Example below is for a Azure DevOps Server project-scoped feed. While resolving
# dependencies it will be registered as a trusted repository with the name specified in the property 'Gallery' above,
# unless property 'Name' is provided in the hashtable below, if so it will override the property 'Gallery' above. The
# registered repository will be removed when dependencies has been resolved, unless it was already registered to begin
# with. If repository is registered already but with different URL:s the repository will be re-registered and reverted
# after dependencies has been resolved. Currently only Windows integrated security works with private Nuget v2 feeds
# (or if it is a public feed with no security), it is not possible yet to securely provide other credentials for the feed.
# Private repositories will currently only work using PowerShellGet.
#RegisterGallery = @{
# #Name = 'MyPrivateFeedName'
# GallerySourceLocation = 'https://azdoserver.company.local/<org_name>/<project_name>/_packaging/<feed_name>/nuget/v2'
# GalleryPublishLocation = 'https://azdoserver.company.local/<org_name>/<project_name>/_packaging/<feed_name>/nuget/v2'
# GalleryScriptSourceLocation = 'https://azdoserver.company.local/<org_name>/<project_name>/_packaging/<feed_name>/nuget/v2'
# GalleryScriptPublishLocation = 'https://azdoserver.company.local/<org_name>/<project_name>/_packaging/<feed_name>/nuget/v2'
# #InstallationPolicy = 'Trusted'
#}
#AllowOldPowerShellGetModule = $true
#MinimumPSDependVersion = '0.3.0'
AllowPrerelease = $false
WithYAML = $true # Will also bootstrap PowerShell-Yaml to read other config files
# Enable ModuleFast to resolve dependencies. Requires PowerShell 7.2 or higher.
# If this is not configured or set to $false then PowerShellGet and PackageManagement
# will be used to resolve dependencies.
#UseModuleFast = $true
# Enable PSResourceGet to resolve dependencies. Requires PowerShell 7.2 or higher.
# If this is not configured or set to $false then PowerShellGet and PackageManagement
# will be used to resolve dependencies.
#UsePSResourceGet = $true
#PSResourceGetVersion = '1.0.0'
#UsePowerShellGetCompatibilityModule = $true
#UsePowerShellGetCompatibilityModuleVersion = '3.0.22-beta22'
}

538
build.ps1 Normal file
View File

@@ -0,0 +1,538 @@
<#
.DESCRIPTION
Bootstrap and build script for PowerShell module CI/CD pipeline.
.PARAMETER Tasks
The task or tasks to run. The default value is '.' (runs the default task).
.PARAMETER CodeCoverageThreshold
The code coverage target threshold to uphold. Set to 0 to disable.
The default value is '' (empty string).
.PARAMETER BuildConfig
Not yet written.
.PARAMETER OutputDirectory
Specifies the folder to build the artefact into. The default value is 'output'.
.PARAMETER BuiltModuleSubdirectory
Subdirectory name to build the module (under $OutputDirectory). The default
value is '' (empty string).
.PARAMETER RequiredModulesDirectory
Can be a path (relative to $PSScriptRoot or absolute) to tell Resolve-Dependency
and PSDepend where to save the required modules. It is also possible to use
'CurrentUser' och 'AllUsers' to install missing dependencies. You can override
the value for PSDepend in the Build.psd1 build manifest. The default value is
'output/RequiredModules'.
.PARAMETER PesterScript
One or more paths that will override the Pester configuration in build
configuration file when running the build task Invoke_Pester_Tests.
If running Pester 5 test, use the alias PesterPath to be future-proof.
.PARAMETER PesterTag
Filter which tags to run when invoking Pester tests. This is used in the
Invoke-Pester.pester.build.ps1 tasks.
.PARAMETER PesterExcludeTag
Filter which tags to exclude when invoking Pester tests. This is used in
the Invoke-Pester.pester.build.ps1 tasks.
.PARAMETER DscTestTag
Filter which tags to run when invoking DSC Resource tests. This is used
in the DscResource.Test.build.ps1 tasks.
.PARAMETER DscTestExcludeTag
Filter which tags to exclude when invoking DSC Resource tests. This is
used in the DscResource.Test.build.ps1 tasks.
.PARAMETER ResolveDependency
Not yet written.
.PARAMETER BuildInfo
The build info object from ModuleBuilder. Defaults to an empty hashtable.
.PARAMETER AutoRestore
Not yet written.
.PARAMETER UseModuleFast
Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies
faster.
.PARAMETER UsePSResourceGet
Specifies to use PSResourceGet instead of PowerShellGet to resolve dependencies
faster. This can also be configured in Resolve-Dependency.psd1.
.PARAMETER UsePowerShellGetCompatibilityModule
Specifies to use the compatibility module PowerShellGet. This parameter
only works then the method of downloading dependencies is PSResourceGet.
This can also be configured in Resolve-Dependency.psd1.
#>
[CmdletBinding()]
param
(
[Parameter(Position = 0)]
[System.String[]]
$Tasks = '.',
[Parameter()]
[System.String]
$CodeCoverageThreshold = '',
[Parameter()]
[System.String]
[ValidateScript(
{ Test-Path -Path $_ }
)]
$BuildConfig,
[Parameter()]
[System.String]
$OutputDirectory = 'output',
[Parameter()]
[System.String]
$BuiltModuleSubdirectory = '',
[Parameter()]
[System.String]
$RequiredModulesDirectory = $(Join-Path 'output' 'RequiredModules'),
[Parameter()]
# This alias is to prepare for the rename of this parameter to PesterPath when Pester 4 support is removed
[Alias('PesterPath')]
[System.Object[]]
$PesterScript,
[Parameter()]
[System.String[]]
$PesterTag,
[Parameter()]
[System.String[]]
$PesterExcludeTag,
[Parameter()]
[System.String[]]
$DscTestTag,
[Parameter()]
[System.String[]]
$DscTestExcludeTag,
[Parameter()]
[Alias('bootstrap')]
[System.Management.Automation.SwitchParameter]
$ResolveDependency,
[Parameter(DontShow)]
[AllowNull()]
[System.Collections.Hashtable]
$BuildInfo,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$AutoRestore,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$UseModuleFast,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$UsePSResourceGet,
[Parameter()]
[System.Management.Automation.SwitchParameter]
$UsePowerShellGetCompatibilityModule
)
<#
The BEGIN block (at the end of this file) handles the Bootstrap of the Environment
before Invoke-Build can run the tasks if the parameter ResolveDependency (or
parameter alias Bootstrap) is specified.
#>
process
{
if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1')
{
# Only run the process block through InvokeBuild (look at the Begin block at the bottom of this script).
return
}
# Execute the Build process from the .build.ps1 path.
Push-Location -Path $PSScriptRoot -StackName 'BeforeBuild'
try
{
Write-Host -Object "[build] Parsing defined tasks" -ForeGroundColor Magenta
# Load the default BuildInfo if the parameter BuildInfo is not set.
if (-not $PSBoundParameters.ContainsKey('BuildInfo'))
{
try
{
if (Test-Path -Path $BuildConfig)
{
$configFile = Get-Item -Path $BuildConfig
Write-Host -Object "[build] Loading Configuration from $configFile"
$BuildInfo = switch -Regex ($configFile.Extension)
{
# Native Support for PSD1
'\.psd1'
{
if (-not (Get-Command -Name Import-PowerShellDataFile -ErrorAction SilentlyContinue))
{
Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion 3.1.0.0
}
Import-PowerShellDataFile -Path $BuildConfig
}
# Support for yaml when module PowerShell-Yaml is available
'\.[yaml|yml]'
{
Import-Module -Name 'powershell-yaml' -ErrorAction Stop
ConvertFrom-Yaml -Yaml (Get-Content -Raw $configFile)
}
# Support for JSON and JSONC (by Removing comments) when module PowerShell-Yaml is available
'\.[json|jsonc]'
{
$jsonFile = Get-Content -Raw -Path $configFile
$jsonContent = $jsonFile -replace '(?m)\s*//.*?$' -replace '(?ms)/\*.*?\*/'
# Yaml is superset of JSON.
ConvertFrom-Yaml -Yaml $jsonContent
}
# Unknown extension, return empty hashtable.
default
{
Write-Error -Message "Extension '$_' not supported. using @{}"
@{ }
}
}
}
else
{
Write-Host -Object "Configuration file '$($BuildConfig.FullName)' not found" -ForegroundColor Red
# No config file was found, return empty hashtable.
$BuildInfo = @{ }
}
}
catch
{
$logMessage = "Error loading Config '$($BuildConfig.FullName)'.`r`nAre you missing dependencies?`r`nMake sure you run './build.ps1 -ResolveDependency -tasks noop' before running build to restore the required modules."
Write-Host -Object $logMessage -ForegroundColor Yellow
$BuildInfo = @{ }
Write-Error -Message $_.Exception.Message
}
}
# If the Invoke-Build Task Header is specified in the Build Info, set it.
if ($BuildInfo.TaskHeader)
{
Set-BuildHeader -Script ([scriptblock]::Create($BuildInfo.TaskHeader))
}
<#
Add BuildModuleOutput to PSModule Path environment variable.
Moved here (not in begin block) because build file can contains BuiltSubModuleDirectory value.
#>
if ($BuiltModuleSubdirectory)
{
if (-not (Split-Path -IsAbsolute -Path $BuiltModuleSubdirectory))
{
$BuildModuleOutput = Join-Path -Path $OutputDirectory -ChildPath $BuiltModuleSubdirectory
}
else
{
$BuildModuleOutput = $BuiltModuleSubdirectory
}
} # test if BuiltModuleSubDirectory set in build config file
elseif ($BuildInfo.ContainsKey('BuiltModuleSubDirectory'))
{
$BuildModuleOutput = Join-Path -Path $OutputDirectory -ChildPath $BuildInfo['BuiltModuleSubdirectory']
}
else
{
$BuildModuleOutput = $OutputDirectory
}
# Pre-pending $BuildModuleOutput folder to PSModulePath to resolve built module from this folder.
if ($powerShellModulePaths -notcontains $BuildModuleOutput)
{
Write-Host -Object "[build] Pre-pending '$BuildModuleOutput' folder to PSModulePath" -ForegroundColor Green
$env:PSModulePath = $BuildModuleOutput + [System.IO.Path]::PathSeparator + $env:PSModulePath
}
<#
Import Tasks from modules via their exported aliases when defined in Build Manifest.
https://github.com/nightroman/Invoke-Build/tree/master/Tasks/Import#example-2-import-from-a-module-with-tasks
#>
if ($BuildInfo.ContainsKey('ModuleBuildTasks'))
{
foreach ($module in $BuildInfo['ModuleBuildTasks'].Keys)
{
try
{
Write-Host -Object "Importing tasks from module $module" -ForegroundColor DarkGray
$loadedModule = Import-Module -Name $module -PassThru -ErrorAction Stop
foreach ($TaskToExport in $BuildInfo['ModuleBuildTasks'].($module))
{
$loadedModule.ExportedAliases.GetEnumerator().Where{
Write-Host -Object "`t Loading $($_.Key)..." -ForegroundColor DarkGray
# Using -like to support wildcard.
$_.Key -like $TaskToExport
}.ForEach{
# Dot-sourcing the Tasks via their exported aliases.
. (Get-Alias $_.Key)
}
}
}
catch
{
Write-Host -Object "Could not load tasks for module $module." -ForegroundColor Red
Write-Error -Message $_
}
}
}
# Loading Build Tasks defined in the .build/ folder (will override the ones imported above if same task name).
Get-ChildItem -Path '.build/' -Recurse -Include '*.ps1' -ErrorAction Ignore |
ForEach-Object {
"Importing file $($_.BaseName)" | Write-Verbose
. $_.FullName
}
# Synopsis: Empty task, useful to test the bootstrap process.
task noop { }
# Define default task sequence ("."), can be overridden in the $BuildInfo.
task . {
Write-Build -Object 'No sequence currently defined for the default task' -ForegroundColor Yellow
}
Write-Host -Object 'Adding Workflow from configuration:' -ForegroundColor DarkGray
# Load Invoke-Build task sequences/workflows from $BuildInfo.
foreach ($workflow in $BuildInfo.BuildWorkflow.keys)
{
Write-Verbose -Message "Creating Build Workflow '$Workflow' with tasks $($BuildInfo.BuildWorkflow.($Workflow) -join ', ')."
$workflowItem = $BuildInfo.BuildWorkflow.($workflow)
if ($workflowItem.Trim() -match '^\{(?<sb>[\w\W]*)\}$')
{
$workflowItem = [ScriptBlock]::Create($Matches['sb'])
}
Write-Host -Object " +-> $workflow" -ForegroundColor DarkGray
task $workflow $workflowItem
}
Write-Host -Object "[build] Executing requested workflow: $($Tasks -join ', ')" -ForeGroundColor Magenta
}
finally
{
Pop-Location -StackName 'BeforeBuild'
}
}
begin
{
# Find build config if not specified.
if (-not $BuildConfig)
{
$config = Get-ChildItem -Path "$PSScriptRoot\*" -Include 'build.y*ml', 'build.psd1', 'build.json*' -ErrorAction Ignore
if (-not $config -or ($config -is [System.Array] -and $config.Length -le 0))
{
throw 'No build configuration found. Specify path via parameter BuildConfig.'
}
elseif ($config -is [System.Array])
{
if ($config.Length -gt 1)
{
throw 'More than one build configuration found. Specify which path to use via parameter BuildConfig.'
}
$BuildConfig = $config[0]
}
else
{
$BuildConfig = $config
}
}
# Bootstrapping the environment before using Invoke-Build as task runner
if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1')
{
Write-Host -Object "[pre-build] Starting Build Init" -ForegroundColor Green
Push-Location $PSScriptRoot -StackName 'BuildModule'
}
if ($RequiredModulesDirectory -in @('CurrentUser', 'AllUsers'))
{
# Installing modules instead of saving them.
Write-Host -Object "[pre-build] Required Modules will be installed to the PowerShell module path that is used for $RequiredModulesDirectory." -ForegroundColor Green
<#
The variable $PSDependTarget will be used below when building the splatting
variable before calling Resolve-Dependency.ps1, unless overridden in the
file Resolve-Dependency.psd1.
#>
$PSDependTarget = $RequiredModulesDirectory
}
else
{
if (-not (Split-Path -IsAbsolute -Path $OutputDirectory))
{
$OutputDirectory = Join-Path -Path $PSScriptRoot -ChildPath $OutputDirectory
}
# Resolving the absolute path to save the required modules to.
if (-not (Split-Path -IsAbsolute -Path $RequiredModulesDirectory))
{
$RequiredModulesDirectory = Join-Path -Path $PSScriptRoot -ChildPath $RequiredModulesDirectory
}
# Create the output/modules folder if not exists, or resolve the Absolute path otherwise.
if (Resolve-Path -Path $RequiredModulesDirectory -ErrorAction SilentlyContinue)
{
Write-Debug -Message "[pre-build] Required Modules path already exist at $RequiredModulesDirectory"
$requiredModulesPath = Convert-Path -Path $RequiredModulesDirectory
}
else
{
Write-Host -Object "[pre-build] Creating required modules directory $RequiredModulesDirectory." -ForegroundColor Green
$requiredModulesPath = (New-Item -ItemType Directory -Force -Path $RequiredModulesDirectory).FullName
}
$powerShellModulePaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator
# Pre-pending $requiredModulesPath folder to PSModulePath to resolve from this folder FIRST.
if ($RequiredModulesDirectory -notin @('CurrentUser', 'AllUsers') -and
($powerShellModulePaths -notcontains $RequiredModulesDirectory))
{
Write-Host -Object "[pre-build] Pre-pending '$RequiredModulesDirectory' folder to PSModulePath" -ForegroundColor Green
$env:PSModulePath = $RequiredModulesDirectory + [System.IO.Path]::PathSeparator + $env:PSModulePath
}
$powerShellYamlModule = Get-Module -Name 'powershell-yaml' -ListAvailable
$invokeBuildModule = Get-Module -Name 'InvokeBuild' -ListAvailable
$psDependModule = Get-Module -Name 'PSDepend' -ListAvailable
# Checking if the user should -ResolveDependency.
if (-not ($powerShellYamlModule -and $invokeBuildModule -and $psDependModule) -and -not $ResolveDependency)
{
if ($AutoRestore -or -not $PSBoundParameters.ContainsKey('Tasks') -or $Tasks -contains 'build')
{
Write-Host -Object "[pre-build] Dependency missing, running './build.ps1 -ResolveDependency -Tasks noop' for you `r`n" -ForegroundColor Yellow
$ResolveDependency = $true
}
else
{
Write-Warning -Message "Some required Modules are missing, make sure you first run with the '-ResolveDependency' parameter. Running 'build.ps1 -ResolveDependency -Tasks noop' will pull required modules without running the build task."
}
}
<#
The variable $PSDependTarget will be used below when building the splatting
variable before calling Resolve-Dependency.ps1, unless overridden in the
file Resolve-Dependency.psd1.
#>
$PSDependTarget = $requiredModulesPath
}
if ($ResolveDependency)
{
Write-Host -Object "[pre-build] Resolving dependencies using preferred method." -ForegroundColor Green
$resolveDependencyParams = @{ }
# If BuildConfig is a Yaml file, bootstrap powershell-yaml via ResolveDependency.
if ($BuildConfig -match '\.[yaml|yml]$')
{
$resolveDependencyParams.Add('WithYaml', $true)
}
$resolveDependencyAvailableParams = (Get-Command -Name '.\Resolve-Dependency.ps1').Parameters.Keys
foreach ($cmdParameter in $resolveDependencyAvailableParams)
{
# The parameter has been explicitly used for calling the .build.ps1
if ($MyInvocation.BoundParameters.ContainsKey($cmdParameter))
{
$paramValue = $MyInvocation.BoundParameters.Item($cmdParameter)
Write-Debug " adding $cmdParameter :: $paramValue [from user-provided parameters to Build.ps1]"
$resolveDependencyParams.Add($cmdParameter, $paramValue)
}
# Use defaults parameter value from Build.ps1, if any
else
{
$paramValue = Get-Variable -Name $cmdParameter -ValueOnly -ErrorAction Ignore
if ($paramValue)
{
Write-Debug " adding $cmdParameter :: $paramValue [from default Build.ps1 variable]"
$resolveDependencyParams.Add($cmdParameter, $paramValue)
}
}
}
Write-Host -Object "[pre-build] Starting bootstrap process." -ForegroundColor Green
.\Resolve-Dependency.ps1 @resolveDependencyParams
}
if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1')
{
Write-Verbose -Message "Bootstrap completed. Handing back to InvokeBuild."
if ($PSBoundParameters.ContainsKey('ResolveDependency'))
{
Write-Verbose -Message "Dependency already resolved. Removing task."
$null = $PSBoundParameters.Remove('ResolveDependency')
}
Write-Host -Object "[build] Starting build with InvokeBuild." -ForegroundColor Green
Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInvocation.MyCommand.Path
Pop-Location -StackName 'BuildModule'
return
}
}

151
build.yaml Normal file
View File

@@ -0,0 +1,151 @@
---
####################################################
# ModuleBuilder Configuration #
####################################################
BuiltModuleSubdirectory: module
CopyPaths:
- en-US
- tests
- helper
# - DSCResources
# - Modules
Encoding: UTF8
# Can be used to manually specify module's semantic version if the preferred method of
# using GitVersion is not available, and it is not possible to set the session environment
# variable `$env:ModuleVersion`, nor setting the variable `$ModuleVersion`, in the
# PowerShell session (parent scope) before running the task `build`.
#SemVer: '99.0.0-preview1'
# Suffix to add to Root module PSM1 after merge (here, the Set-Alias exporting IB tasks)
# suffix: suffix.ps1
# prefix: prefix.ps1
VersionedOutputDirectory: true
####################################################
# ModuleBuilder Submodules Configuration #
####################################################
NestedModule:
# HelperSubmodule: # This is the first submodule to build into the output
# Path: ./*/Modules/HelperSubmodule/HelperSubmodule.psd1
# # is trimmed (remove metadata & Prerelease tag) and OutputDirectory expanded (the only one)
# OutputDirectory: ///Modules/HelperSubmodule
# VersionedOutputDirectory: false
# AddToManifest: false
# SemVer:
# # suffix:
# # prefix:
####################################################
# Sampler Pipeline Configuration #
####################################################
# Defining 'Workflows' (suite of InvokeBuild tasks) to be run using their alias
BuildWorkflow:
'.': # "." is the default Invoke-Build workflow. It is called when no -Tasks is specified to the build.ps1
- build
- test
build:
- Clean
- Build_Module_ModuleBuilder
- Build_NestedModules_ModuleBuilder
- Create_changelog_release_output
pack:
- build
- package_module_nupkg
# Defining test task to be run when invoking `./build.ps1 -Tasks test`
test:
# Uncomment to modify the PSModulePath in the test pipeline (also requires the build configuration section SetPSModulePath).
#- Set_PSModulePath
- Pester_Tests_Stop_On_Fail
# Use this task if pipeline uses code coverage and the module is using the
# pattern of Public, Private, Enum, Classes.
#- Convert_Pester_Coverage
- Pester_if_Code_Coverage_Under_Threshold
# Use this task when you have multiple parallel tests, which produce multiple
# code coverage files and needs to get merged into one file.
#merge:
#- Merge_CodeCoverage_Files
publish:
- publish_module_to_gallery
- Publish_Release_To_GitHub
####################################################
# PESTER Configuration #
####################################################
Pester:
OutputFormat: NUnitXML
# Excludes one or more paths from being used to calculate code coverage.
ExcludeFromCodeCoverage:
# If no scripts are defined the default is to use all the tests under the project's
# tests folder or source folder (if present). Test script paths can be defined to
# only run tests in certain folders, or run specific test files, or can be use to
# specify the order tests are run.
Script:
# - tests/QA/module.tests.ps1
# - tests/QA
# - tests/Unit
# - tests/Integration
ExcludeTag:
# - helpQuality
# - FunctionalQuality
# - TestQuality
Tag:
CodeCoverageThreshold: 85 # Set to 0 to bypass
#CodeCoverageOutputFile: JaCoCo_$OsShortName.xml
#CodeCoverageOutputFileEncoding: ascii
# Use this if code coverage should be merged from several pipeline test jobs.
# Any existing keys above should be replaced. See also CodeCoverage below.
# CodeCoverageOutputFile is the file that is created for each pipeline test job.
#CodeCoverageOutputFile: JaCoCo_Merge.xml
# Use this to merged code coverage from several pipeline test jobs.
# CodeCoverageFilePattern - the pattern used to search all pipeline test job artifacts
# after the file specified in CodeCoverageOutputFile.
# CodeCoverageMergedOutputFile - the file that is created by the merge build task and
# is the file that should be uploaded to code coverage services.
#CodeCoverage:
#CodeCoverageFilePattern: JaCoCo_Merge.xml # the pattern used to search all pipeline test job artifacts
#CodeCoverageMergedOutputFile: JaCoCo_coverage.xml # the file that is created for the merged code coverage
# Import ModuleBuilder tasks from a specific PowerShell module using the build
# task's alias. Wildcard * can be used to specify all tasks that has a similar
# prefix and or suffix. The module contain the task must be added as a required
# module in the file RequiredModules.psd1.
ModuleBuildTasks:
Sampler:
- '*.build.Sampler.ib.tasks'
Sampler.GitHubTasks:
- '*.ib.tasks'
# Invoke-Build Header to be used to 'decorate' the terminal output of the tasks.
TaskHeader: |
param($Path)
""
"=" * 79
Write-Build Cyan "`t`t`t$($Task.Name.replace("_"," ").ToUpper())"
Write-Build DarkGray "$(Get-BuildSynopsis $Task)"
"-" * 79
Write-Build DarkGray " $Path"
Write-Build DarkGray " $($Task.InvocationInfo.ScriptName):$($Task.InvocationInfo.ScriptLineNumber)"
""

378
docs/index.html Normal file
View File

@@ -0,0 +1,378 @@
<!DOCTYPE html>
<!--
<auto-generated>
<synopsis>
This code was generated by a tool. on: 03/25/2024 08:16:49
</synopsis>
<description>
If you'd like to regenerate the documentation, please open up powershell and run
> .\psDoc.ps1 -moduleName NameOfYourModule
If the documentation is incomplete, or eronious,
please edit the comments at the top of the module method within it's respecive .ps1 file.
</description>
</auto-generated>
-->
<html lang="en">
<head>
<title>M365FoundationsCISReport Documentation</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/styles/shCore.min.css" rel="stylesheet" charset="utf-8">
<link href="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/styles/shCoreDefault.min.css" rel="stylesheet" charset="utf-8">
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet" charset="utf-8">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<style>
.syntaxhighlighter {
overflow-y: hidden !important;
overflow-x: auto !important;
}
pre {
min-height: 30px;
}
.navbar-nav {
height: 100%;
overflow-y: auto;
}
.form-group {
padding-top: 12px;
padding-left: 12px;
padding-right: 12px;
}
.sidebar-nav .navbar-header {
float: none;
}
.sidebar-nav .navbar li a {
padding-top: 4px;
padding-bottom: 4px;
}
@media (min-width: 768px) {
.sidebar-nav .navbar .navbar-collapse {
padding: 0;
max-height: none;
}
.sidebar-nav .navbar ul {
float: none;
}
.sidebar-nav .navbar ul:not {
display: block;
}
.sidebar-nav .navbar li {
float: none;
display: block;
}
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row-fluid">
<div><h1>M365FoundationsCISReport</h1></div>
</div>
<div class="row-fluid">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<div class="sidebar-nav">
<div class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".sidebar-navbar-collapse">
<span class="sr-only">Toggle</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<span class="visible-xs navbar-brand">click menu to open</span>
</div>
<div class="navbar-collapse collapse sidebar-navbar-collapse">
<div class="form-group">
<input class="form-control" id="searchinput" type="search" placeholder="Filter..." />
</div>
<ul class="nav navbar-nav list-group" id="searchList">
<li class="nav-menu list-group-item"><a href="#Invoke-M365SecurityAudit">Invoke-M365SecurityAudit</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12">
<div id="Invoke-M365SecurityAudit" class="toggle_container">
<div class="page-header">
<h2> Invoke-M365SecurityAudit </h2>
<p>Invokes a security audit for Microsoft 365 environments.</p>
<p>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.</p>
</div>
<div>
<h3> Syntax </h3>
</div>
<div class="panel panel-default">
<div class='panel-body'>
<pre class="brush: ps">Invoke-M365SecurityAudit -TenantAdminUrl &lt;String&gt; -DomainName &lt;String&gt; [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [&lt;CommonParameters&gt;]
Invoke-M365SecurityAudit -TenantAdminUrl &lt;String&gt; -DomainName &lt;String&gt; [-ELevel &lt;String&gt;] [-ProfileLevel &lt;String&gt;] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [&lt;CommonParameters&gt;]
Invoke-M365SecurityAudit -TenantAdminUrl &lt;String&gt; -DomainName &lt;String&gt; [-IncludeIG1] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [&lt;CommonParameters&gt;]
Invoke-M365SecurityAudit -TenantAdminUrl &lt;String&gt; -DomainName &lt;String&gt; [-IncludeIG2] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [&lt;CommonParameters&gt;]
Invoke-M365SecurityAudit -TenantAdminUrl &lt;String&gt; -DomainName &lt;String&gt; [-IncludeIG3] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [&lt;CommonParameters&gt;]
Invoke-M365SecurityAudit -TenantAdminUrl &lt;String&gt; -DomainName &lt;String&gt; [-IncludeRecommendation &lt;String[]&gt;] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [&lt;CommonParameters&gt;]
Invoke-M365SecurityAudit -TenantAdminUrl &lt;String&gt; -DomainName &lt;String&gt; [-SkipRecommendation &lt;String[]&gt;] [-DoNotConnect] [-DoNotDisconnect] [-NoModuleCheck] [-WhatIf] [-Confirm] [&lt;CommonParameters&gt;]</pre>
</div>
</div>
<div>
<h3> Parameters </h3>
<table class="table table-striped table-bordered table-condensed visible-on">
<thead>
<tr>
<th>Name</th>
<th class="visible-lg visible-md">Alias</th>
<th>Description</th>
<th class="visible-lg visible-md">Required?</th>
<th class="visible-lg">Pipeline Input</th>
<th class="visible-lg">Default Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><nobr>-TenantAdminUrl</nobr></td>
<td class="visible-lg visible-md"></td>
<td>The URL of the tenant admin. This parameter is mandatory.</td>
<td class="visible-lg visible-md">true</td>
<td class="visible-lg">false</td>
<td class="visible-lg"></td>
</tr>
<tr>
<td><nobr>-DomainName</nobr></td>
<td class="visible-lg visible-md"></td>
<td>The domain name of the Microsoft 365 environment. This parameter is mandatory.</td>
<td class="visible-lg visible-md">true</td>
<td class="visible-lg">false</td>
<td class="visible-lg"></td>
</tr>
<tr>
<td><nobr>-ELevel</nobr></td>
<td class="visible-lg visible-md"></td>
<td>Specifies the E-Level (E3 or E5) for the audit. This parameter is optional and can be combined with the ProfileLevel parameter.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg"></td>
</tr>
<tr>
<td><nobr>-ProfileLevel</nobr></td>
<td class="visible-lg visible-md"></td>
<td>Specifies the profile level (L1 or L2) for the audit. This parameter is optional and can be combined with the ELevel parameter.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg"></td>
</tr>
<tr>
<td><nobr>-IncludeIG1</nobr></td>
<td class="visible-lg visible-md"></td>
<td>If specified, includes tests where IG1 is true.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg">False</td>
</tr>
<tr>
<td><nobr>-IncludeIG2</nobr></td>
<td class="visible-lg visible-md"></td>
<td>If specified, includes tests where IG2 is true.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg">False</td>
</tr>
<tr>
<td><nobr>-IncludeIG3</nobr></td>
<td class="visible-lg visible-md"></td>
<td>If specified, includes tests where IG3 is true.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg">False</td>
</tr>
<tr>
<td><nobr>-IncludeRecommendation</nobr></td>
<td class="visible-lg visible-md"></td>
<td>Specifies specific recommendations to include in the audit. Accepts an array of recommendation numbers.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg"></td>
</tr>
<tr>
<td><nobr>-SkipRecommendation</nobr></td>
<td class="visible-lg visible-md"></td>
<td>Specifies specific recommendations to exclude from the audit. Accepts an array of recommendation numbers.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg"></td>
</tr>
<tr>
<td><nobr>-DoNotConnect</nobr></td>
<td class="visible-lg visible-md"></td>
<td>If specified, the cmdlet will not establish a connection to Microsoft 365 services.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg">False</td>
</tr>
<tr>
<td><nobr>-DoNotDisconnect</nobr></td>
<td class="visible-lg visible-md"></td>
<td>If specified, the cmdlet will not disconnect from Microsoft 365 services after execution.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg">False</td>
</tr>
<tr>
<td><nobr>-NoModuleCheck</nobr></td>
<td class="visible-lg visible-md"></td>
<td>If specified, the cmdlet will not check for the presence of required modules.</td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg">False</td>
</tr>
<tr>
<td><nobr>-WhatIf</nobr></td>
<td class="visible-lg visible-md">wi</td>
<td></td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg"></td>
</tr>
<tr>
<td><nobr>-Confirm</nobr></td>
<td class="visible-lg visible-md">cf</td>
<td></td>
<td class="visible-lg visible-md">false</td>
<td class="visible-lg">false</td>
<td class="visible-lg"></td>
</tr>
</tbody>
</table>
</div>
<div>
<h3> Inputs </h3>
<p>The input type is the type of the objects that you can pipe to the cmdlet.</p>
<ul><li>None. You cannot pipe objects to Invoke-M365SecurityAudit.</li></ul>
</div>
<div>
<h3> Outputs </h3>
<p>The output type is the type of the objects that the cmdlet emits.</p>
<ul><li>CISAuditResult[]
The cmdlet returns an array of CISAuditResult objects representing the results of the security audit.</li></ul>
</div>
<div class='panel panel-default'>
<div class='panel-heading'>
<h3 class='panel-title'> Note </h3>
</div>
<div class='panel-body'>This module is based on CIS benchmarks and is governed by the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. For more details, visit:
https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en</div>
</div>
<div>
<h3> Examples </h3>
</div>
<div class='panel panel-default'>
<div class='panel-body'>
<strong>EXAMPLE 1</strong>
<pre class="brush: ps">Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -ELevel "E5" -ProfileLevel "L1"</pre>
<div>Performs a security audit for the E5 level and L1 profile in the specified Microsoft 365 environment.</div>
<strong>EXAMPLE 2</strong>
<pre class="brush: ps">Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -IncludeIG1</pre>
<div>Performs an audit including all tests where IG1 is true.</div>
<strong>EXAMPLE 3</strong>
<pre class="brush: ps">Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -SkipRecommendation '1.1.3', '2.1.1'</pre>
<div>Performs an audit while excluding specific recommendations 1.1.3 and 2.1.1.</div>
<strong>EXAMPLE 4</strong>
<pre class="brush: ps">$auditResults = Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com"
PS&gt; $auditResults | Export-Csv -Path "auditResults.csv" -NoTypeInformation</pre>
<div>Captures the audit results into a variable and exports them to a CSV file.</div>
</div>
</div>
<p class='pull-right'><a onclick='document.body.scrollTop = document.documentElement.scrollTop = 0;' style='cursor: pointer;'>Top of page</a>
<div>
<h3> Links </h3>
<div>
<ul>
<li class='psLink'><a href='#Online Version: [GitHub Repository URL]' target='_top'>Online Version: [GitHub Repository URL]</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js" ></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/scripts/shCore.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/scripts/shBrushPowerShell.min.js" charset="utf-8"></script>
<script>
$(document).ready(function() {
$(".toggle_container").hide();
var previousId;
if(location.hash) {
var id = location.hash.slice(1); //Get rid of the # mark
var elementToShow = $("#" + id); //Save local reference
if(elementToShow.length) { //Check if the element exists
elementToShow.slideToggle('fast'); //Show the element
elementToShow.addClass("check_list_selected"); //Add class to element (the link)
}
previousId = id;
}
$('.nav-menu a, .psLink a').click(function() {
$(".sidebar-navbar-collapse").collapse('hide');
$('.toggle_container').hide(); // Hide all
var elem = $(this).prop("hash");
$(elem).toggle('fast'); // Show HREF/to/ID one
history.pushState({}, '', $(this).attr("href"));
window.scrollTo(0, 0);
return false;
});
SyntaxHighlighter.defaults['toolbar'] = false;
SyntaxHighlighter.defaults['gutter'] = false;
SyntaxHighlighter.all();
$('#searchList').btsListFilter('#searchinput', {itemChild: 'a', initial: false, resetOnBlur: true});
$(document).keyup(function( e ) {
if(!$('#searchinput').is(':focus') && e.which >= 65 && e.which <= 90 ){
$('#searchinput').val(String.fromCharCode(e.keyCode));
$('#searchinput').focus();
}
});
$(document).click(function() {
$('#searchinput').blur();
});
});
</script>
<!-- bootstrap-list-filter.min.js - removed as external resource and added as content -->
<script>
/*
* bootstrap-list-filter v0.1.7 - 2015-03-30
*
* Copyright 2015 Stefano Cudini
* stefano.cudini@gmail.com
* http://labs.easyblog.it/
*
* Licensed under the MIT license.
*
* Demos:
* http://labs.easyblog.it/bootstrap-list-filter/
*
* Source:
* git@github.com:stefanocudini/bootstrap-list-filter.git
*
*/
!function(a){a.fn.btsListFilter=function(b,c){function d(a,b){return a.replace(/\{ *([\w_]+) *\}/g,function(a,c){return b[c]||""})}function e(a,b){var c;return b=b||300,function(){var d=this,e=arguments;clearTimeout(c),c=setTimeout(function(){a.apply(d,Array.prototype.slice.call(e))},b)}}var f,g=this,h=a(this),i=a(b),j=h;return c=a.extend({delay:300,minLength:1,initial:!0,eventKey:"keyup",resetOnBlur:!0,sourceData:null,sourceTmpl:'<a class="list-group-item" href="#"><span>{title}</span></a>',sourceNode:function(a){return d(c.sourceTmpl,a)},emptyNode:function(){return'<a class="list-group-item well" href="#"><span>No Results</span></a>'},itemEl:".list-group-item",itemChild:null,itemFilter:function(b,d){d=d&&d.replace(new RegExp("[({[^.$*+?\\]})]","g"),"");var e=a(b).text(),f=c.initial?"^":"",g=new RegExp(f+d,"i");return g.test(e)}},c),i.on(c.eventKey,e(function(){var b=a(this).val();c.itemEl&&(j=h.find(c.itemEl)),c.itemChild&&(j=j.find(c.itemChild));var d=j.filter(function(){return c.itemFilter.call(g,this,b)}),e=j.not(d);c.itemChild&&(d=d.parents(c.itemEl),e=e.parents(c.itemEl).hide()),""!==b&&b.length>=c.minLength?(d.show(),e.hide(),"function"===a.type(c.sourceData)?(d.hide(),e.hide(),f&&(a.isFunction(f.abort)?f.abort():a.isFunction(f.stop)&&f.stop()),f=c.sourceData.call(g,b,function(b){if(f=null,d.hide(),e.hide(),h.find(".bts-dynamic-item").remove(),b&&0!==b.length)for(var i in b)a(c.sourceNode.call(g,b[i])).addClass("bts-dynamic-item").appendTo(h);else a(c.emptyNode.call(g)).addClass("bts-dynamic-item").appendTo(h)})):0===d.length&&a(c.emptyNode.call(g)).addClass("bts-dynamic-item").appendTo(h)):(d.show(),e.show(),h.find(".bts-dynamic-item").remove())},c.delay)),c.resetOnBlur&&i.on("blur",function(){a(this).val("").trigger(c.eventKey)}),h}}(jQuery);
</script>
</body>
</html>

3
helpers/Build-Help.ps1 Normal file
View File

@@ -0,0 +1,3 @@
Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1
.\helpers\psDoc-master\src\psDoc.ps1 -moduleName M365FoundationsCISReport -outputDir docs -template ".\helpers\psDoc-master\src\out-html-template.ps1"
.\helpers\psDoc-master\src\psDoc.ps1 -moduleName M365FoundationsCISReport -outputDir ".\" -template ".\helpers\psDoc-master\src\out-markdown-template.ps1" -fileName ".\README.md"

View File

@@ -0,0 +1,59 @@
function Test-AntiPhishingPolicy_2.1.7_E5L1_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# Retrieve and validate the anti-phishing policies
$antiPhishPolicies = Get-AntiPhishPolicy
$validatedPolicies = $antiPhishPolicies | Where-Object {
$_.Enabled -eq $true -and
$_.PhishThresholdLevel -ge 2 -and
$_.EnableMailboxIntelligenceProtection -eq $true -and
$_.EnableMailboxIntelligence -eq $true -and
$_.EnableSpoofIntelligence -eq $true
}
# Check if there is at least one policy that meets the requirements
$isCompliant = $validatedPolicies.Count -gt 0
# Prepare failure details if policies are not compliant
$failureDetails = if (-not $isCompliant) {
"No anti-phishing policy is fully compliant with CIS benchmark requirements."
} else {
"Compliant Anti-Phish Policy Names: " + ($validatedPolicies.Name -join ', ')
}
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($isCompliant) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E5"
$auditResult.Profile = "L1"
$auditResult.Rec = "2.1.7"
$auditResult.RecDescription = "Ensure that an anti-phishing policy has been created"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "9.7"
$auditResult.CISDescription = "Deploy and Maintain Email Server Anti-Malware Protections"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $true
$auditResult.Result = $isCompliant
$auditResult.Details = $failureDetails
$auditResult.FailureReason = if (-not $isCompliant) { "Anti-phishing policies do not meet CIS benchmark requirements." } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,43 @@
function Test-AuditDisabledFalse_6.1.1_E3L1_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 6.1.1 (L1) Ensure 'AuditDisabled' organizationally is set to 'False'
# Pass if AuditDisabled is False. Fail otherwise.
$auditDisabledConfig = Get-OrganizationConfig | Select-Object AuditDisabled
$auditNotDisabled = -not $auditDisabledConfig.AuditDisabled
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($auditNotDisabled) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "6.1.1"
$auditResult.RecDescription = "Ensure 'AuditDisabled' organizationally is set to 'False'"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "8.2"
$auditResult.CISDescription = "Collect Audit Logs"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $auditNotDisabled
$auditResult.Details = if ($auditNotDisabled) { "Audit is not disabled organizationally" } else { "Audit is disabled organizationally" }
$auditResult.FailureReason = if (-not $auditNotDisabled) { "AuditDisabled is set to True" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,43 @@
function Test-AuditLogSearch_3.1.1_E3L1_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 3.1.1 (L1) Ensure Microsoft 365 audit log search is Enabled
# Pass if UnifiedAuditLogIngestionEnabled is True. Fail otherwise.
$auditLogConfig = Get-AdminAuditLogConfig | Select-Object UnifiedAuditLogIngestionEnabled
$auditLogResult = $auditLogConfig.UnifiedAuditLogIngestionEnabled
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($auditLogResult) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "3.1.1"
$auditResult.RecDescription = "Ensure Microsoft 365 audit log search is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "8.2"
$auditResult.CISDescription = "Collect Audit Logs"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $auditLogResult
$auditResult.Details = "UnifiedAuditLogIngestionEnabled: $($auditLogConfig.UnifiedAuditLogIngestionEnabled)"
$auditResult.FailureReason = if (-not $auditLogResult) { "Audit log search is not enabled" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,44 @@
function Test-BlockChannelEmails_8.1.2_E3L1 {
[CmdletBinding()]
param (
# Parameters can be added here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.1.2 (L1) Ensure users can't send emails to a channel email address
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$teamsClientConfig = Get-CsTeamsClientConfiguration -Identity Global
$allowEmailIntoChannel = $teamsClientConfig.AllowEmailIntoChannel
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # This control is Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.1.2"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the benchmark
$auditResult.IG2 = $false # Set based on the benchmark
$auditResult.IG3 = $false # Set based on the benchmark
$auditResult.RecDescription = "Ensure users can't send emails to a channel email address"
$auditResult.Result = -not $allowEmailIntoChannel
$auditResult.Details = "AllowEmailIntoChannel is set to $allowEmailIntoChannel"
$auditResult.FailureReason = if ($allowEmailIntoChannel) { "Emails can be sent to a channel email address" } else { "N/A" }
$auditResult.Status = if (-not $allowEmailIntoChannel) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,51 @@
function Test-BlockMailForwarding_6.2.1_E3L1 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
$auditResult.Rec = "6.2.1"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure all forms of mail forwarding are blocked and/or disabled"
}
process {
# Verify that no rules are forwarding the email to external domains
$transportRules = Get-TransportRule | Where-Object { $_.RedirectMessageTo -ne $null }
$forwardingBlocked = $transportRules.Count -eq 0
$auditResult.Result = $forwardingBlocked
$auditResult.Details = if ($transportRules.Count -gt 0) {
$transportRules | ForEach-Object {
"$($_.Name) redirects to $($_.RedirectMessageTo)"
} -join " | "
} else {
"Step 1: No forwarding rules found. Please proceed with Step 2 described in CIS Benchmark."
}
$auditResult.FailureReason = if (-not $forwardingBlocked) {
"Mail forwarding rules found: $($transportRules.Name -join ', ')"
} else {
"N/A"
}
$auditResult.Status = if ($forwardingBlocked) { "Pass" } else { "Fail" }
}
end {
# Return the result object
return $auditResult
}
}

View File

@@ -0,0 +1,47 @@
function Test-BlockSharedMailboxSignIn_1.2.2_E3L1 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 1.2.2 (L1) Ensure sign-in to shared mailboxes is blocked
# Pass if all shared mailboxes have AccountEnabled set to False.
# Fail if any shared mailbox has AccountEnabled set to True.
$MBX = Get-EXOMailbox -RecipientTypeDetails SharedMailbox
$sharedMailboxDetails = $MBX | ForEach-Object { Get-AzureADUser -ObjectId $_.ExternalDirectoryObjectId }
$enabledMailboxes = $sharedMailboxDetails | Where-Object { $_.AccountEnabled } | ForEach-Object { $_.DisplayName }
$allBlocked = $enabledMailboxes.Count -eq 0
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Control is explicitly not mapped
$auditResult.CISDescription = "Control not mapped to CIS Controls v7 or v8"
$auditResult.Rec = "1.2.2"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Control is not mapped, hence IG1 is false
$auditResult.IG2 = $false # Control is not mapped, hence IG2 is false
$auditResult.IG3 = $false # Control is not mapped, hence IG3 is false
$auditResult.RecDescription = "Ensure sign-in to shared mailboxes is blocked"
$auditResult.Result = $allBlocked
$auditResult.Details = "Enabled Mailboxes: $($enabledMailboxes -join ', ')"
$auditResult.FailureReason = if ($allBlocked) { "N/A" } else { "Some mailboxes have sign-in enabled: $($enabledMailboxes -join ', ')" }
$auditResult.Status = if ($allBlocked) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,46 @@
function Test-CommonAttachmentFilter_2.1.2_E3L1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 2.1.2 (L1) Ensure the Common Attachment Types Filter is enabled
# Pass if EnableFileFilter is set to True. Fail otherwise.
$attachmentFilter = Get-MalwareFilterPolicy -Identity Default | Select-Object EnableFileFilter
$result = $attachmentFilter.EnableFileFilter
$details = "File Filter Enabled: $($attachmentFilter.EnableFileFilter)"
$failureReason = if ($result) { "N/A" } else { "Common Attachment Types Filter is disabled" }
$status = if ($result) { "Pass" } else { "Fail" }
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = $status
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "2.1.2"
$auditResult.RecDescription = "Ensure the Common Attachment Types Filter is enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "9.6"
$auditResult.CISDescription = "Block Unnecessary File Types"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $result
$auditResult.Details = $details
$auditResult.FailureReason = $failureReason
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,40 @@
function Test-CustomerLockbox_1.3.6_E5L2 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 1.3.6 (L2) Ensure the customer lockbox feature is enabled
$orgConfig = Get-OrganizationConfig | Select-Object CustomerLockBoxEnabled
$customerLockboxEnabled = $orgConfig.CustomerLockBoxEnabled
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($customerLockboxEnabled) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E5"
$auditResult.Profile = "L2"
$auditResult.Rec = "1.3.6"
$auditResult.RecDescription = "Ensure the customer lockbox feature is enabled"
$auditResult.CISControlVer = 'v8'
$auditResult.CISControl = "0.0" # As per the snapshot provided, this is explicitly not mapped
$auditResult.CISDescription = "Control not mapped to CIS Controls v7 or v8"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.Result = $customerLockboxEnabled
$auditResult.Details = "Customer Lockbox Enabled: $customerLockboxEnabled"
$auditResult.FailureReason = if ($customerLockboxEnabled) { "N/A" } else { "Customer lockbox feature is not enabled." }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,45 @@
function Test-DialInBypassLobby_8.5.4_E3L1 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.5.4 (L1) Ensure users dialing in can't bypass the lobby
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$CsTeamsMeetingPolicyPSTN = Get-CsTeamsMeetingPolicy -Identity Global | Select-Object -Property AllowPSTNUsersToBypassLobby
$PSTNBypassDisabled = -not $CsTeamsMeetingPolicyPSTN.AllowPSTNUsersToBypassLobby
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.5.4"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure users dialing in can't bypass the lobby"
$auditResult.Result = $PSTNBypassDisabled
$auditResult.Details = "AllowPSTNUsersToBypassLobby is set to $($CsTeamsMeetingPolicyPSTN.AllowPSTNUsersToBypassLobby)"
$auditResult.FailureReason = if ($PSTNBypassDisabled) { "N/A" } else { "Users dialing in can bypass the lobby" }
$auditResult.Status = if ($PSTNBypassDisabled) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,41 @@
function Test-DisallowInfectedFilesDownload_7.3.1_E5L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.3.1 (L2) Ensure Office 365 SharePoint infected files are disallowed for download
$SPOTenantDisallowInfectedFileDownload = Get-SPOTenant | Select-Object DisallowInfectedFileDownload
$isDisallowInfectedFileDownloadEnabled = $SPOTenantDisallowInfectedFileDownload.DisallowInfectedFileDownload
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "10.1"
$auditResult.CISDescription = "Deploy and Maintain Anti-Malware Software"
$auditResult.Rec = "7.3.1"
$auditResult.ELevel = "E5"
$auditResult.Profile = "L2"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure Office 365 SharePoint infected files are disallowed for download"
$auditResult.Result = $isDisallowInfectedFileDownloadEnabled
$auditResult.Details = "DisallowInfectedFileDownload: $($SPOTenantDisallowInfectedFileDownload.DisallowInfectedFileDownload)"
$auditResult.FailureReason = if (-not $isDisallowInfectedFileDownloadEnabled) { "Downloading infected files is not disallowed." } else { "N/A" }
$auditResult.Status = if ($isDisallowInfectedFileDownloadEnabled) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,44 @@
function Test-EnableDKIM_2.1.9_E3L1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 2.1.9 (L1) Ensure DKIM is enabled for all Exchange Online Domains
# Pass if Enabled is True for all domains. Fail if any domain has Enabled set to False.
$dkimConfig = Get-DkimSigningConfig | Select-Object Domain, Enabled
$dkimResult = ($dkimConfig | ForEach-Object { $_.Enabled }) -notcontains $false
$dkimFailedDomains = $dkimConfig | Where-Object { -not $_.Enabled } | ForEach-Object { $_.Domain }
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($dkimResult) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "2.1.9"
$auditResult.RecDescription = "Ensure that DKIM is enabled for all Exchange Online Domains"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "9.5"
$auditResult.CISDescription = "Implement DMARC"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $dkimResult
$auditResult.Details = if (-not $dkimResult) { "DKIM not enabled for: $($dkimFailedDomains -join ', ')" } else { "All domains have DKIM enabled" }
$auditResult.FailureReason = if (-not $dkimResult) { "DKIM is not enabled for some domains" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,45 @@
function Test-ExternalNoControl_8.5.7_E3L1 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.5.7 (L1) Ensure external participants can't give or request control
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$CsTeamsMeetingPolicyControl = Get-CsTeamsMeetingPolicy -Identity Global | Select-Object -Property AllowExternalParticipantGiveRequestControl
$externalControlRestricted = -not $CsTeamsMeetingPolicyControl.AllowExternalParticipantGiveRequestControl
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.5.7"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure external participants can't give or request control"
$auditResult.Result = $externalControlRestricted
$auditResult.Details = "AllowExternalParticipantGiveRequestControl is set to $($CsTeamsMeetingPolicyControl.AllowExternalParticipantGiveRequestControl)"
$auditResult.FailureReason = if ($externalControlRestricted) { "N/A" } else { "External participants can give or request control" }
$auditResult.Status = if ($externalControlRestricted) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,53 @@
function Test-ExternalSharingCalendars_1.3.3_E3L2_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 1.3.3 (L2) Ensure 'External sharing' of calendars is not available (Automated)
$sharingPolicies = Get-SharingPolicy | Where-Object { $_.Domains -like '*CalendarSharing*' }
# Check if calendar sharing is disabled in all applicable policies
$isExternalSharingDisabled = $true
$sharingPolicyDetails = @()
foreach ($policy in $sharingPolicies) {
if ($policy.Enabled -eq $true) {
$isExternalSharingDisabled = $false
$sharingPolicyDetails += "$($policy.Name): Enabled"
}
}
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Rec = "1.3.3"
$auditResult.RecDescription = "Ensure 'External sharing' of calendars is not available"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
# The following IG values are placeholders. Replace with actual values when known.
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.CISControlVer = "v8"
# Placeholder for CIS Control, to be replaced with the actual value when available
$auditResult.CISControl = "4.8"
$auditResult.CISDescription = "Uninstall or Disable Unnecessary Services on Enterprise Assets and Software"
$auditResult.Result = $isExternalSharingDisabled
$auditResult.Details = if ($isExternalSharingDisabled) { "Calendar sharing with external users is disabled." } else { "Enabled Sharing Policies: $($sharingPolicyDetails -join ', ')" }
$auditResult.FailureReason = if ($isExternalSharingDisabled) { "N/A" } else { "Calendar sharing with external users is enabled in one or more policies." }
$auditResult.Status = if ($isExternalSharingDisabled) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,46 @@
function Test-GlobalAdminsCount_1.1.3_E3L1_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 1.1.3 (L1) Ensure that between two and four global admins are designated
# Pass if the count of global admins is between 2 and 4. Fail otherwise.
$globalAdminRole = Get-MgDirectoryRole -Filter "RoleTemplateId eq '62e90394-69f5-4237-9190-012177145e10'"
$globalAdmins = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id
$globalAdminCount = $globalAdmins.AdditionalProperties.Count
$globalAdminUsernames = ($globalAdmins | ForEach-Object { $_.AdditionalProperties["displayName"] }) -join ', '
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "5.1"
$auditResult.CISDescription = "Establish and Maintain an Inventory of Accounts"
$auditResult.Rec = "1.1.3"
$auditResult.ELevel = "E3" # Based on your environment (E3, E5, etc.)
$auditResult.Profile = "L1"
$auditResult.IG1 = $true # Set based on the benchmark
$auditResult.IG2 = $true # Set based on the benchmark
$auditResult.IG3 = $true # Set based on the benchmark
$auditResult.RecDescription = "Ensure that between two and four global admins are designated"
$auditResult.Result = $globalAdminCount -ge 2 -and $globalAdminCount -le 4
$auditResult.Details = "Count: $globalAdminCount; Users: $globalAdminUsernames"
$auditResult.FailureReason = if ($globalAdminCount -lt 2) { "Less than 2 global admins: $globalAdminUsernames" } elseif ($globalAdminCount -gt 4) { "More than 4 global admins: $globalAdminUsernames" } else { "N/A" }
$auditResult.Status = if ($globalAdminCount -ge 2 -and $globalAdminCount -le 4) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,41 @@
function Test-GuestAccessExpiration_7.2.9_E3L1 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.9 (L1) Ensure guest access to a site or OneDrive will expire automatically
$SPOTenantGuestAccess = Get-SPOTenant | Select-Object ExternalUserExpirationRequired, ExternalUserExpireInDays
$isGuestAccessExpirationConfiguredCorrectly = $SPOTenantGuestAccess.ExternalUserExpirationRequired -and $SPOTenantGuestAccess.ExternalUserExpireInDays -le 30
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "7.2.9"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure guest access to a site or OneDrive will expire automatically"
$auditResult.Result = $isGuestAccessExpirationConfiguredCorrectly
$auditResult.Details = "ExternalUserExpirationRequired: $($SPOTenantGuestAccess.ExternalUserExpirationRequired); ExternalUserExpireInDays: $($SPOTenantGuestAccess.ExternalUserExpireInDays)"
$auditResult.FailureReason = if (-not $isGuestAccessExpirationConfiguredCorrectly) { "Guest access expiration is not configured to automatically expire within 30 days or less." } else { "N/A" }
$auditResult.Status = if ($isGuestAccessExpirationConfiguredCorrectly) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,44 @@
function Test-IdentifyExternalEmail_6.2.3_E3L1 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 6.2.3 (L1) Ensure email from external senders is identified
# Requirement is to have external sender tagging enabled
$externalInOutlook = Get-ExternalInOutlook
$externalTaggingEnabled = ($externalInOutlook | ForEach-Object { $_.Enabled }) -contains $true
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($externalTaggingEnabled) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "6.2.3"
$auditResult.RecDescription = "Ensure email from external senders is identified"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.Result = $externalTaggingEnabled
$auditResult.Details = "Enabled: $($externalTaggingEnabled); AllowList: $($externalInOutlook.AllowList)"
$auditResult.FailureReason = if (-not $externalTaggingEnabled) { "External sender tagging is disabled" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,42 @@
function Test-LinkSharingRestrictions_7.2.7_E3L1_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
# Test behavior in prod
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.7 (L1) Ensure link sharing is restricted in SharePoint and OneDrive
$SPOTenantLinkSharing = Get-SPOTenant | Select-Object DefaultSharingLinkType
$isLinkSharingRestricted = $SPOTenantLinkSharing.DefaultSharingLinkType -eq 'Direct' # Or 'SpecificPeople' as per the recommendation
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "7.2.7"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure link sharing is restricted in SharePoint and OneDrive"
$auditResult.Result = $isLinkSharingRestricted
$auditResult.Details = "DefaultSharingLinkType: $($SPOTenantLinkSharing.DefaultSharingLinkType)"
$auditResult.FailureReason = if (-not $isLinkSharingRestricted) { "Link sharing is not restricted to 'Specific people'. Current setting: $($SPOTenantLinkSharing.DefaultSharingLinkType)" } else { "N/A" }
$auditResult.Status = if ($isLinkSharingRestricted) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,43 @@
function Test-MailTipsEnabled_6.5.2_E3L2 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 6.5.2 (L2) Ensure MailTips are enabled for end users
$orgConfig = Get-OrganizationConfig | Select-Object MailTipsAllTipsEnabled, MailTipsExternalRecipientsTipsEnabled, MailTipsGroupMetricsEnabled, MailTipsLargeAudienceThreshold
$allTipsEnabled = $orgConfig.MailTipsAllTipsEnabled -and $orgConfig.MailTipsGroupMetricsEnabled -and $orgConfig.MailTipsLargeAudienceThreshold -eq 25
$externalRecipientsTipsEnabled = $orgConfig.MailTipsExternalRecipientsTipsEnabled
# Since there is no direct CIS Control mapping, the control will be set as not applicable.
$auditResult.CISControl = "N/A"
$auditResult.CISControlVer = "v8"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "6.5.2"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure MailTips are enabled for end users"
$auditResult.Result = $allTipsEnabled -and $externalRecipientsTipsEnabled
$auditResult.Details = "MailTipsAllTipsEnabled: $($orgConfig.MailTipsAllTipsEnabled); MailTipsExternalRecipientsTipsEnabled: $($orgConfig.MailTipsExternalRecipientsTipsEnabled); MailTipsGroupMetricsEnabled: $($orgConfig.MailTipsGroupMetricsEnabled); MailTipsLargeAudienceThreshold: $($orgConfig.MailTipsLargeAudienceThreshold)"
$auditResult.FailureReason = if (-not $auditResult.Result) { "One or more MailTips settings are not configured as required." } else { "N/A" }
$auditResult.Status = if ($auditResult.Result) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,111 @@
function Test-MailboxAuditingE3_6.1.2_E3L1_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$e3SkuPartNumbers = @("ENTERPRISEPACK", "OFFICESUBSCRIPTION")
$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")
$auditResult = [CISAuditResult]::new()
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "6.1.2"
$auditResult.RecDescription = "Ensure mailbox auditing for Office E3 users is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "8.2"
$auditResult.CISDescription = "Collect audit logs."
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$allFailures = @()
$allUsers = Get-AzureADUser -All $true
$processedUsers = @{} # Dictionary to track processed users
}
process {
foreach ($user in $allUsers) {
if ($processedUsers.ContainsKey($user.UserPrincipalName)) {
Write-Verbose "Skipping already processed user: $($user.UserPrincipalName)"
continue
}
try {
$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."
if ($hasOfficeE3) {
$userUPN = $user.UserPrincipalName
$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit
$missingActions = @()
if ($mailbox.AuditEnabled) {
foreach ($action in $AdminActions) {
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" }
}
foreach ($action in $DelegateActions) {
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" }
}
foreach ($action in $OwnerActions) {
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" }
}
}
else {
$allFailures += "$userUPN`: AuditEnabled - False"
continue
}
if ($missingActions) {
$formattedActions = Format-MissingActions $missingActions
$allFailures += "$userUPN`: AuditEnabled - True; $formattedActions"
}
# Mark the user as processed
$processedUsers[$user.UserPrincipalName] = $true
}
}
catch {
Write-Warning "Could not retrieve license details for user $($user.UserPrincipalName): $_"
}
}
$auditResult.Result = $allFailures.Count -eq 0
$auditResult.Status = if ($auditResult.Result) { "Pass" } else { "Fail" }
$auditResult.Details = if ($auditResult.Result) { "All Office E3 users have correct mailbox audit settings." } else { $allFailures -join " | " }
$auditResult.FailureReason = if (-not $auditResult.Result) { "Audit issues detected." } else { "N/A" }
}
end {
return $auditResult
}
}
function Format-MissingActions {
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 = @()
foreach ($type in $actionGroups.Keys) {
if ($actionGroups[$type].Count -gt 0) {
$formattedResults += "$($type) actions missing: $($actionGroups[$type] -join ', ')"
}
}
return $formattedResults -join '; '
}

View File

@@ -0,0 +1,120 @@
function Test-MailboxAuditingE5_6.1.3_E5L1_IG1_IG2_IG3 {
[CmdletBinding()]
param ()
begin {
. ".\source\Classes\CISAuditResult.ps1"
$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")
$auditResult = [CISAuditResult]::new()
$auditResult.ELevel = "E5"
$auditResult.Profile = "L1"
$auditResult.Rec = "6.1.3"
$auditResult.RecDescription = "Ensure mailbox auditing for Office E5 users is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "8.2"
$auditResult.CISDescription = "Collect audit logs."
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$allFailures = @()
$allUsers = Get-AzureADUser -All $true
$processedUsers = @{} # Dictionary to track processed users
}
process {
foreach ($user in $allUsers) {
if ($processedUsers.ContainsKey($user.UserPrincipalName)) {
continue
}
try {
# Define SKU Part Numbers for Office E5 licenses
# Define SKU Part Numbers for Office E5 licenses
$e5SkuPartNumbers = @("SPE_E5", "ENTERPRISEPREMIUM", "OFFICEE5")
$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."
if ($hasOfficeE5) {
$userUPN = $user.UserPrincipalName
$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit
$missingActions = @()
if ($mailbox.AuditEnabled) {
foreach ($action in $AdminActions) {
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" }
}
foreach ($action in $DelegateActions) {
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" }
}
foreach ($action in $OwnerActions) {
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" }
}
}
else {
$allFailures += "$userUPN`: AuditEnabled - False"
continue
}
if ($missingActions) {
$formattedActions = Format-MissingActions $missingActions
$allFailures += "$userUPN`: AuditEnabled - True; $formattedActions"
}
else {
Write-Verbose "User $($user.UserPrincipalName) passed the mailbox audit checks."
}
$processedUsers[$user.UserPrincipalName] = $true
}
else {
# Adding verbose output to indicate the user does not have an E5 license
Write-Verbose "User $($user.UserPrincipalName) does not have an Office E5 license."
}
}
catch {
Write-Warning "Could not retrieve license details for user $($user.UserPrincipalName): $_"
}
}
if ($allFailures.Count -eq 0) {
Write-Verbose "All evaluated E5 users have correct mailbox audit settings."
}
$auditResult.Result = $allFailures.Count -eq 0
$auditResult.Status = if ($auditResult.Result) { "Pass" } else { "Fail" }
$auditResult.Details = if ($auditResult.Result) { "All Office E5 users have correct mailbox audit settings." } else { $allFailures -join " | " }
$auditResult.FailureReason = if (-not $auditResult.Result) { "Audit issues detected." } else { "N/A" }
}
end {
return $auditResult
}
}
function Format-MissingActions {
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 = @()
foreach ($type in $actionGroups.Keys) {
if ($actionGroups[$type].Count -gt 0) {
$formattedResults += "$($type) actions missing: $($actionGroups[$type] -join ', ')"
}
}
return $formattedResults -join '; '
}

View File

@@ -0,0 +1,54 @@
function Test-ManagedApprovedPublicGroups_1.2.1_E3L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 1.2.1 (L2) Ensure that only organizationally managed/approved public groups exist (Automated)
$allGroups = Get-MgGroup -All | Where-Object { $_.Visibility -eq "Public" } | Select-Object DisplayName, Visibility
# Check if there are public groups and if they are organizationally managed/approved
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "1.2.1"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true # Based on the provided CIS Control image, IG3 is not applicable
$auditResult.RecDescription = "Ensure that only organizationally managed/approved public groups exist"
if ($null -eq $allGroups -or $allGroups.Count -eq 0) {
$auditResult.Result = $true
$auditResult.Details = "No public groups found."
$auditResult.FailureReason = "N/A"
$auditResult.Status = "Pass"
}
else {
$groupDetails = $allGroups | ForEach-Object { $_.DisplayName + " (" + $_.Visibility + ")" }
$detailsString = $groupDetails -join ', '
$auditResult.Result = $false
$auditResult.Details = "Public groups found: $detailsString"
$auditResult.FailureReason = "There are public groups present that are not organizationally managed/approved."
$auditResult.Status = "Fail"
}
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,46 @@
function Test-MeetingChatNoAnonymous_8.5.5_E3L1 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.5.5 (L2) Ensure meeting chat does not allow anonymous users
# Name doesn't match profile level in benchmarks either.
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$CsTeamsMeetingPolicyChat = Get-CsTeamsMeetingPolicy -Identity Global | Select-Object -Property MeetingChatEnabledType
$chatAnonDisabled = $CsTeamsMeetingPolicyChat.MeetingChatEnabledType -eq 'EnabledExceptAnonymous'
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.5.5"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure meeting chat does not allow anonymous users"
$auditResult.Result = $chatAnonDisabled
$auditResult.Details = "MeetingChatEnabledType is set to $($CsTeamsMeetingPolicyChat.MeetingChatEnabledType)"
$auditResult.FailureReason = if ($chatAnonDisabled) { "N/A" } else { "Meeting chat allows anonymous users" }
$auditResult.Status = if ($chatAnonDisabled) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,54 @@
function Test-ModernAuthExchangeOnline_6.5.1_E3L1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$CISAuditResult = [CISAuditResult]::new()
# Initialization code
}
process {
try {
# Ensuring the ExchangeOnlineManagement module is available
# 6.5.1 (L1) Ensure modern authentication for Exchange Online is enabled
$orgConfig = Get-OrganizationConfig | Select-Object -Property Name, OAuth2ClientProfileEnabled
# Create an instance of CISAuditResult and populate it
$CISAuditResult.CISControlVer = "v8"
$CISAuditResult.CISControl = "3.10"
$CISAuditResult.CISDescription = "Encrypt Sensitive Data in Transit"
$CISAuditResult.IG1 = $false # As per CIS Control v8 mapping for IG1
$CISAuditResult.IG2 = $true # As per CIS Control v8 mapping for IG2
$CISAuditResult.IG3 = $true # As per CIS Control v8 mapping for IG3
$CISAuditResult.ELevel = "E3" # Based on your environment (E3, E5, etc.)
$CISAuditResult.Profile = "L1"
$CISAuditResult.Rec = "6.5.1"
$CISAuditResult.RecDescription = "Ensure modern authentication for Exchange Online is enabled (Automated)"
$CISAuditResult.Result = $orgConfig.OAuth2ClientProfileEnabled
$CISAuditResult.Details = $orgConfig | Out-String
$CISAuditResult.FailureReason = if (-not $orgConfig.OAuth2ClientProfileEnabled) { "Modern authentication is disabled" } else { "N/A" }
$CISAuditResult.Status = if ($orgConfig.OAuth2ClientProfileEnabled) { "Pass" } else { "Fail" }
}
catch {
Write-Error "An error occurred while testing modern authentication for Exchange Online: $_"
}
}
end {
# Return auditResults
return $CISAuditResult
}
}

View File

@@ -0,0 +1,39 @@
function Test-ModernAuthSharePoint_7.2.1_E3L1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.1 (L1) Ensure modern authentication for SharePoint applications is required
$SPOTenant = Get-SPOTenant | Select-Object -Property LegacyAuthProtocolsEnabled
$modernAuthForSPRequired = -not $SPOTenant.LegacyAuthProtocolsEnabled
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.10"
$auditResult.CISDescription = "Encrypt Sensitive Data in Transit"
$auditResult.Rec = "7.2.1"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Modern Authentication for SharePoint Applications"
$auditResult.Result = $modernAuthForSPRequired
$auditResult.Details = "LegacyAuthProtocolsEnabled: $($SPOTenant.LegacyAuthProtocolsEnabled)"
$auditResult.FailureReason = if (-not $modernAuthForSPRequired) { "Legacy authentication protocols are enabled" } else { "N/A" }
$auditResult.Status = if ($modernAuthForSPRequired) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,45 @@
function Test-NoAnonymousMeetingJoin_8.5.1_E3L2 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.5.1 (L2) Ensure anonymous users can't join a meeting
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$teamsMeetingPolicy = Get-CsTeamsMeetingPolicy -Identity Global
$allowAnonymousUsersToJoinMeeting = $teamsMeetingPolicy.AllowAnonymousUsersToJoinMeeting
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # The control is Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.5.1"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure anonymous users can't join a meeting"
$auditResult.Result = -not $allowAnonymousUsersToJoinMeeting
$auditResult.Details = "AllowAnonymousUsersToJoinMeeting is set to $allowAnonymousUsersToJoinMeeting"
$auditResult.FailureReason = if ($allowAnonymousUsersToJoinMeeting) { "Anonymous users are allowed to join meetings" } else { "N/A" }
$auditResult.Status = if (-not $allowAnonymousUsersToJoinMeeting) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,45 @@
function Test-NoAnonymousMeetingStart_8.5.2_E3L1 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.5.2 (L1) Ensure anonymous users and dial-in callers can't start a meeting
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$CsTeamsMeetingPolicyAnonymous = Get-CsTeamsMeetingPolicy -Identity Global | Select-Object -Property AllowAnonymousUsersToStartMeeting
$anonymousStartDisabled = -not $CsTeamsMeetingPolicyAnonymous.AllowAnonymousUsersToStartMeeting
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.5.2"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure anonymous users and dial-in callers can't start a meeting"
$auditResult.Result = $anonymousStartDisabled
$auditResult.Details = "AllowAnonymousUsersToStartMeeting is set to $($CsTeamsMeetingPolicyAnonymous.AllowAnonymousUsersToStartMeeting)"
$auditResult.FailureReason = if ($anonymousStartDisabled) { "N/A" } else { "Anonymous users and dial-in callers can start a meeting" }
$auditResult.Status = if ($anonymousStartDisabled) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,47 @@
function Test-NoWhitelistDomains_6.2.2_E3L1 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 6.2.2 (L1) Ensure mail transport rules do not whitelist specific domains
$whitelistedRules = Get-TransportRule | Where-Object { $_.SetSCL -eq -1 -and $_.SenderDomainIs -ne $null }
$auditResult.CISControl = "0.0"
$auditResult.CISControlVer = "v8"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "6.2.2"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure mail transport rules do not whitelist specific domains"
if ($whitelistedRules) {
$ruleDetails = $whitelistedRules | ForEach-Object { "{0}: {1}" -f $_.Name, ($_.SenderDomainIs -join ', ') }
$auditResult.Result = $false
$auditResult.Details = "Whitelisted Rules: $($ruleDetails -join '; ')"
$auditResult.FailureReason = "There are transport rules whitelisting specific domains."
$auditResult.Status = "Fail"
} else {
$auditResult.Result = $true
$auditResult.Details = "No transport rules whitelisting specific domains found."
$auditResult.FailureReason = "N/A"
$auditResult.Status = "Pass"
}
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,52 @@
function Test-NotifyMalwareInternal_2.1.3_E3L1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# Retrieve all 'Custom' malware filter policies and check notification settings
$malwareNotifications = Get-MalwareFilterPolicy | Where-Object { $_.RecommendedPolicyType -eq 'Custom' }
$policiesToReport = @()
foreach ($policy in $malwareNotifications) {
if ($policy.EnableInternalSenderAdminNotifications -ne $true) {
$policiesToReport += "$($policy.Identity): Notifications Disabled"
}
}
# Determine the result based on the presence of custom policies without notifications
$result = $policiesToReport.Count -eq 0
$details = if ($result) { "All custom malware policies have notifications enabled." } else { "Misconfigured Policies: $($policiesToReport -join ', ')" }
$failureReason = if ($result) { "N/A" } else { "Some custom policies do not have notifications for internal users sending malware enabled." }
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($result) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "2.1.3"
$auditResult.RecDescription = "Ensure notifications for internal users sending malware is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "17.5"
$auditResult.CISDescription = "Assign Key Roles and Responsibilities"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $result
$auditResult.Details = $details
$auditResult.FailureReason = $failureReason
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,41 @@
function Test-OneDriveContentRestrictions_7.2.4_E3L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.4 (L2) Ensure OneDrive content sharing is restricted
$SPOTenant = Get-SPOTenant | Select-Object OneDriveSharingCapability
$isOneDriveSharingRestricted = $SPOTenant.OneDriveSharingCapability -eq 'Disabled'
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "7.2.4"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure OneDrive content sharing is restricted"
$auditResult.Result = $isOneDriveSharingRestricted
$auditResult.Details = "OneDriveSharingCapability: $($SPOTenant.OneDriveSharingCapability)"
$auditResult.FailureReason = if (-not $isOneDriveSharingRestricted) { "OneDrive content sharing is not restricted to 'Disabled'. Current setting: $($SPOTenant.OneDriveSharingCapability)" } else { "N/A" }
$auditResult.Status = if ($isOneDriveSharingRestricted) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,41 @@
function Test-OneDriveSyncRestrictions_7.3.2_E3L2 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.3.2 (L2) Ensure OneDrive sync is restricted for unmanaged devices
$SPOTenantSyncClientRestriction = Get-SPOTenantSyncClientRestriction | Select-Object TenantRestrictionEnabled, AllowedDomainList
$isSyncRestricted = $SPOTenantSyncClientRestriction.TenantRestrictionEnabled -and $SPOTenantSyncClientRestriction.AllowedDomainList
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "7.3.2"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure OneDrive sync is restricted for unmanaged devices"
$auditResult.Result = $isSyncRestricted
$auditResult.Details = "TenantRestrictionEnabled: $($SPOTenantSyncClientRestriction.TenantRestrictionEnabled); AllowedDomainList: $($SPOTenantSyncClientRestriction.AllowedDomainList -join ', ')"
$auditResult.FailureReason = if (-not $isSyncRestricted) { "OneDrive sync is not restricted to managed devices. TenantRestrictionEnabled should be True and AllowedDomainList should contain trusted domains GUIDs." } else { "N/A" }
$auditResult.Status = if ($isSyncRestricted) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,45 @@
function Test-OrgOnlyBypassLobby_8.5.3_E3L1_IG3 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.5.3 (L1) Ensure only people in my org can bypass the lobby
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$CsTeamsMeetingPolicyLobby = Get-CsTeamsMeetingPolicy -Identity Global | Select-Object -Property AutoAdmittedUsers
$lobbyBypassRestricted = $CsTeamsMeetingPolicyLobby.AutoAdmittedUsers -eq 'EveryoneInCompanyExcludingGuests'
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "6.8"
$auditResult.CISDescription = "Define and Maintain Role-Based Access Control"
$auditResult.Rec = "8.5.3"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $true # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure only people in my org can bypass the lobby"
$auditResult.Result = $lobbyBypassRestricted
$auditResult.Details = "AutoAdmittedUsers is set to $($CsTeamsMeetingPolicyLobby.AutoAdmittedUsers)"
$auditResult.FailureReason = if ($lobbyBypassRestricted) { "N/A" } else { "External participants can bypass the lobby" }
$auditResult.Status = if ($lobbyBypassRestricted) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,45 @@
function Test-OrganizersPresent_8.5.6_E3L1 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.5.6 (L2) Ensure only organizers and co-organizers can present
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$CsTeamsMeetingPolicyPresenters = Get-CsTeamsMeetingPolicy -Identity Global | Select-Object -Property DesignatedPresenterRoleMode
$presenterRoleRestricted = $CsTeamsMeetingPolicyPresenters.DesignatedPresenterRoleMode -eq 'OrganizerOnlyUserOverride'
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.5.6"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure only organizers and co-organizers can present"
$auditResult.Result = $presenterRoleRestricted
$auditResult.Details = "DesignatedPresenterRoleMode is set to $($CsTeamsMeetingPolicyPresenters.DesignatedPresenterRoleMode)"
$auditResult.FailureReason = if ($presenterRoleRestricted) { "N/A" } else { "Others besides organizers and co-organizers can present" }
$auditResult.Status = if ($presenterRoleRestricted) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,43 @@
function Test-PasswordHashSync_5.1.8.1_E3L1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 5.1.8.1 (L1) Ensure password hash sync is enabled for hybrid deployments
# Pass if OnPremisesSyncEnabled is True. Fail otherwise.
$passwordHashSync = Get-MgOrganization | Select-Object OnPremisesSyncEnabled
$hashSyncResult = $passwordHashSync.OnPremisesSyncEnabled
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($hashSyncResult) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "5.1.8.1"
$auditResult.RecDescription = "Ensure password hash sync is enabled for hybrid deployments"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "6.7"
$auditResult.CISDescription = "Centralize Access Control"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $hashSyncResult
$auditResult.Details = "OnPremisesSyncEnabled: $($passwordHashSync.OnPremisesSyncEnabled)"
$auditResult.FailureReason = if (-not $hashSyncResult) { "Password hash sync for hybrid deployments is not enabled" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,45 @@
function Test-PasswordNeverExpirePolicy_1.3.1_E3L1_IG1_IG2_IG3 {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$DomainName # DomainName parameter is now mandatory
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 1.3.1 (L1) Ensure the 'Password expiration policy' is set to 'Set passwords to never expire'
# Pass if PasswordValidityPeriodInDays is 0.
# Fail otherwise.
$passwordPolicy = Get-MgDomain -DomainId $DomainName | Select-Object PasswordValidityPeriodInDays
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Rec = "1.3.1"
$auditResult.RecDescription = "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire'"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "5.2"
$auditResult.CISDescription = "Use Unique Passwords"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true # All are true
$auditResult.Result = $passwordPolicy.PasswordValidityPeriodInDays -eq 0
$auditResult.Details = "Validity Period: $($passwordPolicy.PasswordValidityPeriodInDays) days"
$auditResult.FailureReason = if ($passwordPolicy.PasswordValidityPeriodInDays -eq 0) { "N/A" } else { "Password expiration is not set to never expire" }
$auditResult.Status = if ($passwordPolicy.PasswordValidityPeriodInDays -eq 0) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,41 @@
function Test-ReauthWithCode_7.2.10_E3L1 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.10 (L1) Ensure reauthentication with verification code is restricted
$SPOTenantReauthentication = Get-SPOTenant | Select-Object EmailAttestationRequired, EmailAttestationReAuthDays
$isReauthenticationRestricted = $SPOTenantReauthentication.EmailAttestationRequired -and $SPOTenantReauthentication.EmailAttestationReAuthDays -le 15
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "7.2.10"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure reauthentication with verification code is restricted"
$auditResult.Result = $isReauthenticationRestricted
$auditResult.Details = "EmailAttestationRequired: $($SPOTenantReauthentication.EmailAttestationRequired); EmailAttestationReAuthDays: $($SPOTenantReauthentication.EmailAttestationReAuthDays)"
$auditResult.FailureReason = if (-not $isReauthenticationRestricted) { "Reauthentication with verification code does not require reauthentication within 15 days or less." } else { "N/A" }
$auditResult.Status = if ($isReauthenticationRestricted) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,56 @@
function Test-ReportSecurityInTeams_8.6.1_E3L1 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.6.1 (L1) Ensure users can report security concerns in Teams
# Connect to Teams PowerShell using Connect-MicrosoftTeams
# Connect to Exchange Online PowerShell using Connect-ExchangeOnline
$CsTeamsMessagingPolicy = Get-CsTeamsMessagingPolicy -Identity Global | Select-Object -Property AllowSecurityEndUserReporting
$ReportSubmissionPolicy = Get-ReportSubmissionPolicy | Select-Object -Property ReportJunkToCustomizedAddress, ReportNotJunkToCustomizedAddress, ReportPhishToCustomizedAddress, ReportChatMessageToCustomizedAddressEnabled
$securityReportEnabled = $CsTeamsMessagingPolicy.AllowSecurityEndUserReporting -and
$ReportSubmissionPolicy.ReportJunkToCustomizedAddress -and
$ReportSubmissionPolicy.ReportNotJunkToCustomizedAddress -and
$ReportSubmissionPolicy.ReportPhishToCustomizedAddress -and
$ReportSubmissionPolicy.ReportChatMessageToCustomizedAddressEnabled
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.6.1"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure users can report security concerns in Teams"
$auditResult.Result = $securityReportEnabled
$auditResult.Details = "AllowSecurityEndUserReporting: $($CsTeamsMessagingPolicy.AllowSecurityEndUserReporting); " +
"ReportJunkToCustomizedAddress: $($ReportSubmissionPolicy.ReportJunkToCustomizedAddress); " +
"ReportNotJunkToCustomizedAddress: $($ReportSubmissionPolicy.ReportNotJunkToCustomizedAddress); " +
"ReportPhishToCustomizedAddress: $($ReportSubmissionPolicy.ReportPhishToCustomizedAddress); " +
"ReportChatMessageToCustomizedAddressEnabled: $($ReportSubmissionPolicy.ReportChatMessageToCustomizedAddressEnabled)"
$auditResult.FailureReason = if (-not $securityReportEnabled) { "Users cannot report security concerns in Teams due to one or more incorrect settings" } else { "N/A" }
$auditResult.Status = if ($securityReportEnabled) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,52 @@
function Test-RestrictCustomScripts_7.3.4_E3L1_IG3 {
[CmdletBinding()]
param (
# Define your parameters here if needed
)
begin {
# .TODO Test behavior in Prod
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# CIS 2.7 Ensure custom script execution is restricted on site collections
# Pass if DenyAddAndCustomizePages is set to true. Fail otherwise.
# Connect to SharePoint Online using Connect-SPOService
$SPOSitesCustomScript = Get-SPOSite | Select-Object Title, Url, DenyAddAndCustomizePages
$customScriptDisabledSites = $SPOSitesCustomScript | Where-Object { $_.DenyAddAndCustomizePages -eq 'Enabled' }
$customScriptEnabledSites = $SPOSitesCustomScript | Where-Object { $_.DenyAddAndCustomizePages -ne 'Enabled' }
$customScriptDisabledResult = $customScriptEnabledSites.Count -eq 0
# Correctly gathering details for sites with custom scripts enabled
$customScriptEnabledDetails = $customScriptEnabledSites | ForEach-Object { "$($_.Title) ($($_.Url)): Custom Script - $($_.DenyAddAndCustomizePages)" }
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "2.7"
$auditResult.CISDescription = "Allowlist Authorized Scripts"
$auditResult.Rec = "7.3.4"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false # Set based on the benchmark
$auditResult.IG2 = $false # Set based on the benchmark
$auditResult.IG3 = $true # Set based on the benchmark
$auditResult.RecDescription = "Ensure custom script execution is restricted on site collections"
$auditResult.Result = $customScriptDisabledResult
$auditResult.Details = if (-not $customScriptDisabledResult) { $customScriptEnabledDetails -join "; " } else { "All site collections have custom script execution restricted" }
$auditResult.FailureReason = if (-not $customScriptDisabledResult) { "The following site collections have custom script execution enabled: " + ($customScriptEnabledDetails -join "; ") } else { "N/A" }
$auditResult.Status = if ($customScriptDisabledResult) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,41 @@
function Test-RestrictExternalSharing_7.2.3_E3L1_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.3 (L1) Ensure external content sharing is restricted
$SPOTenantSharingCapability = Get-SPOTenant | Select-Object SharingCapability
$isRestricted = $SPOTenantSharingCapability.SharingCapability -in @('ExternalUserSharingOnly', 'ExistingExternalUserSharingOnly', 'Disabled')
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "7.2.3"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure external content sharing is restricted"
$auditResult.Result = $isRestricted
$auditResult.Details = "SharingCapability: $($SPOTenantSharingCapability.SharingCapability)"
$auditResult.FailureReason = if (-not $isRestricted) { "External content sharing is not adequately restricted. Current setting: $($SPOTenantSharingCapability.SharingCapability)" } else { "N/A" }
$auditResult.Status = if ($isRestricted) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,90 @@
function Test-RestrictOutlookAddins_6.3.1_E3L2_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters could include credentials or other necessary data
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
$customPolicyFailures = @()
$defaultPolicyFailureDetails = @()
$relevantRoles = @('My Custom Apps', 'My Marketplace Apps', 'My ReadWriteMailbox Apps')
}
process {
# Main functionality
# 6.3.1 (L2) Ensure users installing Outlook add-ins is not allowed
# Check all mailboxes for custom policies with unallowed add-ins
$roleAssignmentPolicies = Get-EXOMailbox | Select-Object -Unique RoleAssignmentPolicy
if ($roleAssignmentPolicies.RoleAssignmentPolicy) {
foreach ($policy in $roleAssignmentPolicies) {
if ($policy.RoleAssignmentPolicy) {
$rolePolicyDetails = Get-RoleAssignmentPolicy -Identity $policy.RoleAssignmentPolicy
$foundRoles = $rolePolicyDetails.AssignedRoles | Where-Object { $_ -in $relevantRoles }
if ($foundRoles) {
$customPolicyFailures += "Policy: $($policy.RoleAssignmentPolicy): Roles: $($foundRoles -join ', ')"
}
}
}
}
# Check Default Role Assignment Policy
$defaultPolicy = Get-RoleAssignmentPolicy "Default Role Assignment Policy"
$defaultPolicyRoles = $defaultPolicy.AssignedRoles | Where-Object { $_ -in $relevantRoles }
if ($defaultPolicyRoles) {
$defaultPolicyFailureDetails = $defaultPolicyRoles
}
}
end {
# Prepare result object
$auditResult.Rec = "6.3.1"
$auditResult.CISControl = "9.4"
$auditResult.CISDescription = "Restrict Unnecessary or Unauthorized Browser and Email Client Extensions"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure users installing Outlook add-ins is not allowed"
$detailsString = ""
if ($customPolicyFailures) {
$detailsString += "Custom Policy Failures: `n"
foreach ($failure in $customPolicyFailures) {
$detailsString += "`t$failure`n"
}
}
else {
$detailsString += "Custom Policy Failures: None`n"
}
$detailsString += "Default Role Assignment Policy: "
if ($defaultPolicyFailureDetails) {
$detailsString += "$($defaultPolicyFailureDetails -join ', ')"
}
else {
$detailsString += "Compliant"
}
if ($customPolicyFailures -or $defaultPolicyFailureDetails) {
$auditResult.Result = $false
$auditResult.Status = "Fail"
$auditResult.Details = $detailsString
$auditResult.FailureReason = "Unauthorized Outlook add-ins found in custom or default policies."
}
else {
$auditResult.Result = $true
$auditResult.Status = "Pass"
$auditResult.Details = "No unauthorized Outlook add-ins found in custom or default policies."
$auditResult.FailureReason = "N/A"
}
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,48 @@
function Test-RestrictStorageProvidersOutlook_6.5.3_E3L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 6.5.3 (L2) Ensure additional storage providers are restricted in Outlook on the web
$owaPolicies = Get-OwaMailboxPolicy
$allPoliciesRestricted = $owaPolicies | ForEach-Object { $_.AdditionalStorageProvidersAvailable } | ForEach-Object { -not $_ }
# Create an instance of CISAuditResult and populate it
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "6.5.3"
$auditResult.ELevel = "E3" # Based on your environment
$auditResult.Profile = "L2"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure additional storage providers are restricted in Outlook on the web"
$auditResult.Result = $allPoliciesRestricted
$auditResult.Details = if($allPoliciesRestricted) {
"All OwaMailbox policies restrict AdditionalStorageProvidersAvailable"
} else {
$nonCompliantPolicies = $owaPolicies | Where-Object { $_.AdditionalStorageProvidersAvailable } | Select-Object -ExpandProperty Name
"Non-compliant OwaMailbox policies: $($nonCompliantPolicies -join ', ')"
}
$auditResult.FailureReason = if(-not $allPoliciesRestricted) { "One or more OwaMailbox policies allow AdditionalStorageProvidersAvailable." } else { "N/A" }
$auditResult.Status = if($allPoliciesRestricted) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}
# Additional helper functions (if any)

View File

@@ -0,0 +1,43 @@
function Test-RestrictTenantCreation_5.1.2.3_E3L1 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 5.1.2.3 (L1) Ensure 'Restrict non-admin users from creating tenants' is set to 'Yes'
# Pass if AllowedToCreateTenants is False. Fail otherwise.
$tenantCreationPolicy = (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Select-Object AllowedToCreateTenants
$tenantCreationResult = -not $tenantCreationPolicy.AllowedToCreateTenants
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($tenantCreationResult) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "5.1.2.3"
$auditResult.RecDescription = "Ensure 'Restrict non-admin users from creating tenants' is set to 'Yes'"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.Result = $tenantCreationResult
$auditResult.Details = "AllowedToCreateTenants: $($tenantCreationPolicy.AllowedToCreateTenants)"
$auditResult.FailureReason = if (-not $tenantCreationResult) { "Non-admin users can create tenants" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,49 @@
function Test-SafeAttachmentsPolicy_2.1.4_E5L2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# Retrieve all Safe Attachment policies where Enable is set to True
$safeAttachmentPolicies = Get-SafeAttachmentPolicy | Where-Object { $_.Enable -eq $true }
# If there are any enabled policies, the result is Pass. If not, it's Fail.
$result = $safeAttachmentPolicies -ne $null -and $safeAttachmentPolicies.Count -gt 0
$details = if ($result) {
"Enabled Safe Attachments Policies: $($safeAttachmentPolicies.Name -join ', ')"
} else {
"No Safe Attachments Policies are enabled."
}
$failureReason = if ($result) { "N/A" } else { "Safe Attachments policy is not enabled." }
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($result) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E5"
$auditResult.Profile = "L2"
$auditResult.Rec = "2.1.4"
$auditResult.RecDescription = "Ensure Safe Attachments policy is enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "9.7"
$auditResult.CISDescription = "Deploy and Maintain Email Server Anti-Malware Protections"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $true
$auditResult.Result = $result
$auditResult.Details = $details
$auditResult.FailureReason = $failureReason
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,56 @@
function Test-SafeAttachmentsTeams_2.1.5_E5L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# Retrieve the ATP policies for Office 365 and check Safe Attachments settings
$atpPolicies = Get-AtpPolicyForO365
# Check if the required ATP policies are enabled
$atpPolicyResult = $atpPolicies | Where-Object {
$_.EnableATPForSPOTeamsODB -eq $true -and
$_.EnableSafeDocs -eq $true -and
$_.AllowSafeDocsOpen -eq $false
}
# Determine the result based on the ATP policy settings
$result = $null -ne $atpPolicyResult
$details = if ($result) {
"ATP for SharePoint, OneDrive, and Teams is enabled with correct settings."
} else {
"ATP for SharePoint, OneDrive, and Teams is not enabled with correct settings."
}
$failureReason = if ($result) { "N/A" } else { "ATP policy for SharePoint, OneDrive, and Microsoft Teams is not correctly configured." }
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($result) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E5"
$auditResult.Profile = "L2"
$auditResult.Rec = "2.1.5"
$auditResult.RecDescription = "Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "9.7, 10.1"
$auditResult.CISDescription = "Deploy and Maintain Email Server Anti-Malware Protections, Deploy and Maintain Anti-Malware Software"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $result
$auditResult.Details = $details
$auditResult.FailureReason = $failureReason
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,70 @@
function Test-SafeLinksOfficeApps_2.1.1_E5L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here if needed
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# Retrieve all Safe Links policies
$policies = Get-SafeLinksPolicy
# Initialize the details collection
$misconfiguredDetails = @()
foreach ($policy in $policies) {
# Get the detailed configuration of each policy
$policyDetails = Get-SafeLinksPolicy -Identity $policy.Name
# Check each required property and record failures
$failures = @()
if ($policyDetails.EnableSafeLinksForEmail -ne $true) { $failures += "EnableSafeLinksForEmail: False" }
if ($policyDetails.EnableSafeLinksForTeams -ne $true) { $failures += "EnableSafeLinksForTeams: False" }
if ($policyDetails.EnableSafeLinksForOffice -ne $true) { $failures += "EnableSafeLinksForOffice: False" }
if ($policyDetails.TrackClicks -ne $true) { $failures += "TrackClicks: False" }
if ($policyDetails.AllowClickThrough -ne $false) { $failures += "AllowClickThrough: True" }
if ($policyDetails.ScanUrls -ne $true) { $failures += "ScanUrls: False" }
if ($policyDetails.EnableForInternalSenders -ne $true) { $failures += "EnableForInternalSenders: False" }
if ($policyDetails.DeliverMessageAfterScan -ne $true) { $failures += "DeliverMessageAfterScan: False" }
if ($policyDetails.DisableUrlRewrite -ne $false) { $failures += "DisableUrlRewrite: True" }
# Only add details for policies that have misconfigurations
if ($failures.Count -gt 0) {
$misconfiguredDetails += "Policy: $($policy.Name); Failures: $($failures -join ', ')"
}
}
# Prepare the final result
$result = $misconfiguredDetails.Count -eq 0
$details = if ($result) { "All Safe Links policies are correctly configured." } else { $misconfiguredDetails -join ' | ' }
# Create the audit result object
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($result) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E5"
$auditResult.Profile = "L2"
$auditResult.Rec = "2.1.1"
$auditResult.RecDescription = "Ensure Safe Links for Office Applications is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "10.1"
$auditResult.CISDescription = "Deploy and Maintain Anti-Malware Software"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $result
$auditResult.Details = $details
$auditResult.FailureReason = if ($result) { "N/A" } else { "The following Safe Links policies settings do not meet the recommended configuration: $($misconfiguredDetails -join ' | ')" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,40 @@
function Test-SharePointAADB2B_7.2.2_E3L1 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.2 (L1) Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled
$SPOTenantAzureADB2B = Get-SPOTenant | Select-Object EnableAzureADB2BIntegration
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "7.2.2"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled"
$auditResult.Result = $SPOTenantAzureADB2B.EnableAzureADB2BIntegration
$auditResult.Details = "EnableAzureADB2BIntegration: $($SPOTenantAzureADB2B.EnableAzureADB2BIntegration)"
$auditResult.FailureReason = if (-not $SPOTenantAzureADB2B.EnableAzureADB2BIntegration) { "Azure AD B2B integration is not enabled" } else { "N/A" }
$auditResult.Status = if ($SPOTenantAzureADB2B.EnableAzureADB2BIntegration) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,41 @@
function Test-SharePointExternalSharingDomains_7.2.6_E3L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.6 (L2) Ensure SharePoint external sharing is managed through domain whitelist/blacklists
$SPOTenant = Get-SPOTenant | Select-Object SharingDomainRestrictionMode, SharingAllowedDomainList
$isDomainRestrictionConfigured = $SPOTenant.SharingDomainRestrictionMode -eq 'AllowList'
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "7.2.6"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure SharePoint external sharing is managed through domain whitelist/blacklists"
$auditResult.Result = $isDomainRestrictionConfigured
$auditResult.Details = "SharingDomainRestrictionMode: $($SPOTenant.SharingDomainRestrictionMode); SharingAllowedDomainList: $($SPOTenant.SharingAllowedDomainList)"
$auditResult.FailureReason = if (-not $isDomainRestrictionConfigured) { "Domain restrictions for SharePoint external sharing are not configured to 'AllowList'. Current setting: $($SPOTenant.SharingDomainRestrictionMode)" } else { "N/A" }
$auditResult.Status = if ($isDomainRestrictionConfigured) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,41 @@
function Test-SharePointGuestsItemSharing_7.2.5_E3L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
. ".\source\Classes\CISAuditResult.ps1"
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.5 (L2) Ensure that SharePoint guest users cannot share items they don't own
$SPOTenant = Get-SPOTenant | Select-Object PreventExternalUsersFromResharing
$isGuestResharingPrevented = $SPOTenant.PreventExternalUsersFromResharing
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "7.2.5"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure that SharePoint guest users cannot share items they don't own"
$auditResult.Result = $isGuestResharingPrevented
$auditResult.Details = "PreventExternalUsersFromResharing: $isGuestResharingPrevented"
$auditResult.FailureReason = if (-not $isGuestResharingPrevented) { "Guest users can reshare items they don't own." } else { "N/A" }
$auditResult.Status = if ($isGuestResharingPrevented) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,56 @@
function Test-SpamPolicyAdminNotify_2.1.6_E3L1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# Get the default hosted outbound spam filter policy
$hostedOutboundSpamFilterPolicy = Get-HostedOutboundSpamFilterPolicy | Where-Object { $_.IsDefault -eq $true }
# Check if both settings are enabled
$bccSuspiciousOutboundMailEnabled = $hostedOutboundSpamFilterPolicy.BccSuspiciousOutboundMail
$notifyOutboundSpamEnabled = $hostedOutboundSpamFilterPolicy.NotifyOutboundSpam
$areSettingsEnabled = $bccSuspiciousOutboundMailEnabled -and $notifyOutboundSpamEnabled
# Prepare failure details if any setting is not enabled
$failureDetails = @()
if (-not $bccSuspiciousOutboundMailEnabled) {
$failureDetails += "BccSuspiciousOutboundMail is not enabled."
}
if (-not $notifyOutboundSpamEnabled) {
$failureDetails += "NotifyOutboundSpam is not enabled."
}
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($areSettingsEnabled) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.Profile = "L1"
$auditResult.Rec = "2.1.6"
$auditResult.RecDescription = "Ensure Exchange Online Spam Policies are set to notify administrators"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "17.5"
$auditResult.CISDescription = "Assign Key Roles and Responsibilities"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $areSettingsEnabled
$auditResult.Details = if ($areSettingsEnabled) { "Both BccSuspiciousOutboundMail and NotifyOutboundSpam are enabled." } else { $failureDetails -join ' ' }
$auditResult.FailureReason = if (-not $areSettingsEnabled) { "One or both spam policies are not set to notify administrators." } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,52 @@
function Test-TeamsExternalAccess_8.2.1_E3L2 {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.2.1 (L1) Ensure 'external access' is restricted in the Teams admin center
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$externalAccessConfig = Get-CsTenantFederationConfiguration
$allowedDomainsLimited = $false
if ($externalAccessConfig.AllowFederatedUsers -and $externalAccessConfig.AllowedDomains -and $externalAccessConfig.AllowedDomains.AllowedDomain.Count -gt 0) {
$allowedDomainsLimited = $true
}
# Check if the configurations are as recommended
$isCompliant = -not $externalAccessConfig.AllowTeamsConsumer -and -not $externalAccessConfig.AllowPublicUsers -and (-not $externalAccessConfig.AllowFederatedUsers -or $allowedDomainsLimited)
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # The control is Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.2.1"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure 'external access' is restricted in the Teams admin center"
$auditResult.Result = $isCompliant
$auditResult.Details = "AllowTeamsConsumer: $($externalAccessConfig.AllowTeamsConsumer); AllowPublicUsers: $($externalAccessConfig.AllowPublicUsers); AllowFederatedUsers: $($externalAccessConfig.AllowFederatedUsers); AllowedDomains limited: $allowedDomainsLimited"
$auditResult.FailureReason = if (-not $isCompliant) { "One or more external access configurations are not compliant." } else { "N/A" }
$auditResult.Status = if ($isCompliant) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,56 @@
function Test-TeamsExternalFileSharing_8.1.1_E3L2_IG1_IG2_IG3 {
[CmdletBinding()]
param (
# Parameters can be added here if needed
)
begin {
# Dot source the class script
. ".\source\Classes\CISAuditResult.ps1"
$auditResults = @()
}
process {
# 8.1.1 (L2) Ensure external file sharing in Teams is enabled for only approved cloud storage services
# Connect to Teams PowerShell using Connect-MicrosoftTeams
# Assuming that 'approvedProviders' is a list of approved cloud storage service names
# This list must be defined according to your organization's approved cloud storage services
$approvedProviders = @("AllowDropBox", "AllowBox", "AllowGoogleDrive", "AllowShareFile", "AllowEgnyte")
$clientConfig = Get-CsTeamsClientConfiguration
$isCompliant = $true
$nonCompliantProviders = @()
foreach ($provider in $approvedProviders) {
if (-not $clientConfig.$provider) {
$isCompliant = $false
$nonCompliantProviders += $provider
}
}
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "8.1.1"
$auditResult.ELevel = "E3"
$auditResult.Profile = "L2"
$auditResult.IG1 = $true # Set based on the benchmark
$auditResult.IG2 = $true # Set based on the benchmark
$auditResult.IG3 = $true # Set based on the benchmark
$auditResult.RecDescription = "Ensure external file sharing in Teams is enabled for only approved cloud storage services"
$auditResult.Result = $isCompliant
$auditResult.Details = if (-not $isCompliant) { "Non-approved providers enabled: $($nonCompliantProviders -join ', ')" } else { "All cloud storage services are approved providers" }
$auditResult.FailureReason = if (-not $isCompliant) { "The following non-approved providers are enabled: $($nonCompliantProviders -join ', ')" } else { "N/A" }
$auditResult.Status = if ($isCompliant) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,60 @@
---
Module Name: ADAuditTasks
Module Guid: '7ddb359a-e07f-4be0-b63a-a81f44c61fff'
Download Help Link: https://audittaskshelpfiles.blob.core.windows.net/helpfiles/
Help Version: 1.0.0.5
Locale: en-US
---
# ADAuditTasks Module
## Description
{{ Fill in the Description }}
## ADAuditTasks Cmdlets
### [Convert-NmapXMLToCSV](Convert-NmapXMLToCSV)
Converts an Nmap XML scan output file to a CSV file.
### [Get-ADActiveUserAudit](Get-ADActiveUserAudit)
Gets active but stale AD User accounts that haven't logged in within the last 90 days by default.
### [Get-ADHostAudit](Get-ADHostAudit)
Active Directory Server and Workstation Audit with Report export option (Can also be piped to CSV if Report isn't specified).
### [Get-ADUserLogonAudit](Get-ADUserLogonAudit)
Retrieves the most recent LastLogon timestamp for a specified Active Directory user
account from all domain controllers and outputs it as a DateTime object.
### [Get-ADUserPrivilegeAudit](Get-ADUserPrivilegeAudit)
Produces three object outputs: PrivilegedGroups, AdExtendedRights, and possible service accounts.
### [Get-ADUserWildCardAudit](Get-ADUserWildCardAudit)
Takes a search string to find commonly named accounts.
### [Get-HostTag](Get-HostTag)
Creates a host name or tag based on predetermined criteria for as many as 999 hosts at a time.
### [Get-NetworkAudit](Get-NetworkAudit)
Discovers the local network and runs port scans on all hosts found for specific or default sets of ports, displaying MAC ID vendor info.
### [Get-QuickPing](Get-QuickPing)
Performs a quick ping on a range of IP addresses and returns an array of IP addresses
that responded to the ping and an array of IP addresses that failed to respond.
### [Get-WebCertAudit](Get-WebCertAudit)
Retrieves the certificate information for a web server.
### [Join-CSVFile](Join-CSVFile)
Joins multiple CSV files with the same headers into a single CSV file.
### [Merge-ADAuditZip](Merge-ADAuditZip)
Combines multiple audit report files into a single compressed ZIP file.
### [Merge-NmapToADHostAudit](Merge-NmapToADHostAudit)
Merges Nmap network audit data with Active Directory host audit data.
### [Send-AuditEmail](Send-AuditEmail)
This is a wrapper function for Send-MailKitMessage and takes string arrays as input.
### [Submit-FTPUpload](Submit-FTPUpload)
Uploads a file to an FTP server using the WinSCP module.

View File

@@ -0,0 +1,21 @@
Microsoft Public License (MS-PL)
This license governs use of the accompanying software. If you use the software, you
accept this license. If you do not accept the license, do not use the software.
1. Definitions
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the
same meaning here as under U.S. copyright law.
A "contribution" is the original software, or any additions or changes to the software.
A "contributor" is any person that distributes its contribution under this license.
"Licensed patents" are a contributor's patent claims that read directly on its contribution.
2. Grant of Rights
(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
3. Conditions and Limitations
(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.

View File

@@ -0,0 +1,20 @@
psDoc is a Powershell Help Document generator.
----
### Using psDoc ###
To generate documentation off of your module, simply import your module
```
Import-Module MySpecialModule
```
And generate the documentation
```
.\psDoc.ps1 -moduleName MySpecialModule
```
### License ###
[Microsoft Public License (Ms-PL)](https://opensource.org/licenses/MS-PL)

View File

@@ -0,0 +1,194 @@
function TrimAllLines([string] $str) {
$lines = $str -split "`n"
for ($i = 0; $i -lt $lines.Count; $i++) {
$lines[$i] = $lines[$i].Trim()
}
# Trim EOL.
($lines | Out-String).Trim()
}
function FixMarkdownString([string] $in = '', [bool] $includeBreaks = $false, [bool]$BlankStringToSpace = $False) {
if ($in -eq $null) { return }
if($in -eq "" -and $BlankStringToSpace ) { return " " }
$replacements = @{
'\' = '\\'
'`' = '\`'
'*' = '\*'
'_' = '\_'
'{' = '\{'
'}' = '\}'
'[' = '\['
']' = '\]'
'(' = '\('
')' = '\)'
'#' = '\#'
'+' = '\+'
'!' = '\!'
}
$rtn = $in.Trim()
foreach ($key in $replacements.Keys) {
$rtn = $rtn.Replace($key, $replacements[$key])
}
$rtn = TrimAllLines $rtn
if ($includeBreaks) {
$crlf = [Environment]::NewLine
$rtn = $rtn.Replace($crlf, " $crlf")
}
$rtn
}
function FixMarkdownCodeString([string] $in) {
if ($in -eq $null) { return }
TrimAllLines $in
}
function IncludeTableOfContents {
return "{toc:printable=true|style=square|maxLevel=2|indent=5px|minLevel=2|class=bigpink|exclude=[1//2]|type=list|outline=true|include=.*}"
}
@"
h1. $moduleName
$(IncludeTableOfContents)
\\
\\
"@
$progress = 0
$commandsHelp | % {
Update-Progress $_.Name 'Documentation'
$progress++
@"
h2. $(FixMarkdownString($_.Name))
"@
$synopsis = $_.synopsis.Trim()
$syntax = $_.syntax | out-string
if(-not ($synopsis -ilike "$($_.Name.Trim())*")){
$tmp = $synopsis
$synopsis = $syntax
$syntax = $tmp
@"
h3. Synopsis
$(FixMarkdownString($syntax))
"@
}
@"
h3. Description
$(FixMarkdownString $(($_.Description | out-string).Trim()) $true)
"@
@"
h3. Syntax
{code:theme=Confluence|linenumbers=false|language=Powershell|firstline=0001|collapse=false}
$(TrimAllLines $synopsis)
{code}
"@
if (!($_.alias.Length -eq 0)) {
@"
h3. $($_.Name) Aliases
"@
$_.alias | % {
@"
- $($_.Name)
"@
}
@"
"@
}
if($_.parameters){
@"
h3. Parameters
||Name||Alias||Description||Required?||Pipeline Input||Default Value||
"@
$_.parameters.parameter | % {
@"
|$(FixMarkdownString $_.Name $false $true)|$(FixMarkdownString $_.Aliases $false $true)|$(FixMarkdownString $($_.Description | out-string).Trim() $true $true)|$(FixMarkdownString $_.Required $false $true)|$(FixMarkdownString $_.PipelineInput $false $true)|$(FixMarkdownString $_.DefaultValue $false $true)|
"@
}
@"
"@
}
$inputTypes = $(FixMarkdownString($_.inputTypes | out-string))
if ($inputTypes.Length -gt 0 -and -not $inputTypes.Contains('inputType')) {
@"
h3. Inputs
- $inputTypes
"@
}
$returnValues = $(FixMarkdownString($_.returnValues | out-string))
if ($returnValues.Length -gt 0 -and -not $returnValues.StartsWith("returnValue")) {
@"
h3. Outputs
- $returnValues
"@
}
$notes = $(FixMarkdownString($_.alertSet | out-string))
if ($notes.Trim().Length -gt 0) {
@"
h3. Note
$notes
"@
}
if(($_.examples | Out-String).Trim().Length -gt 0) {
@"
h3. Examples
"@
$_.examples.example | % {
@"
{code:title=$(FixMarkdownString($_.title.Trim(('-',' '))))|theme=Confluence|linenumbers=true|language=Powershell|firstline=0001|collapse=false}
$(FixMarkdownCodeString($_.code | out-string ))
{code}
$(FixMarkdownString($_.remarks | out-string ) $true)
"@
}
}
if(($_.relatedLinks | Out-String).Trim().Length -gt 0) {
@"
h3. Links
"@
$_.links | % {
@"
- [$($_.name)|$($_.link)]
"@
}
}
@"
\\
\\
\\
----
\\
\\
\\
"@
}

View File

@@ -0,0 +1,346 @@
@"
<!DOCTYPE html>
<!--
<auto-generated>
<synopsis>
This code was generated by a tool. on: $(Get-Date)
</synopsis>
<description>
If you'd like to regenerate the documentation, please open up powershell and run
> .\psDoc.ps1 -moduleName NameOfYourModule
If the documentation is incomplete, or eronious,
please edit the comments at the top of the module method within it's respecive .ps1 file.
</description>
</auto-generated>
-->
<html lang="en">
<head>
<title>$moduleName Documentation</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/styles/shCore.min.css" rel="stylesheet" charset="utf-8">
<link href="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/styles/shCoreDefault.min.css" rel="stylesheet" charset="utf-8">
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet" charset="utf-8">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<style>
.syntaxhighlighter {
overflow-y: hidden !important;
overflow-x: auto !important;
}
pre {
min-height: 30px;
}
.navbar-nav {
height: 100%;
overflow-y: auto;
}
.form-group {
padding-top: 12px;
padding-left: 12px;
padding-right: 12px;
}
.sidebar-nav .navbar-header {
float: none;
}
.sidebar-nav .navbar li a {
padding-top: 4px;
padding-bottom: 4px;
}
@media (min-width: 768px) {
.sidebar-nav .navbar .navbar-collapse {
padding: 0;
max-height: none;
}
.sidebar-nav .navbar ul {
float: none;
}
.sidebar-nav .navbar ul:not {
display: block;
}
.sidebar-nav .navbar li {
float: none;
display: block;
}
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row-fluid">
<div><h1>$moduleName</h1></div>
</div>
<div class="row-fluid">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<div class="sidebar-nav">
<div class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".sidebar-navbar-collapse">
<span class="sr-only">Toggle</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<span class="visible-xs navbar-brand">click menu to open</span>
</div>
<div class="navbar-collapse collapse sidebar-navbar-collapse">
<div class="form-group">
<input class="form-control" id="searchinput" type="search" placeholder="Filter..." />
</div>
<ul class="nav navbar-nav list-group" id="searchList">
"@
$progress = 0
$commandsHelp | % {
Update-Progress $_.Name 'Navigation'
$progress++
" <li class=`"nav-menu list-group-item`"><a href=`"#$($_.Name)`">$($_.Name)</a></li>"
}
@'
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12">
'@
$progress = 0
$commandsHelp | % {
Update-Progress $_.Name 'Documentation'
$progress++
@"
<div id=`"$(FixString($_.Name))`" class="toggle_container">
<div class="page-header">
<h2> $(FixString($_.Name)) </h2>
"@
$syn = FixString($_.synopsis)
if(!($syn).StartsWith($(FixString($_.Name)))){
@"
<p>$syn</p>
<p>$(FixString(($_.Description | out-string).Trim()) $true)</p>
"@
}
@"
</div>
"@
if (!($_.alias.Length -eq 0)) {
@"
<div class='panel panel-default'>
<div class='panel-heading'>
<h3 class='panel-title'> $($_.Name) Aliases </h3>
</div>
<div class='panel-body'>
<ul>
"@
$_.alias | % {
@"
<li>$($_.Name)</li>
"@
}
@"
</ul>
</div>
</div>
"@
}
if (!($_.syntax | Out-String ).Trim().Contains('syntaxItem')) {
@"
<div>
<h3> Syntax </h3>
</div>
<div class="panel panel-default">
<div class='panel-body'>
<pre class="brush: ps">$(FixString($_.syntax | out-string))</pre>
</div>
</div>
"@
}
if($_.parameters){
@"
<div>
<h3> Parameters </h3>
<table class="table table-striped table-bordered table-condensed visible-on">
<thead>
<tr>
<th>Name</th>
<th class="visible-lg visible-md">Alias</th>
<th>Description</th>
<th class="visible-lg visible-md">Required?</th>
<th class="visible-lg">Pipeline Input</th>
<th class="visible-lg">Default Value</th>
</tr>
</thead>
<tbody>
"@
$_.parameters.parameter | % {
@"
<tr>
<td><nobr>-$(FixString($_.Name))</nobr></td>
<td class="visible-lg visible-md">$(FixString($_.Aliases))</td>
<td>$(FixString(($_.Description | out-string).Trim()) $true)</td>
<td class="visible-lg visible-md">$(FixString($_.Required))</td>
<td class="visible-lg">$(FixString($_.PipelineInput))</td>
<td class="visible-lg">$(FixString($_.DefaultValue))</td>
</tr>
"@
}
@"
</tbody>
</table>
</div>
"@
}
$inputTypes = $(FixString($_.inputTypes | out-string))
if ($inputTypes.Length -gt 0 -and -not $inputTypes.Contains('inputType')) {
@"
<div>
<h3> Inputs </h3>
<p>The input type is the type of the objects that you can pipe to the cmdlet.</p>
<ul><li>$inputTypes</li></ul>
</div>
"@
}
$returnValues = $(FixString($_.returnValues | out-string))
if ($returnValues.Length -gt 0 -and -not $returnValues.StartsWith("returnValue")) {
@"
<div>
<h3> Outputs </h3>
<p>The output type is the type of the objects that the cmdlet emits.</p>
<ul><li>$returnValues</li></ul>
</div>
"@
}
$notes = $(FixString($_.alertSet | out-string))
if ($notes.Trim().Length -gt 0) {
@"
<div class='panel panel-default'>
<div class='panel-heading'>
<h3 class='panel-title'> Note </h3>
</div>
<div class='panel-body'>$notes</div>
</div>
"@
}
if(($_.examples | Out-String).Trim().Length -gt 0) {
@"
<div>
<h3> Examples </h3>
</div>
<div class='panel panel-default'>
<div class='panel-body'>
"@
$_.examples.example | % {
@"
<strong>$(FixString($_.title.Trim(('-',' '))))</strong>
<pre class="brush: ps">$(FixString($_.code | out-string ).Trim())</pre>
<div>$(FixString($_.remarks | out-string ).Trim())</div>
"@
}
@"
</div>
</div>
<p class='pull-right'><a onclick='document.body.scrollTop = document.documentElement.scrollTop = 0;' style='cursor: pointer;'>Top of page</a>
"@
}
if(($_.relatedLinks | Out-String).Trim().Length -gt 0) {
@"
<div>
<h3> Links </h3>
<div>
<ul>
"@
$_.links | % {
@"
<li class='$($_.cssClass)'><a href='$($_.link)' target='$($_.target)'>$($_.name)</a></li>
"@
}
@"
</ul>
</div>
</div>
"@
}
@"
</div>
"@
}
@'
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js" ></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/scripts/shCore.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/scripts/shBrushPowerShell.min.js" charset="utf-8"></script>
<script>
$(document).ready(function() {
$(".toggle_container").hide();
var previousId;
if(location.hash) {
var id = location.hash.slice(1); //Get rid of the # mark
var elementToShow = $("#" + id); //Save local reference
if(elementToShow.length) { //Check if the element exists
elementToShow.slideToggle('fast'); //Show the element
elementToShow.addClass("check_list_selected"); //Add class to element (the link)
}
previousId = id;
}
$('.nav-menu a, .psLink a').click(function() {
$(".sidebar-navbar-collapse").collapse('hide');
$('.toggle_container').hide(); // Hide all
var elem = $(this).prop("hash");
$(elem).toggle('fast'); // Show HREF/to/ID one
history.pushState({}, '', $(this).attr("href"));
window.scrollTo(0, 0);
return false;
});
SyntaxHighlighter.defaults['toolbar'] = false;
SyntaxHighlighter.defaults['gutter'] = false;
SyntaxHighlighter.all();
$('#searchList').btsListFilter('#searchinput', {itemChild: 'a', initial: false, resetOnBlur: true});
$(document).keyup(function( e ) {
if(!$('#searchinput').is(':focus') && e.which >= 65 && e.which <= 90 ){
$('#searchinput').val(String.fromCharCode(e.keyCode));
$('#searchinput').focus();
}
});
$(document).click(function() {
$('#searchinput').blur();
});
});
</script>
<!-- bootstrap-list-filter.min.js - removed as external resource and added as content -->
<script>
/*
* bootstrap-list-filter v0.1.7 - 2015-03-30
*
* Copyright 2015 Stefano Cudini
* stefano.cudini@gmail.com
* http://labs.easyblog.it/
*
* Licensed under the MIT license.
*
* Demos:
* http://labs.easyblog.it/bootstrap-list-filter/
*
* Source:
* git@github.com:stefanocudini/bootstrap-list-filter.git
*
*/
!function(a){a.fn.btsListFilter=function(b,c){function d(a,b){return a.replace(/\{ *([\w_]+) *\}/g,function(a,c){return b[c]||""})}function e(a,b){var c;return b=b||300,function(){var d=this,e=arguments;clearTimeout(c),c=setTimeout(function(){a.apply(d,Array.prototype.slice.call(e))},b)}}var f,g=this,h=a(this),i=a(b),j=h;return c=a.extend({delay:300,minLength:1,initial:!0,eventKey:"keyup",resetOnBlur:!0,sourceData:null,sourceTmpl:'<a class="list-group-item" href="#"><span>{title}</span></a>',sourceNode:function(a){return d(c.sourceTmpl,a)},emptyNode:function(){return'<a class="list-group-item well" href="#"><span>No Results</span></a>'},itemEl:".list-group-item",itemChild:null,itemFilter:function(b,d){d=d&&d.replace(new RegExp("[({[^.$*+?\\]})]","g"),"");var e=a(b).text(),f=c.initial?"^":"",g=new RegExp(f+d,"i");return g.test(e)}},c),i.on(c.eventKey,e(function(){var b=a(this).val();c.itemEl&&(j=h.find(c.itemEl)),c.itemChild&&(j=j.find(c.itemChild));var d=j.filter(function(){return c.itemFilter.call(g,this,b)}),e=j.not(d);c.itemChild&&(d=d.parents(c.itemEl),e=e.parents(c.itemEl).hide()),""!==b&&b.length>=c.minLength?(d.show(),e.hide(),"function"===a.type(c.sourceData)?(d.hide(),e.hide(),f&&(a.isFunction(f.abort)?f.abort():a.isFunction(f.stop)&&f.stop()),f=c.sourceData.call(g,b,function(b){if(f=null,d.hide(),e.hide(),h.find(".bts-dynamic-item").remove(),b&&0!==b.length)for(var i in b)a(c.sourceNode.call(g,b[i])).addClass("bts-dynamic-item").appendTo(h);else a(c.emptyNode.call(g)).addClass("bts-dynamic-item").appendTo(h)})):0===d.length&&a(c.emptyNode.call(g)).addClass("bts-dynamic-item").appendTo(h)):(d.show(),e.show(),h.find(".bts-dynamic-item").remove())},c.delay)),c.resetOnBlur&&i.on("blur",function(){a(this).val("").trigger(c.eventKey)}),h}}(jQuery);
</script>
</body>
</html>
'@

View File

@@ -0,0 +1,159 @@
function TrimAllLines([string] $str) {
$lines = $str -split "`n"
for ($i = 0; $i -lt $lines.Count; $i++) {
$lines[$i] = $lines[$i].Trim()
}
# Trim EOL.
($lines | Out-String).Trim()
}
function FixMarkdownString([string] $in = '', [bool] $includeBreaks = $false) {
if ($in -eq $null) { return }
$replacements = @{
'\' = '\\'
'`' = '\`'
'*' = '\*'
'_' = '\_'
'{' = '\{'
'}' = '\}'
'[' = '\['
']' = '\]'
'(' = '\('
')' = '\)'
'#' = '\#'
'+' = '\+'
'!' = '\!'
'<' = '\<'
'>' = '\>'
}
$rtn = $in.Trim()
foreach ($key in $replacements.Keys) {
$rtn = $rtn.Replace($key, $replacements[$key])
}
$rtn = TrimAllLines $rtn
$crlf = [Environment]::NewLine
if ($includeBreaks) {
$rtn = $rtn.Replace($crlf, " $crlf")
}
else {
$rtn = $rtn.Replace($crlf, " ").Trim()
}
$rtn
}
function FixMarkdownCodeString([string] $in) {
if ($in -eq $null) { return }
TrimAllLines $in
}
@"
# $moduleName Module
"@
$progress = 0
$commandsHelp | % {
Update-Progress $_.Name 'Documentation'
$progress++
@"
## $(FixMarkdownString($_.Name))
"@
$synopsis = $_.synopsis.Trim()
$syntax = $_.syntax | out-string
if (-not ($synopsis -ilike "$($_.Name.Trim())*")) {
$tmp = $synopsis
$synopsis = $syntax
$syntax = $tmp
@"
### Synopsis
$(FixMarkdownString($syntax))
"@
}
@"
### Syntax
``````powershell
$($synopsis)
``````
"@
if (!($_.alias.Length -eq 0)) {
@"
### $($_.Name) Aliases
"@
$_.alias | % {
@"
- $($_.Name)
"@
}
@"
"@
}
if ($_.parameters) {
@"
### Parameters
| Name | Alias | Description | Required? | Pipeline Input | Default Value |
| - | - | - | - | - | - |
"@
$_.parameters.parameter | % {
@"
| <nobr>$(FixMarkdownString($_.Name))</nobr> | $(FixMarkdownString($_.Aliases)) | $(FixMarkdownString(($_.Description | out-string).Trim())) | $(FixMarkdownString($_.Required)) | $(FixMarkdownString($_.PipelineInput)) | $(FixMarkdownString($_.DefaultValue)) |
"@
}
}
$inputTypes = $(FixMarkdownString($_.inputTypes | out-string))
if ($inputTypes.Length -gt 0 -and -not $inputTypes.Contains('inputType')) {
@"
### Inputs
- $inputTypes
"@
}
$returnValues = $(FixMarkdownString($_.returnValues | out-string))
if ($returnValues.Length -gt 0 -and -not $returnValues.StartsWith("returnValue")) {
@"
### Outputs
- $returnValues
"@
}
$notes = $(FixMarkdownString($_.alertSet | out-string))
if ($notes.Trim().Length -gt 0) {
@"
### Note
$notes
"@
}
if (($_.examples | Out-String).Trim().Length -gt 0) {
@"
### Examples
"@
$_.examples.example | % {
@"
**$(FixMarkdownString($_.title.Trim(('-',' '))))**
``````powershell
$(FixMarkdownCodeString($_.code | out-string ))
``````
$(FixMarkdownString($_.remarks | out-string ) $true)
"@
}
}
if (($_.relatedLinks | Out-String).Trim().Length -gt 0) {
@"
### Links
"@
$_.links | % {
@"
- [$($_.name)]($($_.link))
"@
}
}
}

View File

@@ -0,0 +1,63 @@
param(
[parameter(Mandatory=$true, Position=0)] [string] $moduleName,
[parameter(Mandatory=$false, Position=1)] [string] $template = "./out-html-template.ps1",
[parameter(Mandatory=$false, Position=2)] [string] $outputDir = './help',
[parameter(Mandatory=$false, Position=3)] [string] $fileName = 'index.html'
)
function FixString ($in = '', [bool]$includeBreaks = $false){
if ($in -eq $null) { return }
$rtn = $in.Replace('&', '&amp;').Replace('<', '&lt;').Replace('>', '&gt;').Trim()
if($includeBreaks){
$rtn = $rtn.Replace([Environment]::NewLine, '<br>')
}
return $rtn
}
function Update-Progress($name, $action){
Write-Progress -Activity "Rendering $action for $name" -CurrentOperation "Completed $progress of $totalCommands." -PercentComplete $(($progress/$totalCommands)*100)
}
$i = 0
$commandsHelp = (Get-Command -module $moduleName) | get-help -full | Where-Object {! $_.name.EndsWith('.ps1')}
foreach ($h in $commandsHelp){
$cmdHelp = (Get-Command $h.Name)
# Get any aliases associated with the method
$alias = get-alias -definition $h.Name -ErrorAction SilentlyContinue
if($alias){
$h | Add-Member Alias $alias
}
# Parse the related links and assign them to a links hashtable.
if(($h.relatedLinks | Out-String).Trim().Length -gt 0) {
$links = $h.relatedLinks.navigationLink | % {
if($_.uri){ @{name = $_.uri; link = $_.uri; target='_blank'} }
if($_.linkText){ @{name = $_.linkText; link = "#$($_.linkText)"; cssClass = 'psLink'; target='_top'} }
}
$h | Add-Member Links $links
}
# Add parameter aliases to the object.
foreach($p in $h.parameters.parameter ){
$paramAliases = ($cmdHelp.parameters.values | where name -like $p.name | select aliases).Aliases
if($paramAliases){
$p | Add-Member Aliases "$($paramAliases -join ', ')" -Force
}
}
}
# Create the output directory if it does not exist
if (-Not (Test-Path $outputDir)) {
New-Item -Path $outputDir -ItemType Directory | Out-Null
}
$totalCommands = $commandsHelp.Count
if (!$totalCommands) {
$totalCommands = 1
}
$template = Get-Content $template -raw -force
Invoke-Expression $template > "$outputDir\$fileName"

View File

@@ -0,0 +1,16 @@
class CISAuditResult {
[string]$Status
[string]$ELevel
[string]$ProfileLevel
[string]$Rec
[string]$RecDescription
[string]$CISControlVer = 'v8'
[string]$CISControl
[string]$CISDescription
[bool]$IG1
[bool]$IG2
[bool]$IG3
[bool]$Result
[string]$Details
[string]$FailureReason
}

View File

@@ -0,0 +1,136 @@
#
# Module manifest for module 'M365FoundationsCISReport'
#
# Generated by: Douglas S. Rios (DrIOSx)
#
# Generated on: 3/25/2024
#
@{
# Script module or binary module file associated with this manifest.
RootModule = 'M365FoundationsCISReport.psm1'
# Version number of this module.
ModuleVersion = '0.0.1'
# Supported PSEditions
# CompatiblePSEditions = @()
# ID used to uniquely identify this module
GUID = '0d064bfb-d1ce-484b-a173-993b55984dc9'
# Author of this module
Author = 'Douglas S. Rios (DrIOSx)'
# Company or vendor of this module
CompanyName = 'CrticalSolutions Net LLC'
# Copyright statement for this module
Copyright = '(c) 2024 Douglas S. Rios (DrIOSx). All rights reserved.'
# Description of the functionality provided by this module
Description = 'Automated assessment of 50 CIS 365 Foundations v3.0.0 benchmark.'
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.0'
# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @()
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @()
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
# Variables to export from this module
VariablesToExport = @()
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()
# DSC resources to export from this module
DscResourcesToExport = @()
# List of all modules packaged with this module
# ModuleList = @()
# List of all files packaged with this module
# FileList = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
Prerelease = ''
# Tags applied to this module. These help with module discovery in online galleries.
# Tags = @()
# A URL to the license for this module.
LicenseUri = 'https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en'
# A URL to the main website for this project.
# ProjectUri = ''
# A URL to an icon representing this module.
# IconUri = ''
# ReleaseNotes of this module
ReleaseNotes = ''
} # End of PSData hashtable
} # End of PrivateData hashtable
# HelpInfo URI of this module
# HelpInfoURI = ''
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''
}

View File

@@ -0,0 +1,5 @@
<#
This file is intentionally left empty. It is must be left here for the module
manifest to refer to. It is recreated during the build process.
#>

View File

@@ -0,0 +1,33 @@
function Assert-ModuleAvailability {
param(
[string]$ModuleName,
[string]$RequiredVersion,
[string]$SubModuleName
)
try {
$module = Get-Module -ListAvailable -Name $ModuleName | Where-Object { $_.Version -ge [version]$RequiredVersion }
if ($null -eq $module) {$auditResult.Profile
Write-Host "Installing $ModuleName module..."
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..."
Update-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force | Out-Null
}
else {
Write-Host "$ModuleName module is already at required version or newer."
}
if ($SubModuleName) {
Import-Module -Name "$ModuleName.$SubModuleName" -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null
}
else {
Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null
}
}
catch {
Write-Warning "An error occurred with module $ModuleName`: $_"
}
}

View File

@@ -0,0 +1,56 @@
function Connect-M365Suite {
[CmdletBinding()]
param (
# Parameter to specify the SharePoint Online Tenant Admin URL
[Parameter(Mandatory)]
[string]$TenantAdminUrl
)
$VerbosePreference = "SilentlyContinue"
try {
# Attempt to connect to Azure Active Directory
Write-Host "Connecting to Azure Active Directory..." -ForegroundColor Cyan
Connect-AzureAD | Out-Null
Write-Host "Successfully connected to Azure Active Directory." -ForegroundColor Green
# Attempt to connect to Exchange Online
Write-Host "Connecting to Exchange Online..." -ForegroundColor Cyan
Connect-ExchangeOnline | Out-Null
Write-Host "Successfully connected to Exchange Online." -ForegroundColor Green
try {
# Attempt to connect to Microsoft Graph with specified scopes
Write-Host "Connecting to Microsoft Graph with scopes: Directory.Read.All, Domain.Read.All, Policy.Read.All, Organization.Read.All" -ForegroundColor Cyan
Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -NoWelcome | Out-Null
Write-Host "Successfully connected to Microsoft Graph with specified scopes." -ForegroundColor Green
}
catch {
Write-Host "Failed to connect o MgGraph, attempting device auth." -ForegroundColor Yellow
# Attempt to connect to Microsoft Graph with specified scopes
Write-Host "Connecting to Microsoft Graph using device auth with scopes: Directory.Read.All, Domain.Read.All, Policy.Read.All, Organization.Read.All" -ForegroundColor Cyan
Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -UseDeviceCode -NoWelcome | Out-Null
Write-Host "Successfully connected to Microsoft Graph with specified scopes." -ForegroundColor Green
}
# Validate SharePoint Online Tenant Admin URL
if (-not $TenantAdminUrl) {
throw "SharePoint Online Tenant Admin URL is required."
}
# Attempt to connect to SharePoint Online
Write-Host "Connecting to SharePoint Online..." -ForegroundColor Cyan
Connect-SPOService -Url $TenantAdminUrl | Out-Null
Write-Host "Successfully connected to SharePoint Online." -ForegroundColor Green
# Attempt to connect to Microsoft Teams
Write-Host "Connecting to Microsoft Teams..." -ForegroundColor Cyan
Connect-MicrosoftTeams | Out-Null
Write-Host "Successfully connected to Microsoft Teams." -ForegroundColor Green
}
catch {
$VerbosePreference = "Continue"
Write-Host "There was an error establishing one or more connections: $_" -ForegroundColor Red
throw $_
}
$VerbosePreference = "Continue"
}

View File

@@ -0,0 +1,39 @@
function Disconnect-M365Suite {
# Clean up sessions
try {
Write-Host "Disconnecting from Exchange Online..." -ForegroundColor Green
Disconnect-ExchangeOnline -Confirm:$false | Out-Null
}
catch {
Write-Warning "Failed to disconnect from Exchange Online: $_"
}
try {
Write-Host "Disconnecting from Azure AD..." -ForegroundColor Green
Disconnect-AzureAD | Out-Null
}
catch {
Write-Warning "Failed to disconnect from Azure AD: $_"
}
try {
Write-Host "Disconnecting from Microsoft Graph..." -ForegroundColor Green
Disconnect-MgGraph | Out-Null
}
catch {
Write-Warning "Failed to disconnect from Microsoft Graph: $_"
}
try {
Write-Host "Disconnecting from SharePoint Online..." -ForegroundColor Green
Disconnect-SPOService | Out-Null
}
catch {
Write-Warning "Failed to disconnect from SharePoint Online: $_"
}
try {
Write-Host "Disconnecting from Microsoft Teams..." -ForegroundColor Green
Disconnect-MicrosoftTeams | Out-Null
}
catch {
Write-Warning "Failed to disconnect from Microsoft Teams: $_"
}
Write-Host "All sessions have been disconnected." -ForegroundColor Green
}

View File

@@ -0,0 +1,258 @@
<#
.SYNOPSIS
Invokes a security audit for Microsoft 365 environments.
.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.
.PARAMETER TenantAdminUrl
The URL of the tenant admin. This parameter is mandatory.
.PARAMETER DomainName
The domain name of the Microsoft 365 environment. This parameter is mandatory.
.PARAMETER ELevel
Specifies the E-Level (E3 or E5) for the audit. This parameter is optional and can be combined with the ProfileLevel parameter.
.PARAMETER ProfileLevel
Specifies the profile level (L1 or L2) for the audit. This parameter is optional and can be combined with the ELevel parameter.
.PARAMETER IncludeIG1
If specified, includes tests where IG1 is true.
.PARAMETER IncludeIG2
If specified, includes tests where IG2 is true.
.PARAMETER IncludeIG3
If specified, includes tests where IG3 is true.
.PARAMETER IncludeRecommendation
Specifies specific recommendations to include in the audit. Accepts an array of recommendation numbers.
.PARAMETER SkipRecommendation
Specifies specific recommendations to exclude from the audit. Accepts an array of recommendation numbers.
.PARAMETER DoNotConnect
If specified, the cmdlet will not establish a connection to Microsoft 365 services.
.PARAMETER DoNotDisconnect
If specified, the cmdlet will not disconnect from Microsoft 365 services after execution.
.PARAMETER NoModuleCheck
If specified, the cmdlet will not check for the presence of required modules.
.EXAMPLE
PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -ELevel "E5" -ProfileLevel "L1"
Performs a security audit for the E5 level and L1 profile in the specified Microsoft 365 environment.
.EXAMPLE
PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -IncludeIG1
Performs an audit including all tests where IG1 is true.
.EXAMPLE
PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com" -SkipRecommendation '1.1.3', '2.1.1'
Performs an audit while excluding specific recommendations 1.1.3 and 2.1.1.
.EXAMPLE
PS> $auditResults = Invoke-M365SecurityAudit -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DomainName "contoso.com"
PS> $auditResults | Export-Csv -Path "auditResults.csv" -NoTypeInformation
Captures the audit results into a variable and exports them to a CSV file.
.INPUTS
None. You cannot pipe objects to Invoke-M365SecurityAudit.
.OUTPUTS
CISAuditResult[]
The cmdlet returns an array of CISAuditResult objects representing the results of the security audit.
.NOTES
- This module is based on CIS benchmarks.
- 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.
- 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
.LINK
Online Version: [GitHub Repository URL]
#>
function Invoke-M365SecurityAudit {
[CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Default')]
[OutputType([CISAuditResult[]])]
param (
[Parameter(Mandatory = $true)]
[string]$TenantAdminUrl,
[Parameter(Mandatory = $true)]
[string]$DomainName,
# E-Level with optional ProfileLevel selection
[Parameter(ParameterSetName = 'ELevelFilter')]
[ValidateSet('E3', 'E5')]
[string]$ELevel,
[Parameter(ParameterSetName = 'ELevelFilter')]
[ValidateSet('L1', 'L2')]
[string]$ProfileLevel,
# IG Filters, one at a time
[Parameter(ParameterSetName = 'IG1Filter')]
[switch]$IncludeIG1,
[Parameter(ParameterSetName = 'IG2Filter')]
[switch]$IncludeIG2,
[Parameter(ParameterSetName = 'IG3Filter')]
[switch]$IncludeIG3,
# Inclusion of specific recommendation numbers
[Parameter(ParameterSetName = 'RecFilter')]
[ValidateSet(
'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', `
'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', `
'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.5.7', '8.6.1'
)]
[string[]]$IncludeRecommendation,
# Exclusion of specific recommendation numbers
[Parameter(ParameterSetName = 'SkipRecFilter')]
[ValidateSet(
'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', `
'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', `
'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.5.7', '8.6.1'
)]
[string[]]$SkipRecommendation,
# Common parameters for all parameter sets
[switch]$DoNotConnect,
[switch]$DoNotDisconnect,
[switch]$NoModuleCheck
)
Begin {
if ($script:MaximumFunctionCount -lt 8192) {
$script:MaximumFunctionCount = 8192
}
# Ensure required modules are installed
# Define the required modules and versions in a hashtable
if (!($NoModuleCheck)) {
$requiredModules = @(
@{ 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" }
)
foreach ($module in $requiredModules) {
Assert-ModuleAvailability -ModuleName $module.ModuleName -RequiredVersion $module.RequiredVersion -SubModuleName $module.SubModuleName
}
}
# Loop through each required module and assert its availability
# Establishing connections
#if (!($DoNotConnect -or $DoNotTest)) {
# Establishing connections
if (!($DoNotConnect)) {
Connect-M365Suite -TenantAdminUrl $TenantAdminUrl
}
# Load test definitions from CSV
$testDefinitionsPath = Join-Path -Path $PSScriptRoot -ChildPath "helper\TestDefinitions.csv"
$testDefinitions = Import-Csv -Path $testDefinitionsPath
# Apply filters based on parameter sets
switch ($PSCmdlet.ParameterSetName) {
'ELevelFilter' {
if ($null -ne $ELevel -and $null -ne $ProfileLevel) {
$testDefinitions = $testDefinitions | Where-Object {
$_.ELevel -eq $ELevel -and $_.ProfileLevel -eq $ProfileLevel
}
}
elseif ($null -ne $ELevel) {
$testDefinitions = $testDefinitions | Where-Object {
$_.ELevel -eq $ELevel
}
}
elseif ($null -ne $ProfileLevel) {
$testDefinitions = $testDefinitions | Where-Object {
$_.ProfileLevel -eq $ProfileLevel
}
}
}
'IG1Filter' {
$testDefinitions = $testDefinitions | Where-Object { $_.IG1 -eq 'TRUE' }
}
'IG2Filter' {
$testDefinitions = $testDefinitions | Where-Object { $_.IG2 -eq 'TRUE' }
}
'IG3Filter' {
$testDefinitions = $testDefinitions | Where-Object { $_.IG3 -eq 'TRUE' }
}
'RecFilter' {
$testDefinitions = $testDefinitions | Where-Object { $IncludeRecommendation -contains $_.Rec }
}
'SkipRecFilter' {
$testDefinitions = $testDefinitions | Where-Object { $SkipRecommendation -notcontains $_.Rec }
}
}
# End switch ($PSCmdlet.ParameterSetName)
# Determine which test files to load based on filtering
$testsToLoad = $testDefinitions.TestFileName | ForEach-Object { $_ -replace '.ps1$', '' }
# Display the tests that would be loaded if the function is called with -WhatIf
Write-Verbose "The $(($testsToLoad).count) test/s that would be loaded based on filter criteria:"
$testsToLoad | ForEach-Object { Write-Verbose " $_" }
} # End Begin
Process {
$allAuditResults = @() # Initialize a collection to hold all results
# Dynamically dot-source the test scripts
$testsFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "tests"
$testFiles = Get-ChildItem -Path $testsFolderPath -Filter "Test-*.ps1" |
Where-Object { $testsToLoad -contains $_.BaseName }
# Import the test functions
$testFiles | ForEach-Object {
Try {
. $_.FullName
}
Catch {
Write-Error "Failed to load test function $($_.Name): $_"
}
}
# Execute each test function from the prepared list
foreach ($testFunction in $testFiles) {
$functionName = $testFunction.BaseName
$functionCmd = Get-Command -Name $functionName
# Check if the test function needs DomainName parameter
$paramList = @{}
if ('DomainName' -in $functionCmd.Parameters.Keys) {
$paramList.DomainName = $DomainName
}
# Use splatting to pass parameters
if ($PSCmdlet.ShouldProcess($functionName, "Execute test")) {
Write-Host "Running $functionName..."
$result = & $functionName @paramList
# Assuming each function returns an array of CISAuditResult or a single CISAuditResult
$allAuditResults += $result
}
}
}
End {
# Return all collected audit results
return $allAuditResults
# Check if the Disconnect switch is present
if (!($DoNotDisconnect)) {
# Clean up sessions
Disconnect-M365Suite
}
}
}

View File

@@ -0,0 +1,24 @@
TOPIC
about_M365FoundationsCISReport
SHORT DESCRIPTION
Automated assessment of 50 CIS 365 Foundations v3.0.0 benchmark.
LONG DESCRIPTION
Automated assessment of 50 CIS 365 Foundations v3.0.0 benchmark.
EXAMPLES
PS C:\> {{ add examples here }}
NOTE:
Thank you to all those who contributed to this module, by writing code, sharing opinions, and provided feedback.
TROUBLESHOOTING NOTE:
Look out on the Github repository for issues and new releases.
SEE ALSO
- {{ Please add Project URI such as github }}}
KEYWORDS
{{ Add comma separated keywords here }}

View File

@@ -0,0 +1,51 @@
Index,TestFileName,Rec,ELevel,ProfileLevel,IG1,IG2,IG3
1,Test-AntiPhishingPolicy.ps1,2.1.7,E5,L1,FALSE,FALSE,TRUE
2,Test-AuditDisabledFalse.ps1,6.1.1,E3,L1,TRUE,TRUE,TRUE
3,Test-AuditLogSearch.ps1,3.1.1,E3,L1,TRUE,TRUE,TRUE
4,Test-BlockChannelEmails.ps1,8.1.2,E3,L1,FALSE,FALSE,FALSE
5,Test-BlockMailForwarding.ps1,6.2.1,E3,L1,FALSE,FALSE,FALSE
6,Test-BlockSharedMailboxSignIn.ps1,1.2.2,E3,L1,FALSE,FALSE,FALSE
7,Test-CommonAttachmentFilter.ps1,2.1.2,E3,L1,FALSE,TRUE,TRUE
8,Test-CustomerLockbox.ps1,1.3.6,E5,L2,FALSE,FALSE,FALSE
9,Test-DialInBypassLobby.ps1,8.5.4,E3,L1,FALSE,FALSE,FALSE
10,Test-DisallowInfectedFilesDownload.ps1,7.3.1,E5,L2,TRUE,TRUE,TRUE
11,Test-EnableDKIM.ps1,2.1.9,E3,L1,FALSE,TRUE,TRUE
12,Test-ExternalNoControl.ps1,8.5.7,E3,L1,FALSE,FALSE,FALSE
13,Test-ExternalSharingCalendars.ps1,1.3.3,E3,L2,FALSE,TRUE,TRUE
14,Test-GlobalAdminsCount.ps1,1.1.3,E3,L1,TRUE,TRUE,TRUE
15,Test-GuestAccessExpiration.ps1,7.2.9,E3,L1,FALSE,FALSE,FALSE
16,Test-IdentifyExternalEmail.ps1,6.2.3,E3,L1,FALSE,FALSE,FALSE
17,Test-LinkSharingRestrictions.ps1,7.2.7,E3,L1,TRUE,TRUE,TRUE
18,Test-MailboxAuditingE3.ps1,6.1.2,E3,L1,TRUE,TRUE,TRUE
19,Test-MailboxAuditingE5.ps1,6.1.3,E5,L1,TRUE,TRUE,TRUE
20,Test-MailTipsEnabled.ps1,6.5.2,E3,L2,FALSE,FALSE,FALSE
21,Test-ManagedApprovedPublicGroups.ps1,1.2.1,E3,L2,TRUE,TRUE,TRUE
22,Test-MeetingChatNoAnonymous.ps1,8.5.5,E3,L1,FALSE,FALSE,FALSE
23,Test-ModernAuthExchangeOnline.ps1,6.5.1,E3,L1,FALSE,TRUE,TRUE
24,Test-ModernAuthSharePoint.ps1,7.2.1,E3,L1,FALSE,TRUE,TRUE
25,Test-NoAnonymousMeetingJoin.ps1,8.5.1,E3,L2,FALSE,FALSE,FALSE
26,Test-NoAnonymousMeetingStart.ps1,8.5.2,E3,L1,FALSE,FALSE,FALSE
27,Test-NotifyMalwareInternal.ps1,2.1.3,E3,L1,FALSE,TRUE,TRUE
28,Test-NoWhitelistDomains.ps1,6.2.2,E3,L1,FALSE,FALSE,FALSE
29,Test-OneDriveContentRestrictions.ps1,7.2.4,E3,L2,TRUE,TRUE,TRUE
30,Test-OneDriveSyncRestrictions.ps1,7.3.2,E3,L2,FALSE,FALSE,FALSE
31,Test-OrganizersPresent.ps1,8.5.6,E3,L1,FALSE,FALSE,FALSE
32,Test-OrgOnlyBypassLobby.ps1,8.5.3,E3,L1,FALSE,FALSE,TRUE
33,Test-PasswordHashSync.ps1,5.1.8.1,E3,L1,FALSE,TRUE,TRUE
34,Test-PasswordNeverExpirePolicy.ps1,1.3.1,E3,L1,TRUE,TRUE,TRUE
35,Test-ReauthWithCode.ps1,7.2.10,E3,L1,FALSE,FALSE,FALSE
36,Test-ReportSecurityInTeams.ps1,8.6.1,E3,L1,FALSE,FALSE,FALSE
37,Test-RestrictCustomScripts.ps1,7.3.4,E3,L1,FALSE,FALSE,TRUE
38,Test-RestrictExternalSharing.ps1,7.2.3,E3,L1,TRUE,TRUE,TRUE
39,Test-RestrictOutlookAddins.ps1,6.3.1,E3,L2,FALSE,TRUE,TRUE
40,Test-RestrictStorageProvidersOutlook.ps1,6.5.3,E3,L2,TRUE,TRUE,TRUE
41,Test-RestrictTenantCreation.ps1,5.1.2.3,E3,L1,FALSE,FALSE,FALSE
42,Test-SafeAttachmentsPolicy.ps1,2.1.4,E5,L2,FALSE,FALSE,TRUE
43,Test-SafeAttachmentsTeams.ps1,2.1.5,E5,L2,TRUE,TRUE,TRUE
44,Test-SafeLinksOfficeApps.ps1,2.1.1,E5,L2,TRUE,TRUE,TRUE
45,Test-SharePointAADB2B.ps1,7.2.2,E3,L1,FALSE,FALSE,FALSE
46,Test-SharePointExternalSharingDomains.ps1,7.2.6,E3,L2,TRUE,TRUE,TRUE
47,Test-SharePointGuestsItemSharing.ps1,7.2.5,E3,L2,TRUE,TRUE,TRUE
48,Test-SpamPolicyAdminNotify.ps1,2.1.6,E3,L1,FALSE,TRUE,TRUE
49,Test-TeamsExternalAccess.ps1,8.2.1,E3,L2,FALSE,FALSE,FALSE
50,Test-TeamsExternalFileSharing.ps1,8.1.1,E3,L2,TRUE,TRUE,TRUE
1 Index TestFileName Rec ELevel ProfileLevel IG1 IG2 IG3
2 1 Test-AntiPhishingPolicy.ps1 2.1.7 E5 L1 FALSE FALSE TRUE
3 2 Test-AuditDisabledFalse.ps1 6.1.1 E3 L1 TRUE TRUE TRUE
4 3 Test-AuditLogSearch.ps1 3.1.1 E3 L1 TRUE TRUE TRUE
5 4 Test-BlockChannelEmails.ps1 8.1.2 E3 L1 FALSE FALSE FALSE
6 5 Test-BlockMailForwarding.ps1 6.2.1 E3 L1 FALSE FALSE FALSE
7 6 Test-BlockSharedMailboxSignIn.ps1 1.2.2 E3 L1 FALSE FALSE FALSE
8 7 Test-CommonAttachmentFilter.ps1 2.1.2 E3 L1 FALSE TRUE TRUE
9 8 Test-CustomerLockbox.ps1 1.3.6 E5 L2 FALSE FALSE FALSE
10 9 Test-DialInBypassLobby.ps1 8.5.4 E3 L1 FALSE FALSE FALSE
11 10 Test-DisallowInfectedFilesDownload.ps1 7.3.1 E5 L2 TRUE TRUE TRUE
12 11 Test-EnableDKIM.ps1 2.1.9 E3 L1 FALSE TRUE TRUE
13 12 Test-ExternalNoControl.ps1 8.5.7 E3 L1 FALSE FALSE FALSE
14 13 Test-ExternalSharingCalendars.ps1 1.3.3 E3 L2 FALSE TRUE TRUE
15 14 Test-GlobalAdminsCount.ps1 1.1.3 E3 L1 TRUE TRUE TRUE
16 15 Test-GuestAccessExpiration.ps1 7.2.9 E3 L1 FALSE FALSE FALSE
17 16 Test-IdentifyExternalEmail.ps1 6.2.3 E3 L1 FALSE FALSE FALSE
18 17 Test-LinkSharingRestrictions.ps1 7.2.7 E3 L1 TRUE TRUE TRUE
19 18 Test-MailboxAuditingE3.ps1 6.1.2 E3 L1 TRUE TRUE TRUE
20 19 Test-MailboxAuditingE5.ps1 6.1.3 E5 L1 TRUE TRUE TRUE
21 20 Test-MailTipsEnabled.ps1 6.5.2 E3 L2 FALSE FALSE FALSE
22 21 Test-ManagedApprovedPublicGroups.ps1 1.2.1 E3 L2 TRUE TRUE TRUE
23 22 Test-MeetingChatNoAnonymous.ps1 8.5.5 E3 L1 FALSE FALSE FALSE
24 23 Test-ModernAuthExchangeOnline.ps1 6.5.1 E3 L1 FALSE TRUE TRUE
25 24 Test-ModernAuthSharePoint.ps1 7.2.1 E3 L1 FALSE TRUE TRUE
26 25 Test-NoAnonymousMeetingJoin.ps1 8.5.1 E3 L2 FALSE FALSE FALSE
27 26 Test-NoAnonymousMeetingStart.ps1 8.5.2 E3 L1 FALSE FALSE FALSE
28 27 Test-NotifyMalwareInternal.ps1 2.1.3 E3 L1 FALSE TRUE TRUE
29 28 Test-NoWhitelistDomains.ps1 6.2.2 E3 L1 FALSE FALSE FALSE
30 29 Test-OneDriveContentRestrictions.ps1 7.2.4 E3 L2 TRUE TRUE TRUE
31 30 Test-OneDriveSyncRestrictions.ps1 7.3.2 E3 L2 FALSE FALSE FALSE
32 31 Test-OrganizersPresent.ps1 8.5.6 E3 L1 FALSE FALSE FALSE
33 32 Test-OrgOnlyBypassLobby.ps1 8.5.3 E3 L1 FALSE FALSE TRUE
34 33 Test-PasswordHashSync.ps1 5.1.8.1 E3 L1 FALSE TRUE TRUE
35 34 Test-PasswordNeverExpirePolicy.ps1 1.3.1 E3 L1 TRUE TRUE TRUE
36 35 Test-ReauthWithCode.ps1 7.2.10 E3 L1 FALSE FALSE FALSE
37 36 Test-ReportSecurityInTeams.ps1 8.6.1 E3 L1 FALSE FALSE FALSE
38 37 Test-RestrictCustomScripts.ps1 7.3.4 E3 L1 FALSE FALSE TRUE
39 38 Test-RestrictExternalSharing.ps1 7.2.3 E3 L1 TRUE TRUE TRUE
40 39 Test-RestrictOutlookAddins.ps1 6.3.1 E3 L2 FALSE TRUE TRUE
41 40 Test-RestrictStorageProvidersOutlook.ps1 6.5.3 E3 L2 TRUE TRUE TRUE
42 41 Test-RestrictTenantCreation.ps1 5.1.2.3 E3 L1 FALSE FALSE FALSE
43 42 Test-SafeAttachmentsPolicy.ps1 2.1.4 E5 L2 FALSE FALSE TRUE
44 43 Test-SafeAttachmentsTeams.ps1 2.1.5 E5 L2 TRUE TRUE TRUE
45 44 Test-SafeLinksOfficeApps.ps1 2.1.1 E5 L2 TRUE TRUE TRUE
46 45 Test-SharePointAADB2B.ps1 7.2.2 E3 L1 FALSE FALSE FALSE
47 46 Test-SharePointExternalSharingDomains.ps1 7.2.6 E3 L2 TRUE TRUE TRUE
48 47 Test-SharePointGuestsItemSharing.ps1 7.2.5 E3 L2 TRUE TRUE TRUE
49 48 Test-SpamPolicyAdminNotify.ps1 2.1.6 E3 L1 FALSE TRUE TRUE
50 49 Test-TeamsExternalAccess.ps1 8.2.1 E3 L2 FALSE FALSE FALSE
51 50 Test-TeamsExternalFileSharing.ps1 8.1.1 E3 L2 TRUE TRUE TRUE

View File

@@ -0,0 +1,59 @@
function Test-AntiPhishingPolicy {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# Retrieve and validate the anti-phishing policies
$antiPhishPolicies = Get-AntiPhishPolicy
$validatedPolicies = $antiPhishPolicies | Where-Object {
$_.Enabled -eq $true -and
$_.PhishThresholdLevel -ge 2 -and
$_.EnableMailboxIntelligenceProtection -eq $true -and
$_.EnableMailboxIntelligence -eq $true -and
$_.EnableSpoofIntelligence -eq $true
}
# Check if there is at least one policy that meets the requirements
$isCompliant = $validatedPolicies.Count -gt 0
# Prepare failure details if policies are not compliant
$failureDetails = if (-not $isCompliant) {
"No anti-phishing policy is fully compliant with CIS benchmark requirements."
} else {
"Compliant Anti-Phish Policy Names: " + ($validatedPolicies.Name -join ', ')
}
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($isCompliant) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E5"
$auditResult.ProfileLevel = "L1"
$auditResult.Rec = "2.1.7"
$auditResult.RecDescription = "Ensure that an anti-phishing policy has been created"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "9.7"
$auditResult.CISDescription = "Deploy and Maintain Email Server Anti-Malware Protections"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $true
$auditResult.Result = $isCompliant
$auditResult.Details = $failureDetails
$auditResult.FailureReason = if (-not $isCompliant) { "Anti-phishing policies do not meet CIS benchmark requirements." } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,43 @@
function Test-AuditDisabledFalse {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 6.1.1 (L1) Ensure 'AuditDisabled' organizationally is set to 'False'
# Pass if AuditDisabled is False. Fail otherwise.
$auditDisabledConfig = Get-OrganizationConfig | Select-Object AuditDisabled
$auditNotDisabled = -not $auditDisabledConfig.AuditDisabled
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($auditNotDisabled) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.Rec = "6.1.1"
$auditResult.RecDescription = "Ensure 'AuditDisabled' organizationally is set to 'False'"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "8.2"
$auditResult.CISDescription = "Collect Audit Logs"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $auditNotDisabled
$auditResult.Details = if ($auditNotDisabled) { "Audit is not disabled organizationally" } else { "Audit is disabled organizationally" }
$auditResult.FailureReason = if (-not $auditNotDisabled) { "AuditDisabled is set to True" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,43 @@
function Test-AuditLogSearch {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 3.1.1 (L1) Ensure Microsoft 365 audit log search is Enabled
# Pass if UnifiedAuditLogIngestionEnabled is True. Fail otherwise.
$auditLogConfig = Get-AdminAuditLogConfig | Select-Object UnifiedAuditLogIngestionEnabled
$auditLogResult = $auditLogConfig.UnifiedAuditLogIngestionEnabled
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($auditLogResult) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.Rec = "3.1.1"
$auditResult.RecDescription = "Ensure Microsoft 365 audit log search is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "8.2"
$auditResult.CISDescription = "Collect Audit Logs"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $auditLogResult
$auditResult.Details = "UnifiedAuditLogIngestionEnabled: $($auditLogConfig.UnifiedAuditLogIngestionEnabled)"
$auditResult.FailureReason = if (-not $auditLogResult) { "Audit log search is not enabled" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,44 @@
function Test-BlockChannelEmails {
[CmdletBinding()]
param (
# Parameters can be added here if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 8.1.2 (L1) Ensure users can't send emails to a channel email address
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$teamsClientConfig = Get-CsTeamsClientConfiguration -Identity Global
$allowEmailIntoChannel = $teamsClientConfig.AllowEmailIntoChannel
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # This control is Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.1.2"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.IG1 = $false # Set based on the benchmark
$auditResult.IG2 = $false # Set based on the benchmark
$auditResult.IG3 = $false # Set based on the benchmark
$auditResult.RecDescription = "Ensure users can't send emails to a channel email address"
$auditResult.Result = -not $allowEmailIntoChannel
$auditResult.Details = "AllowEmailIntoChannel is set to $allowEmailIntoChannel"
$auditResult.FailureReason = if ($allowEmailIntoChannel) { "Emails can be sent to a channel email address" } else { "N/A" }
$auditResult.Status = if (-not $allowEmailIntoChannel) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,51 @@
function Test-BlockMailForwarding {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
$auditResult = [CISAuditResult]::new()
$auditResult.Rec = "6.2.1"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure all forms of mail forwarding are blocked and/or disabled"
}
process {
# Verify that no rules are forwarding the email to external domains
$transportRules = Get-TransportRule | Where-Object { $_.RedirectMessageTo -ne $null }
$forwardingBlocked = $transportRules.Count -eq 0
$auditResult.Result = $forwardingBlocked
$auditResult.Details = if ($transportRules.Count -gt 0) {
$transportRules | ForEach-Object {
"$($_.Name) redirects to $($_.RedirectMessageTo)"
} -join " | "
} else {
"Step 1: No forwarding rules found. Please proceed with Step 2 described in CIS Benchmark."
}
$auditResult.FailureReason = if (-not $forwardingBlocked) {
"Mail forwarding rules found: $($transportRules.Name -join ', ')"
} else {
"N/A"
}
$auditResult.Status = if ($forwardingBlocked) { "Pass" } else { "Fail" }
}
end {
# Return the result object
return $auditResult
}
}

View File

@@ -0,0 +1,47 @@
function Test-BlockSharedMailboxSignIn {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 1.2.2 (L1) Ensure sign-in to shared mailboxes is blocked
# Pass if all shared mailboxes have AccountEnabled set to False.
# Fail if any shared mailbox has AccountEnabled set to True.
$MBX = Get-EXOMailbox -RecipientTypeDetails SharedMailbox
$sharedMailboxDetails = $MBX | ForEach-Object { Get-AzureADUser -ObjectId $_.ExternalDirectoryObjectId }
$enabledMailboxes = $sharedMailboxDetails | Where-Object { $_.AccountEnabled } | ForEach-Object { $_.DisplayName }
$allBlocked = $enabledMailboxes.Count -eq 0
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Control is explicitly not mapped
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "1.2.2"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.IG1 = $false # Control is not mapped, hence IG1 is false
$auditResult.IG2 = $false # Control is not mapped, hence IG2 is false
$auditResult.IG3 = $false # Control is not mapped, hence IG3 is false
$auditResult.RecDescription = "Ensure sign-in to shared mailboxes is blocked"
$auditResult.Result = $allBlocked
$auditResult.Details = "Enabled Mailboxes: $($enabledMailboxes -join ', ')"
$auditResult.FailureReason = if ($allBlocked) { "N/A" } else { "Some mailboxes have sign-in enabled: $($enabledMailboxes -join ', ')" }
$auditResult.Status = if ($allBlocked) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,46 @@
function Test-CommonAttachmentFilter {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
$auditResults = @()
}
process {
# 2.1.2 (L1) Ensure the Common Attachment Types Filter is enabled
# Pass if EnableFileFilter is set to True. Fail otherwise.
$attachmentFilter = Get-MalwareFilterPolicy -Identity Default | Select-Object EnableFileFilter
$result = $attachmentFilter.EnableFileFilter
$details = "File Filter Enabled: $($attachmentFilter.EnableFileFilter)"
$failureReason = if ($result) { "N/A" } else { "Common Attachment Types Filter is disabled" }
$status = if ($result) { "Pass" } else { "Fail" }
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = $status
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.Rec = "2.1.2"
$auditResult.RecDescription = "Ensure the Common Attachment Types Filter is enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "9.6"
$auditResult.CISDescription = "Block Unnecessary File Types"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $result
$auditResult.Details = $details
$auditResult.FailureReason = $failureReason
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,40 @@
function Test-CustomerLockbox {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
$auditResults = @()
}
process {
# 1.3.6 (L2) Ensure the customer lockbox feature is enabled
$orgConfig = Get-OrganizationConfig | Select-Object CustomerLockBoxEnabled
$customerLockboxEnabled = $orgConfig.CustomerLockBoxEnabled
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($customerLockboxEnabled) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E5"
$auditResult.ProfileLevel = "L2"
$auditResult.Rec = "1.3.6"
$auditResult.RecDescription = "Ensure the customer lockbox feature is enabled"
$auditResult.CISControlVer = 'v8'
$auditResult.CISControl = "0.0" # As per the snapshot provided, this is explicitly not mapped
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.Result = $customerLockboxEnabled
$auditResult.Details = "Customer Lockbox Enabled: $customerLockboxEnabled"
$auditResult.FailureReason = if ($customerLockboxEnabled) { "N/A" } else { "Customer lockbox feature is not enabled." }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,45 @@
function Test-DialInBypassLobby {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 8.5.4 (L1) Ensure users dialing in can't bypass the lobby
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$CsTeamsMeetingPolicyPSTN = Get-CsTeamsMeetingPolicy -Identity Global | Select-Object -Property AllowPSTNUsersToBypassLobby
$PSTNBypassDisabled = -not $CsTeamsMeetingPolicyPSTN.AllowPSTNUsersToBypassLobby
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.5.4"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure users dialing in can't bypass the lobby"
$auditResult.Result = $PSTNBypassDisabled
$auditResult.Details = "AllowPSTNUsersToBypassLobby is set to $($CsTeamsMeetingPolicyPSTN.AllowPSTNUsersToBypassLobby)"
$auditResult.FailureReason = if ($PSTNBypassDisabled) { "N/A" } else { "Users dialing in can bypass the lobby" }
$auditResult.Status = if ($PSTNBypassDisabled) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,41 @@
function Test-DisallowInfectedFilesDownload {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
$auditResult = [CISAuditResult]::new()
}
process {
# 7.3.1 (L2) Ensure Office 365 SharePoint infected files are disallowed for download
$SPOTenantDisallowInfectedFileDownload = Get-SPOTenant | Select-Object DisallowInfectedFileDownload
$isDisallowInfectedFileDownloadEnabled = $SPOTenantDisallowInfectedFileDownload.DisallowInfectedFileDownload
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "10.1"
$auditResult.CISDescription = "Deploy and Maintain Anti-Malware Software"
$auditResult.Rec = "7.3.1"
$auditResult.ELevel = "E5"
$auditResult.ProfileLevel = "L2"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure Office 365 SharePoint infected files are disallowed for download"
$auditResult.Result = $isDisallowInfectedFileDownloadEnabled
$auditResult.Details = "DisallowInfectedFileDownload: $($SPOTenantDisallowInfectedFileDownload.DisallowInfectedFileDownload)"
$auditResult.FailureReason = if (-not $isDisallowInfectedFileDownloadEnabled) { "Downloading infected files is not disallowed." } else { "N/A" }
$auditResult.Status = if ($isDisallowInfectedFileDownloadEnabled) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,44 @@
function Test-EnableDKIM {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 2.1.9 (L1) Ensure DKIM is enabled for all Exchange Online Domains
# Pass if Enabled is True for all domains. Fail if any domain has Enabled set to False.
$dkimConfig = Get-DkimSigningConfig | Select-Object Domain, Enabled
$dkimResult = ($dkimConfig | ForEach-Object { $_.Enabled }) -notcontains $false
$dkimFailedDomains = $dkimConfig | Where-Object { -not $_.Enabled } | ForEach-Object { $_.Domain }
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($dkimResult) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.Rec = "2.1.9"
$auditResult.RecDescription = "Ensure that DKIM is enabled for all Exchange Online Domains"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "9.5"
$auditResult.CISDescription = "Implement DMARC"
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.Result = $dkimResult
$auditResult.Details = if (-not $dkimResult) { "DKIM not enabled for: $($dkimFailedDomains -join ', ')" } else { "All domains have DKIM enabled" }
$auditResult.FailureReason = if (-not $dkimResult) { "DKIM is not enabled for some domains" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,45 @@
function Test-ExternalNoControl {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 8.5.7 (L1) Ensure external participants can't give or request control
# Connect to Teams PowerShell using Connect-MicrosoftTeams
$CsTeamsMeetingPolicyControl = Get-CsTeamsMeetingPolicy -Identity Global | Select-Object -Property AllowExternalParticipantGiveRequestControl
$externalControlRestricted = -not $CsTeamsMeetingPolicyControl.AllowExternalParticipantGiveRequestControl
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0" # Explicitly Not Mapped as per the image provided
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "8.5.7"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.IG1 = $false # Set based on the CIS Controls image
$auditResult.IG2 = $false # Set based on the CIS Controls image
$auditResult.IG3 = $false # Set based on the CIS Controls image
$auditResult.RecDescription = "Ensure external participants can't give or request control"
$auditResult.Result = $externalControlRestricted
$auditResult.Details = "AllowExternalParticipantGiveRequestControl is set to $($CsTeamsMeetingPolicyControl.AllowExternalParticipantGiveRequestControl)"
$auditResult.FailureReason = if ($externalControlRestricted) { "N/A" } else { "External participants can give or request control" }
$auditResult.Status = if ($externalControlRestricted) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,53 @@
function Test-ExternalSharingCalendars {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 1.3.3 (L2) Ensure 'External sharing' of calendars is not available (Automated)
$sharingPolicies = Get-SharingPolicy | Where-Object { $_.Domains -like '*CalendarSharing*' }
# Check if calendar sharing is disabled in all applicable policies
$isExternalSharingDisabled = $true
$sharingPolicyDetails = @()
foreach ($policy in $sharingPolicies) {
if ($policy.Enabled -eq $true) {
$isExternalSharingDisabled = $false
$sharingPolicyDetails += "$($policy.Name): Enabled"
}
}
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Rec = "1.3.3"
$auditResult.RecDescription = "Ensure 'External sharing' of calendars is not available"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L2"
# The following IG values are placeholders. Replace with actual values when known.
$auditResult.IG1 = $false
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.CISControlVer = "v8"
# Placeholder for CIS Control, to be replaced with the actual value when available
$auditResult.CISControl = "4.8"
$auditResult.CISDescription = "Uninstall or Disable Unnecessary Services on Enterprise Assets and Software"
$auditResult.Result = $isExternalSharingDisabled
$auditResult.Details = if ($isExternalSharingDisabled) { "Calendar sharing with external users is disabled." } else { "Enabled Sharing Policies: $($sharingPolicyDetails -join ', ')" }
$auditResult.FailureReason = if ($isExternalSharingDisabled) { "N/A" } else { "Calendar sharing with external users is enabled in one or more policies." }
$auditResult.Status = if ($isExternalSharingDisabled) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,46 @@
function Test-GlobalAdminsCount {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 1.1.3 (L1) Ensure that between two and four global admins are designated
# Pass if the count of global admins is between 2 and 4. Fail otherwise.
$globalAdminRole = Get-MgDirectoryRole -Filter "RoleTemplateId eq '62e90394-69f5-4237-9190-012177145e10'"
$globalAdmins = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id
$globalAdminCount = $globalAdmins.AdditionalProperties.Count
$globalAdminUsernames = ($globalAdmins | ForEach-Object { $_.AdditionalProperties["displayName"] }) -join ', '
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "5.1"
$auditResult.CISDescription = "Establish and Maintain an Inventory of Accounts"
$auditResult.Rec = "1.1.3"
$auditResult.ELevel = "E3" # Based on your environment (E3, E5, etc.)
$auditResult.ProfileLevel = "L1"
$auditResult.IG1 = $true # Set based on the benchmark
$auditResult.IG2 = $true # Set based on the benchmark
$auditResult.IG3 = $true # Set based on the benchmark
$auditResult.RecDescription = "Ensure that between two and four global admins are designated"
$auditResult.Result = $globalAdminCount -ge 2 -and $globalAdminCount -le 4
$auditResult.Details = "Count: $globalAdminCount; Users: $globalAdminUsernames"
$auditResult.FailureReason = if ($globalAdminCount -lt 2) { "Less than 2 global admins: $globalAdminUsernames" } elseif ($globalAdminCount -gt 4) { "More than 4 global admins: $globalAdminUsernames" } else { "N/A" }
$auditResult.Status = if ($globalAdminCount -ge 2 -and $globalAdminCount -le 4) { "Pass" } else { "Fail" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,41 @@
function Test-GuestAccessExpiration {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.9 (L1) Ensure guest access to a site or OneDrive will expire automatically
$SPOTenantGuestAccess = Get-SPOTenant | Select-Object ExternalUserExpirationRequired, ExternalUserExpireInDays
$isGuestAccessExpirationConfiguredCorrectly = $SPOTenantGuestAccess.ExternalUserExpirationRequired -and $SPOTenantGuestAccess.ExternalUserExpireInDays -le 30
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "7.2.9"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure guest access to a site or OneDrive will expire automatically"
$auditResult.Result = $isGuestAccessExpirationConfiguredCorrectly
$auditResult.Details = "ExternalUserExpirationRequired: $($SPOTenantGuestAccess.ExternalUserExpirationRequired); ExternalUserExpireInDays: $($SPOTenantGuestAccess.ExternalUserExpireInDays)"
$auditResult.FailureReason = if (-not $isGuestAccessExpirationConfiguredCorrectly) { "Guest access expiration is not configured to automatically expire within 30 days or less." } else { "N/A" }
$auditResult.Status = if ($isGuestAccessExpirationConfiguredCorrectly) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,44 @@
function Test-IdentifyExternalEmail {
[CmdletBinding()]
param (
# Parameters can be defined here if needed
)
begin {
# Dot source the class script
$auditResults = @()
}
process {
# 6.2.3 (L1) Ensure email from external senders is identified
# Requirement is to have external sender tagging enabled
$externalInOutlook = Get-ExternalInOutlook
$externalTaggingEnabled = ($externalInOutlook | ForEach-Object { $_.Enabled }) -contains $true
# Create an instance of CISAuditResult and populate it
$auditResult = [CISAuditResult]::new()
$auditResult.Status = if ($externalTaggingEnabled) { "Pass" } else { "Fail" }
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.Rec = "6.2.3"
$auditResult.RecDescription = "Ensure email from external senders is identified"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "0.0"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.Result = $externalTaggingEnabled
$auditResult.Details = "Enabled: $($externalTaggingEnabled); AllowList: $($externalInOutlook.AllowList)"
$auditResult.FailureReason = if (-not $externalTaggingEnabled) { "External sender tagging is disabled" } else { "N/A" }
$auditResults += $auditResult
}
end {
# Return auditResults
return $auditResults
}
}

View File

@@ -0,0 +1,42 @@
function Test-LinkSharingRestrictions {
[CmdletBinding()]
param (
# Define your parameters here
# Test behavior in prod
)
begin {
# Initialization code
$auditResult = [CISAuditResult]::new()
}
process {
# 7.2.7 (L1) Ensure link sharing is restricted in SharePoint and OneDrive
$SPOTenantLinkSharing = Get-SPOTenant | Select-Object DefaultSharingLinkType
$isLinkSharingRestricted = $SPOTenantLinkSharing.DefaultSharingLinkType -eq 'Direct' # Or 'SpecificPeople' as per the recommendation
# Populate the auditResult object with the required properties
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "3.3"
$auditResult.CISDescription = "Configure Data Access Control Lists"
$auditResult.Rec = "7.2.7"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$auditResult.RecDescription = "Ensure link sharing is restricted in SharePoint and OneDrive"
$auditResult.Result = $isLinkSharingRestricted
$auditResult.Details = "DefaultSharingLinkType: $($SPOTenantLinkSharing.DefaultSharingLinkType)"
$auditResult.FailureReason = if (-not $isLinkSharingRestricted) { "Link sharing is not restricted to 'Specific people'. Current setting: $($SPOTenantLinkSharing.DefaultSharingLinkType)" } else { "N/A" }
$auditResult.Status = if ($isLinkSharingRestricted) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,43 @@
function Test-MailTipsEnabled {
[CmdletBinding()]
param (
# Define your parameters here
)
begin {
# Initialization code
$auditResult = [CISAuditResult]::new()
}
process {
# 6.5.2 (L2) Ensure MailTips are enabled for end users
$orgConfig = Get-OrganizationConfig | Select-Object MailTipsAllTipsEnabled, MailTipsExternalRecipientsTipsEnabled, MailTipsGroupMetricsEnabled, MailTipsLargeAudienceThreshold
$allTipsEnabled = $orgConfig.MailTipsAllTipsEnabled -and $orgConfig.MailTipsGroupMetricsEnabled -and $orgConfig.MailTipsLargeAudienceThreshold -eq 25
$externalRecipientsTipsEnabled = $orgConfig.MailTipsExternalRecipientsTipsEnabled
# Since there is no direct CIS Control mapping, the control will be set as not applicable.
$auditResult.CISControl = "0"
$auditResult.CISControlVer = "v8"
$auditResult.CISDescription = "Explicitly Not Mapped"
$auditResult.Rec = "6.5.2"
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L2"
$auditResult.IG1 = $false
$auditResult.IG2 = $false
$auditResult.IG3 = $false
$auditResult.RecDescription = "Ensure MailTips are enabled for end users"
$auditResult.Result = $allTipsEnabled -and $externalRecipientsTipsEnabled
$auditResult.Details = "MailTipsAllTipsEnabled: $($orgConfig.MailTipsAllTipsEnabled); MailTipsExternalRecipientsTipsEnabled: $($orgConfig.MailTipsExternalRecipientsTipsEnabled); MailTipsGroupMetricsEnabled: $($orgConfig.MailTipsGroupMetricsEnabled); MailTipsLargeAudienceThreshold: $($orgConfig.MailTipsLargeAudienceThreshold)"
$auditResult.FailureReason = if (-not $auditResult.Result) { "One or more MailTips settings are not configured as required." } else { "N/A" }
$auditResult.Status = if ($auditResult.Result) { "Pass" } else { "Fail" }
}
end {
# Return auditResult
return $auditResult
}
}

View File

@@ -0,0 +1,111 @@
function Test-MailboxAuditingE3 {
[CmdletBinding()]
param (
# Parameters can be added if needed
)
begin {
$e3SkuPartNumbers = @("ENTERPRISEPACK", "OFFICESUBSCRIPTION")
$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")
$auditResult = [CISAuditResult]::new()
$auditResult.ELevel = "E3"
$auditResult.ProfileLevel = "L1"
$auditResult.Rec = "6.1.2"
$auditResult.RecDescription = "Ensure mailbox auditing for Office E3 users is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "8.2"
$auditResult.CISDescription = "Collect audit logs."
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$allFailures = @()
$allUsers = Get-AzureADUser -All $true
$processedUsers = @{} # Dictionary to track processed users
}
process {
foreach ($user in $allUsers) {
if ($processedUsers.ContainsKey($user.UserPrincipalName)) {
Write-Verbose "Skipping already processed user: $($user.UserPrincipalName)"
continue
}
try {
$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."
if ($hasOfficeE3) {
$userUPN = $user.UserPrincipalName
$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit
$missingActions = @()
if ($mailbox.AuditEnabled) {
foreach ($action in $AdminActions) {
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" }
}
foreach ($action in $DelegateActions) {
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" }
}
foreach ($action in $OwnerActions) {
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" }
}
}
else {
$allFailures += "$userUPN`: AuditEnabled - False"
continue
}
if ($missingActions) {
$formattedActions = Format-MissingActions $missingActions
$allFailures += "$userUPN`: AuditEnabled - True; $formattedActions"
}
# Mark the user as processed
$processedUsers[$user.UserPrincipalName] = $true
}
}
catch {
Write-Warning "Could not retrieve license details for user $($user.UserPrincipalName): $_"
}
}
$auditResult.Result = $allFailures.Count -eq 0
$auditResult.Status = if ($auditResult.Result) { "Pass" } else { "Fail" }
$auditResult.Details = if ($auditResult.Result) { "All Office E3 users have correct mailbox audit settings." } else { $allFailures -join " | " }
$auditResult.FailureReason = if (-not $auditResult.Result) { "Audit issues detected." } else { "N/A" }
}
end {
return $auditResult
}
}
function Format-MissingActions {
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 = @()
foreach ($type in $actionGroups.Keys) {
if ($actionGroups[$type].Count -gt 0) {
$formattedResults += "$($type) actions missing: $($actionGroups[$type] -join ', ')"
}
}
return $formattedResults -join '; '
}

View File

@@ -0,0 +1,120 @@
function Test-MailboxAuditingE5 {
[CmdletBinding()]
param ()
begin {
$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")
$auditResult = [CISAuditResult]::new()
$auditResult.ELevel = "E5"
$auditResult.ProfileLevel = "L1"
$auditResult.Rec = "6.1.3"
$auditResult.RecDescription = "Ensure mailbox auditing for Office E5 users is Enabled"
$auditResult.CISControlVer = "v8"
$auditResult.CISControl = "8.2"
$auditResult.CISDescription = "Collect audit logs."
$auditResult.IG1 = $true
$auditResult.IG2 = $true
$auditResult.IG3 = $true
$allFailures = @()
$allUsers = Get-AzureADUser -All $true
$processedUsers = @{} # Dictionary to track processed users
}
process {
foreach ($user in $allUsers) {
if ($processedUsers.ContainsKey($user.UserPrincipalName)) {
continue
}
try {
# Define SKU Part Numbers for Office E5 licenses
# Define SKU Part Numbers for Office E5 licenses
$e5SkuPartNumbers = @("SPE_E5", "ENTERPRISEPREMIUM", "OFFICEE5")
$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."
if ($hasOfficeE5) {
$userUPN = $user.UserPrincipalName
$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit
$missingActions = @()
if ($mailbox.AuditEnabled) {
foreach ($action in $AdminActions) {
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" }
}
foreach ($action in $DelegateActions) {
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" }
}
foreach ($action in $OwnerActions) {
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" }
}
}
else {
$allFailures += "$userUPN`: AuditEnabled - False"
continue
}
if ($missingActions) {
$formattedActions = Format-MissingActions $missingActions
$allFailures += "$userUPN`: AuditEnabled - True; $formattedActions"
}
else {
Write-Verbose "User $($user.UserPrincipalName) passed the mailbox audit checks."
}
$processedUsers[$user.UserPrincipalName] = $true
}
else {
# Adding verbose output to indicate the user does not have an E5 license
Write-Verbose "User $($user.UserPrincipalName) does not have an Office E5 license."
}
}
catch {
Write-Warning "Could not retrieve license details for user $($user.UserPrincipalName): $_"
}
}
if ($allFailures.Count -eq 0) {
Write-Verbose "All evaluated E5 users have correct mailbox audit settings."
}
$auditResult.Result = $allFailures.Count -eq 0
$auditResult.Status = if ($auditResult.Result) { "Pass" } else { "Fail" }
$auditResult.Details = if ($auditResult.Result) { "All Office E5 users have correct mailbox audit settings." } else { $allFailures -join " | " }
$auditResult.FailureReason = if (-not $auditResult.Result) { "Audit issues detected." } else { "N/A" }
}
end {
return $auditResult
}
}
function Format-MissingActions {
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 = @()
foreach ($type in $actionGroups.Keys) {
if ($actionGroups[$type].Count -gt 0) {
$formattedResults += "$($type) actions missing: $($actionGroups[$type] -join ', ')"
}
}
return $formattedResults -join '; '
}

Some files were not shown because too many files have changed in this diff Show More