Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
91bb61b317 | ||
|
3ecd8bb8af | ||
|
a6720dbc5e | ||
|
b2eaee54e1 | ||
|
0125d4261d | ||
|
0c28009498 | ||
|
b78cb17bc1 | ||
|
3e5f9b3ac5 | ||
|
07bd30a27f | ||
|
b07344bb71 | ||
|
d6c500f953 | ||
|
aa76de6649 | ||
|
daadad391e | ||
|
a97eda1662 | ||
|
99933f7655 | ||
|
411ee5d36f | ||
|
4dc996b2fb | ||
|
5e25d6ee1b | ||
|
a88535e258 | ||
|
a43485f05e | ||
|
486e053dfb | ||
|
6bace63c62 | ||
|
46d71900ce | ||
|
51edc331ab | ||
|
04e63f72fc | ||
|
9b624680fd | ||
|
bbc74494c3 | ||
|
54a369bde3 | ||
|
06cdb4d0d1 | ||
|
2d4593f207 | ||
|
949a2aaa43 | ||
|
3aef8a0ca3 | ||
|
14d33493b0 | ||
|
234f0cdd31 | ||
|
e1cc2a3da7 | ||
|
848438c33f | ||
|
f981e59b43 | ||
|
919d6cdd08 | ||
|
3211ebc089 | ||
|
7b37621917 | ||
|
6752e56be9 | ||
|
c4b2427539 | ||
|
c2cc980a91 | ||
|
4b3e448e48 | ||
|
342d0ac4a9 | ||
|
d4252a1839 | ||
|
1fde9947e0 | ||
|
da856b96e4 | ||
|
8835ddfbfd | ||
|
9a7de2f549 | ||
|
c9940c2a09 | ||
|
83332207b4 | ||
|
ccacf76e6c | ||
|
273630839e | ||
|
3ca779650e | ||
|
0cde0ae5e2 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,4 +15,5 @@ output/
|
|||||||
markdownissues.txt
|
markdownissues.txt
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
Aligned.xlsx
|
Aligned.xlsx
|
||||||
|
test-gh1.ps1
|
36
CHANGELOG.md
36
CHANGELOG.md
@@ -6,6 +6,42 @@ The format is based on and uses the types of changes according to [Keep a Change
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added tenant output to connect function.
|
||||||
|
- Added skip tenant connection confirmation to main function.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed comment examples for `Export-M365SecurityAuditTable`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated `Sync-CISExcelAndCsvData` to be one function.
|
||||||
|
|
||||||
|
## [0.1.12] - 2024-06-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `Export-M365SecurityAuditTable` public function to export applicable audit results to a table format.
|
||||||
|
- Added paramter to `Export-M365SecurityAuditTable` to specify output of the original audit results.
|
||||||
|
- Added `Remove-RowsWithEmptyCSVStatus` public function to remove rows with empty status from the CSV file.
|
||||||
|
- Added `Get-Action` private function to retrieve the action for the test 6.1.2 and 6.1.3 tests.
|
||||||
|
- Added output modifications to tests that produce tables to ensure they can be exported with the new `Export-M365SecurityAuditTable` function.
|
||||||
|
|
||||||
|
## [0.1.11] - 2024-06-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added Get-MFAStatus function to help with auditing mfa for conditional access controls.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed 6.1.2/6.1.3 tests to minimize calls to the Graph API.
|
||||||
|
- Fixed 2.1.1,2.1.4,2.1.5 to suppress error messages and create a standard object when no e5"
|
||||||
|
|
||||||
|
## [0.1.10] - 2024-06-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
- Added condition comments to each test.
|
- Added condition comments to each test.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
BIN
docs/index.html
BIN
docs/index.html
Binary file not shown.
@@ -4,7 +4,7 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1
|
|||||||
|
|
||||||
|
|
||||||
<#
|
<#
|
||||||
$ver = "v0.1.9"
|
$ver = "v0.1.12"
|
||||||
git checkout main
|
git checkout main
|
||||||
git pull origin main
|
git pull origin main
|
||||||
git tag -a $ver -m "Release version $ver refactor Update"
|
git tag -a $ver -m "Release version $ver refactor Update"
|
||||||
@@ -13,73 +13,3 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1
|
|||||||
git push origin $ver
|
git push origin $ver
|
||||||
# git tag -d $ver
|
# git tag -d $ver
|
||||||
#>
|
#>
|
||||||
|
|
||||||
# Refresh authentication to ensure the correct scopes
|
|
||||||
gh auth refresh -s project,read:project,write:project,repo
|
|
||||||
|
|
||||||
# Create the project
|
|
||||||
gh project create --owner CriticalSolutionsNetwork --title "Test Validation Project"
|
|
||||||
|
|
||||||
$repoOwner = "CriticalSolutionsNetwork"
|
|
||||||
$repoName = "M365FoundationsCISReport"
|
|
||||||
$directoryPath = ".\source\tests"
|
|
||||||
$projectName = "Test Validation Project"
|
|
||||||
|
|
||||||
# Function to create GitHub issues
|
|
||||||
function Create-GitHubIssue {
|
|
||||||
param (
|
|
||||||
[string]$title,
|
|
||||||
[string]$body,
|
|
||||||
[string]$project
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the issue and add it to the specified project
|
|
||||||
$issue = gh issue create --repo "$repoOwner/$repoName" --title "$title" --body "$body" --project "$project"
|
|
||||||
return $issue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load test definitions from CSV
|
|
||||||
$testDefinitionsPath = ".\source\helper\TestDefinitions.csv"
|
|
||||||
$testDefinitions = Import-Csv -Path $testDefinitionsPath
|
|
||||||
|
|
||||||
# Iterate over each .ps1 file in the directory
|
|
||||||
Get-ChildItem -Path $directoryPath -Filter "*.ps1" | ForEach-Object {
|
|
||||||
$fileName = $_.Name
|
|
||||||
$testDefinition = $testDefinitions | Where-Object { $_.TestFileName -eq $fileName }
|
|
||||||
|
|
||||||
if ($testDefinition) {
|
|
||||||
$rec = $testDefinition.Rec
|
|
||||||
$elevel = $testDefinition.ELevel
|
|
||||||
$profileLevel = $testDefinition.ProfileLevel
|
|
||||||
$ig1 = $testDefinition.IG1
|
|
||||||
$ig2 = $testDefinition.IG2
|
|
||||||
$ig3 = $testDefinition.IG3
|
|
||||||
$connection = $testDefinition.Connection
|
|
||||||
|
|
||||||
$issueTitle = "Rec: $rec - Validate $fileName, ELevel: $elevel, ProfileLevel: $profileLevel, IG1: $ig1, IG2: $ig2, IG3: $ig3, Connection: $connection"
|
|
||||||
$issueBody = @"
|
|
||||||
# Validation for $fileName
|
|
||||||
|
|
||||||
## Tasks
|
|
||||||
- [ ] Validate test for a pass
|
|
||||||
- Description of passing criteria:
|
|
||||||
- [ ] Validate test for a fail
|
|
||||||
- Description of failing criteria:
|
|
||||||
- [ ] Add notes and observations
|
|
||||||
- Placeholder for additional notes:
|
|
||||||
"@
|
|
||||||
|
|
||||||
# Create the issue using GitHub CLI
|
|
||||||
try {
|
|
||||||
Create-GitHubIssue -title "$issueTitle" -body "$issueBody" -project "$projectName"
|
|
||||||
Write-Output "Created issue for $fileName"
|
|
||||||
} catch {
|
|
||||||
Write-Error "Failed to create issue for $fileName : $_"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Introduce a delay of 2 seconds
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
} else {
|
|
||||||
Write-Warning "No matching test definition found for $fileName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -2,52 +2,116 @@ function Connect-M365Suite {
|
|||||||
[OutputType([void])]
|
[OutputType([void])]
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param (
|
param (
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[string]$TenantAdminUrl,
|
[string]$TenantAdminUrl,
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string[]]$RequiredConnections
|
[string[]]$RequiredConnections,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[switch]$SkipConfirmation
|
||||||
)
|
)
|
||||||
|
|
||||||
$VerbosePreference = "SilentlyContinue"
|
$VerbosePreference = "SilentlyContinue"
|
||||||
|
$tenantInfo = @()
|
||||||
|
$connectedServices = @()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($RequiredConnections -contains "AzureAD" -or $RequiredConnections -contains "AzureAD | EXO" -or $RequiredConnections -contains "AzureAD | EXO | Microsoft Graph") {
|
if ($RequiredConnections -contains "AzureAD" -or $RequiredConnections -contains "AzureAD | EXO" -or $RequiredConnections -contains "AzureAD | EXO | Microsoft Graph") {
|
||||||
Write-Host "Connecting to Azure Active Directory..." -ForegroundColor Cyan
|
Write-Host "Connecting to Azure Active Directory..." -ForegroundColor Cyan
|
||||||
Connect-AzureAD | Out-Null
|
Connect-AzureAD | Out-Null
|
||||||
|
$tenantDetails = Get-AzureADTenantDetail
|
||||||
|
$tenantInfo += [PSCustomObject]@{
|
||||||
|
Service = "Azure Active Directory"
|
||||||
|
TenantName = $tenantDetails.DisplayName
|
||||||
|
TenantID = $tenantDetails.ObjectId
|
||||||
|
}
|
||||||
|
$connectedServices += "AzureAD"
|
||||||
Write-Host "Successfully connected to Azure Active Directory." -ForegroundColor Green
|
Write-Host "Successfully connected to Azure Active Directory." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($RequiredConnections -contains "Microsoft Graph" -or $RequiredConnections -contains "AzureAD | EXO | Microsoft Graph") {
|
if ($RequiredConnections -contains "Microsoft Graph" -or $RequiredConnections -contains "EXO | Microsoft Graph") {
|
||||||
Write-Host "Connecting to Microsoft Graph with scopes: Directory.Read.All, Domain.Read.All, Policy.Read.All, Organization.Read.All" -ForegroundColor Cyan
|
Write-Host "Connecting to Microsoft Graph with scopes: Directory.Read.All, Domain.Read.All, Policy.Read.All, Organization.Read.All" -ForegroundColor Cyan
|
||||||
try {
|
try {
|
||||||
Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -NoWelcome | Out-Null
|
Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -NoWelcome | Out-Null
|
||||||
|
$graphOrgDetails = Get-MgOrganization
|
||||||
|
$tenantInfo += [PSCustomObject]@{
|
||||||
|
Service = "Microsoft Graph"
|
||||||
|
TenantName = $graphOrgDetails.DisplayName
|
||||||
|
TenantID = $graphOrgDetails.Id
|
||||||
|
}
|
||||||
|
$connectedServices += "Microsoft Graph"
|
||||||
Write-Host "Successfully connected to Microsoft Graph with specified scopes." -ForegroundColor Green
|
Write-Host "Successfully connected to Microsoft Graph with specified scopes." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Host "Failed to connect to MgGraph, attempting device auth." -ForegroundColor Yellow
|
Write-Host "Failed to connect to MgGraph, attempting device auth." -ForegroundColor Yellow
|
||||||
Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -UseDeviceCode -NoWelcome | Out-Null
|
Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -UseDeviceCode -NoWelcome | Out-Null
|
||||||
|
$graphOrgDetails = Get-MgOrganization
|
||||||
|
$tenantInfo += [PSCustomObject]@{
|
||||||
|
Service = "Microsoft Graph"
|
||||||
|
TenantName = $graphOrgDetails.DisplayName
|
||||||
|
TenantID = $graphOrgDetails.Id
|
||||||
|
}
|
||||||
|
$connectedServices += "Microsoft Graph"
|
||||||
Write-Host "Successfully connected to Microsoft Graph with specified scopes." -ForegroundColor Green
|
Write-Host "Successfully connected to Microsoft Graph with specified scopes." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($RequiredConnections -contains "EXO" -or $RequiredConnections -contains "AzureAD | EXO" -or $RequiredConnections -contains "Microsoft Teams | EXO" -or $RequiredConnections -contains "AzureAD | EXO | Microsoft Graph") {
|
if ($RequiredConnections -contains "EXO" -or $RequiredConnections -contains "AzureAD | EXO" -or $RequiredConnections -contains "Microsoft Teams | EXO" -or $RequiredConnections -contains "EXO | Microsoft Graph") {
|
||||||
Write-Host "Connecting to Exchange Online..." -ForegroundColor Cyan
|
Write-Host "Connecting to Exchange Online..." -ForegroundColor Cyan
|
||||||
Connect-ExchangeOnline | Out-Null
|
Connect-ExchangeOnline | Out-Null
|
||||||
|
$exoTenant = (Get-OrganizationConfig).Identity
|
||||||
|
$tenantInfo += [PSCustomObject]@{
|
||||||
|
Service = "Exchange Online"
|
||||||
|
TenantName = $exoTenant
|
||||||
|
TenantID = "N/A"
|
||||||
|
}
|
||||||
|
$connectedServices += "EXO"
|
||||||
Write-Host "Successfully connected to Exchange Online." -ForegroundColor Green
|
Write-Host "Successfully connected to Exchange Online." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($RequiredConnections -contains "SPO") {
|
if ($RequiredConnections -contains "SPO") {
|
||||||
Write-Host "Connecting to SharePoint Online..." -ForegroundColor Cyan
|
Write-Host "Connecting to SharePoint Online..." -ForegroundColor Cyan
|
||||||
Connect-SPOService -Url $TenantAdminUrl | Out-Null
|
Connect-SPOService -Url $TenantAdminUrl | Out-Null
|
||||||
|
$spoContext = Get-SPOSite -Limit 1
|
||||||
|
$tenantInfo += [PSCustomObject]@{
|
||||||
|
Service = "SharePoint Online"
|
||||||
|
TenantName = $spoContext.Url
|
||||||
|
TenantID = $spoContext.GroupId
|
||||||
|
}
|
||||||
|
$connectedServices += "SPO"
|
||||||
Write-Host "Successfully connected to SharePoint Online." -ForegroundColor Green
|
Write-Host "Successfully connected to SharePoint Online." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($RequiredConnections -contains "Microsoft Teams" -or $RequiredConnections -contains "Microsoft Teams | EXO") {
|
if ($RequiredConnections -contains "Microsoft Teams" -or $RequiredConnections -contains "Microsoft Teams | EXO") {
|
||||||
Write-Host "Connecting to Microsoft Teams..." -ForegroundColor Cyan
|
Write-Host "Connecting to Microsoft Teams..." -ForegroundColor Cyan
|
||||||
Connect-MicrosoftTeams | Out-Null
|
Connect-MicrosoftTeams | Out-Null
|
||||||
|
$teamsTenantDetails = Get-CsTenant
|
||||||
|
$tenantInfo += [PSCustomObject]@{
|
||||||
|
Service = "Microsoft Teams"
|
||||||
|
TenantName = $teamsTenantDetails.DisplayName
|
||||||
|
TenantID = $teamsTenantDetails.TenantId
|
||||||
|
}
|
||||||
|
$connectedServices += "Microsoft Teams"
|
||||||
Write-Host "Successfully connected to Microsoft Teams." -ForegroundColor Green
|
Write-Host "Successfully connected to Microsoft Teams." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Display tenant information and confirm with the user
|
||||||
|
if (-not $SkipConfirmation) {
|
||||||
|
Write-Host "Connected to the following tenants:" -ForegroundColor Yellow
|
||||||
|
foreach ($tenant in $tenantInfo) {
|
||||||
|
Write-Host "Service: $($tenant.Service)" -ForegroundColor Cyan
|
||||||
|
Write-Host "Tenant Name: $($tenant.TenantName)" -ForegroundColor Green
|
||||||
|
#Write-Host "Tenant ID: $($tenant.TenantID)"
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
$confirmation = Read-Host "Do you want to proceed with these connections? (Y/N)"
|
||||||
|
if ($confirmation -notlike 'Y') {
|
||||||
|
Write-Host "Connection setup aborted by user." -ForegroundColor Red
|
||||||
|
Disconnect-M365Suite -RequiredConnections $connectedServices
|
||||||
|
throw "User aborted connection setup."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
$VerbosePreference = "Continue"
|
$VerbosePreference = "Continue"
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
function Format-MissingAction {
|
|
||||||
[CmdletBinding()]
|
|
||||||
[OutputType([hashtable])]
|
|
||||||
param (
|
|
||||||
[array]$missingActions
|
|
||||||
)
|
|
||||||
|
|
||||||
$actionGroups = @{
|
|
||||||
"Admin" = @()
|
|
||||||
"Delegate" = @()
|
|
||||||
"Owner" = @()
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($action in $missingActions) {
|
|
||||||
if ($action -match "(Admin|Delegate|Owner) action '([^']+)' missing") {
|
|
||||||
$type = $matches[1]
|
|
||||||
$actionName = $matches[2]
|
|
||||||
$actionGroups[$type] += $actionName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$formattedResults = @{
|
|
||||||
Admin = $actionGroups["Admin"] -join ', '
|
|
||||||
Delegate = $actionGroups["Delegate"] -join ', '
|
|
||||||
Owner = $actionGroups["Owner"] -join ', '
|
|
||||||
}
|
|
||||||
|
|
||||||
return $formattedResults
|
|
||||||
}
|
|
113
source/Private/Get-Action.ps1
Normal file
113
source/Private/Get-Action.ps1
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
function Get-Action {
|
||||||
|
[CmdletBinding(DefaultParameterSetName = "GetDictionaries")]
|
||||||
|
param (
|
||||||
|
[Parameter(Position = 0, ParameterSetName = "GetDictionaries")]
|
||||||
|
[switch]$Dictionaries,
|
||||||
|
|
||||||
|
[Parameter(Position = 0, ParameterSetName = "ConvertActions")]
|
||||||
|
[string[]]$Actions,
|
||||||
|
|
||||||
|
[Parameter(Position = 1, Mandatory = $true, ParameterSetName = "ConvertActions")]
|
||||||
|
[ValidateSet("Admin", "Delegate", "Owner")]
|
||||||
|
[string]$ActionType,
|
||||||
|
|
||||||
|
[Parameter(Position = 0, ParameterSetName = "ReverseActions")]
|
||||||
|
[string[]]$AbbreviatedActions,
|
||||||
|
|
||||||
|
[Parameter(Position = 1, Mandatory = $true, ParameterSetName = "ReverseActions")]
|
||||||
|
[ValidateSet("Admin", "Delegate", "Owner")]
|
||||||
|
[string]$ReverseActionType
|
||||||
|
)
|
||||||
|
|
||||||
|
$Dictionary = @{
|
||||||
|
AdminActions = @{
|
||||||
|
ApplyRecord = 'AR'
|
||||||
|
Copy = 'CP'
|
||||||
|
Create = 'CR'
|
||||||
|
FolderBind = 'FB'
|
||||||
|
HardDelete = 'HD'
|
||||||
|
MailItemsAccessed = 'MIA'
|
||||||
|
Move = 'MV'
|
||||||
|
MoveToDeletedItems = 'MTDI'
|
||||||
|
SendAs = 'SA'
|
||||||
|
SendOnBehalf = 'SOB'
|
||||||
|
Send = 'SD'
|
||||||
|
SoftDelete = 'SD'
|
||||||
|
Update = 'UP'
|
||||||
|
UpdateCalendarDelegation = 'UCD'
|
||||||
|
UpdateFolderPermissions = 'UFP'
|
||||||
|
UpdateInboxRules = 'UIR'
|
||||||
|
}
|
||||||
|
DelegateActions = @{
|
||||||
|
ApplyRecord = 'AR'
|
||||||
|
Create = 'CR'
|
||||||
|
FolderBind = 'FB'
|
||||||
|
HardDelete = 'HD'
|
||||||
|
MailItemsAccessed = 'MIA'
|
||||||
|
Move = 'MV'
|
||||||
|
MoveToDeletedItems = 'MTDI'
|
||||||
|
SendAs = 'SA'
|
||||||
|
SendOnBehalf = 'SOB'
|
||||||
|
SoftDelete = 'SD'
|
||||||
|
Update = 'UP'
|
||||||
|
UpdateFolderPermissions = 'UFP'
|
||||||
|
UpdateInboxRules = 'UIR'
|
||||||
|
}
|
||||||
|
OwnerActions = @{
|
||||||
|
ApplyRecord = 'AR'
|
||||||
|
Create = 'CR'
|
||||||
|
HardDelete = 'HD'
|
||||||
|
MailboxLogin = 'ML'
|
||||||
|
MailItemsAccessed = 'MIA'
|
||||||
|
Move = 'MV'
|
||||||
|
MoveToDeletedItems = 'MTDI'
|
||||||
|
Send = 'SD'
|
||||||
|
SoftDelete = 'SD'
|
||||||
|
Update = 'UP'
|
||||||
|
UpdateCalendarDelegation = 'UCD'
|
||||||
|
UpdateFolderPermissions = 'UFP'
|
||||||
|
UpdateInboxRules = 'UIR'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($PSCmdlet.ParameterSetName) {
|
||||||
|
"GetDictionaries" {
|
||||||
|
return $Dictionary
|
||||||
|
}
|
||||||
|
"ConvertActions" {
|
||||||
|
$actionDictionary = switch ($ActionType) {
|
||||||
|
"Admin" { $Dictionary.AdminActions }
|
||||||
|
"Delegate" { $Dictionary.DelegateActions }
|
||||||
|
"Owner" { $Dictionary.OwnerActions }
|
||||||
|
}
|
||||||
|
|
||||||
|
$abbreviatedActions = @()
|
||||||
|
foreach ($action in $Actions) {
|
||||||
|
if ($actionDictionary.ContainsKey($action)) {
|
||||||
|
$abbreviatedActions += $actionDictionary[$action]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $abbreviatedActions
|
||||||
|
}
|
||||||
|
"ReverseActions" {
|
||||||
|
$reverseDictionary = @{}
|
||||||
|
$originalDictionary = switch ($ReverseActionType) {
|
||||||
|
"Admin" { $Dictionary.AdminActions }
|
||||||
|
"Delegate" { $Dictionary.DelegateActions }
|
||||||
|
"Owner" { $Dictionary.OwnerActions }
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($key in $originalDictionary.Keys) {
|
||||||
|
$reverseDictionary[$originalDictionary[$key]] = $key
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullNames = @()
|
||||||
|
foreach ($abbrAction in $AbbreviatedActions) {
|
||||||
|
if ($reverseDictionary.ContainsKey($abbrAction)) {
|
||||||
|
$fullNames += $reverseDictionary[$abbrAction]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $fullNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
source/Private/Get-ExceededLengthResultDetail.ps1
Normal file
54
source/Private/Get-ExceededLengthResultDetail.ps1
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
function Get-ExceededLengthResultDetail {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')]
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')]
|
||||||
|
[object[]]$AuditResults,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')]
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')]
|
||||||
|
[string[]]$TestNumbersToCheck,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')]
|
||||||
|
[string[]]$ExportedTests,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')]
|
||||||
|
[switch]$ReturnExceedingTestsOnly,
|
||||||
|
|
||||||
|
[int]$DetailsLengthLimit = 30000,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')]
|
||||||
|
[int]$PreviewLineCount = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
$exceedingTests = @()
|
||||||
|
$updatedResults = @()
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $AuditResults.Count; $i++) {
|
||||||
|
$auditResult = $AuditResults[$i]
|
||||||
|
if ($auditResult.Rec -in $TestNumbersToCheck) {
|
||||||
|
if ($auditResult.Details.Length -gt $DetailsLengthLimit) {
|
||||||
|
if ($ReturnExceedingTestsOnly) {
|
||||||
|
$exceedingTests += $auditResult.Rec
|
||||||
|
} else {
|
||||||
|
$previewLines = ($auditResult.Details -split '\r?\n' | Select-Object -First $PreviewLineCount) -join "`n"
|
||||||
|
$message = "The test result is too large to be exported to CSV. Use the audit result and the export function for full output.`n`nPreview:`n$previewLines"
|
||||||
|
|
||||||
|
if ($ExportedTests -contains $auditResult.Rec) {
|
||||||
|
Write-Information "The test result for $($auditResult.Rec) is too large for CSV and was included in the export. Check the exported files."
|
||||||
|
$auditResult.Details = $message
|
||||||
|
} else {
|
||||||
|
$auditResult.Details = $message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$updatedResults += $auditResult
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ReturnExceedingTestsOnly) {
|
||||||
|
return $exceedingTests
|
||||||
|
} else {
|
||||||
|
return $updatedResults
|
||||||
|
}
|
||||||
|
}
|
36
source/Private/Initialize-LargeTestTable.ps1
Normal file
36
source/Private/Initialize-LargeTestTable.ps1
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
This function generates a large table with the specified number of lines.
|
||||||
|
.DESCRIPTION
|
||||||
|
This function generates a large table with the specified number of lines. The table has a header and each line has the same format.
|
||||||
|
.EXAMPLE
|
||||||
|
Initialize-LargeTestTable -lineCount 1000
|
||||||
|
.PARAMETER lineCount
|
||||||
|
The number of lines to generate.
|
||||||
|
.INPUTS
|
||||||
|
System.Int32
|
||||||
|
.OUTPUTS
|
||||||
|
System.String
|
||||||
|
.NOTES
|
||||||
|
The function is intended for testing purposes.
|
||||||
|
#>
|
||||||
|
function Initialize-LargeTestTable {
|
||||||
|
[cmdletBinding()]
|
||||||
|
[OutputType([string])]
|
||||||
|
param(
|
||||||
|
[Parameter()]
|
||||||
|
[int]$lineCount = 1000 # Number of lines to generate
|
||||||
|
)
|
||||||
|
process {
|
||||||
|
$header = "UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing"
|
||||||
|
$lineTemplate = "user{0}@contosonorthwind.net|True|FB,CP,MV|FB,MV|ML,MV,CR"
|
||||||
|
# Generate the header and lines
|
||||||
|
$lines = @($header)
|
||||||
|
for ($i = 1; $i -le $lineCount; $i++) {
|
||||||
|
$lines += [string]::Format($lineTemplate, $i)
|
||||||
|
}
|
||||||
|
$output = $lines -join "`n"
|
||||||
|
Write-Host "Details character count: $($output.Length)"
|
||||||
|
return $output
|
||||||
|
}
|
||||||
|
}
|
@@ -1,42 +0,0 @@
|
|||||||
function Merge-CISExcelAndCsvData {
|
|
||||||
[CmdletBinding(DefaultParameterSetName = 'CsvInput')]
|
|
||||||
[OutputType([PSCustomObject[]])]
|
|
||||||
param (
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$ExcelPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$WorksheetName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true, ParameterSetName = 'CsvInput')]
|
|
||||||
[string]$CsvPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true, ParameterSetName = 'ObjectInput')]
|
|
||||||
[CISAuditResult[]]$AuditResults
|
|
||||||
)
|
|
||||||
|
|
||||||
process {
|
|
||||||
# Import data from Excel
|
|
||||||
$import = Import-Excel -Path $ExcelPath -WorksheetName $WorksheetName
|
|
||||||
|
|
||||||
# Import data from CSV or use provided object
|
|
||||||
$csvData = if ($PSCmdlet.ParameterSetName -eq 'CsvInput') {
|
|
||||||
Import-Csv -Path $CsvPath
|
|
||||||
} else {
|
|
||||||
$AuditResults
|
|
||||||
}
|
|
||||||
|
|
||||||
# Iterate over each item in the imported Excel object and merge with CSV data or audit results
|
|
||||||
$mergedData = foreach ($item in $import) {
|
|
||||||
$csvRow = $csvData | Where-Object { $_.Rec -eq $item.'recommendation #' }
|
|
||||||
if ($csvRow) {
|
|
||||||
New-MergedObject -ExcelItem $item -CsvRow $csvRow
|
|
||||||
} else {
|
|
||||||
New-MergedObject -ExcelItem $item -CsvRow ([PSCustomObject]@{Connection=$null;Status=$null; Details=$null; FailureReason=$null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Return the merged data
|
|
||||||
return $mergedData
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
function New-MergedObject {
|
|
||||||
[CmdletBinding()]
|
|
||||||
[OutputType([PSCustomObject])]
|
|
||||||
param (
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[psobject]$ExcelItem,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[psobject]$CsvRow
|
|
||||||
)
|
|
||||||
|
|
||||||
$newObject = New-Object PSObject
|
|
||||||
|
|
||||||
foreach ($property in $ExcelItem.PSObject.Properties) {
|
|
||||||
$newObject | Add-Member -MemberType NoteProperty -Name $property.Name -Value $property.Value
|
|
||||||
}
|
|
||||||
$newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Connection' -Value $CsvRow.Connection
|
|
||||||
$newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Status' -Value $CsvRow.Status
|
|
||||||
$newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Details' -Value $CsvRow.Details
|
|
||||||
$newObject | Add-Member -MemberType NoteProperty -Name 'CSV_FailureReason' -Value $CsvRow.FailureReason
|
|
||||||
return $newObject
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
function Update-CISExcelWorksheet {
|
|
||||||
[OutputType([void])]
|
|
||||||
[CmdletBinding()]
|
|
||||||
param (
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$ExcelPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$WorksheetName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[psobject[]]$Data,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[int]$StartingRowIndex = 2 # Default starting row index, assuming row 1 has headers
|
|
||||||
)
|
|
||||||
|
|
||||||
process {
|
|
||||||
# Load the existing Excel sheet
|
|
||||||
$excelPackage = Open-ExcelPackage -Path $ExcelPath
|
|
||||||
$worksheet = $excelPackage.Workbook.Worksheets[$WorksheetName]
|
|
||||||
|
|
||||||
if (-not $worksheet) {
|
|
||||||
throw "Worksheet '$WorksheetName' not found in '$ExcelPath'"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Update the worksheet with the provided data
|
|
||||||
Update-WorksheetCell -Worksheet $worksheet -Data $Data -StartingRowIndex $StartingRowIndex
|
|
||||||
|
|
||||||
# Save and close the Excel package
|
|
||||||
Close-ExcelPackage $excelPackage
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,29 +0,0 @@
|
|||||||
function Update-WorksheetCell {
|
|
||||||
[OutputType([void])]
|
|
||||||
param (
|
|
||||||
$Worksheet,
|
|
||||||
$Data,
|
|
||||||
$StartingRowIndex
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check and set headers
|
|
||||||
$firstItem = $Data[0]
|
|
||||||
$colIndex = 1
|
|
||||||
foreach ($property in $firstItem.PSObject.Properties) {
|
|
||||||
if ($StartingRowIndex -eq 2 -and $Worksheet.Cells[1, $colIndex].Value -eq $null) {
|
|
||||||
$Worksheet.Cells[1, $colIndex].Value = $property.Name
|
|
||||||
}
|
|
||||||
$colIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
# Iterate over each row in the data and update cells
|
|
||||||
$rowIndex = $StartingRowIndex
|
|
||||||
foreach ($item in $Data) {
|
|
||||||
$colIndex = 1
|
|
||||||
foreach ($property in $item.PSObject.Properties) {
|
|
||||||
$Worksheet.Cells[$rowIndex, $colIndex].Value = $property.Value
|
|
||||||
$colIndex++
|
|
||||||
}
|
|
||||||
$rowIndex++
|
|
||||||
}
|
|
||||||
}
|
|
208
source/Public/Export-M365SecurityAuditTable.ps1
Normal file
208
source/Public/Export-M365SecurityAuditTable.ps1
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Exports M365 security audit results to a CSV file or outputs a specific test result as an object.
|
||||||
|
.DESCRIPTION
|
||||||
|
This function exports M365 security audit results from either an array of CISAuditResult objects or a CSV file.
|
||||||
|
It can export all results to a specified path or output a specific test result as an object.
|
||||||
|
.PARAMETER AuditResults
|
||||||
|
An array of CISAuditResult objects containing the audit results.
|
||||||
|
.PARAMETER CsvPath
|
||||||
|
The path to a CSV file containing the audit results.
|
||||||
|
.PARAMETER OutputTestNumber
|
||||||
|
The test number to output as an object. Valid values are "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4".
|
||||||
|
.PARAMETER ExportAllTests
|
||||||
|
Switch to export all test results.
|
||||||
|
.PARAMETER ExportPath
|
||||||
|
The path where the CSV files will be exported.
|
||||||
|
.PARAMETER ExportOriginalTests
|
||||||
|
Switch to export the original audit results to a CSV file.
|
||||||
|
.INPUTS
|
||||||
|
[CISAuditResult[]], [string]
|
||||||
|
.OUTPUTS
|
||||||
|
[PSCustomObject]
|
||||||
|
.EXAMPLE
|
||||||
|
Export-M365SecurityAuditTable -AuditResults $object -OutputTestNumber 6.1.2
|
||||||
|
# Output object for a single test number from audit results
|
||||||
|
.EXAMPLE
|
||||||
|
Export-M365SecurityAuditTable -ExportAllTests -AuditResults $object -ExportPath "C:\temp"
|
||||||
|
# Export all results from audit results to the specified path
|
||||||
|
.EXAMPLE
|
||||||
|
Export-M365SecurityAuditTable -CsvPath "C:\temp\auditresultstoday1.csv" -OutputTestNumber 6.1.2
|
||||||
|
# Output object for a single test number from CSV
|
||||||
|
.EXAMPLE
|
||||||
|
Export-M365SecurityAuditTable -ExportAllTests -CsvPath "C:\temp\auditresultstoday1.csv" -ExportPath "C:\temp"
|
||||||
|
# Export all results from CSV to the specified path
|
||||||
|
.EXAMPLE
|
||||||
|
Export-M365SecurityAuditTable -ExportAllTests -AuditResults $object -ExportPath "C:\temp" -ExportOriginalTests
|
||||||
|
# Export all results from audit results to the specified path along with the original tests
|
||||||
|
.EXAMPLE
|
||||||
|
Export-M365SecurityAuditTable -ExportAllTests -CsvPath "C:\temp\auditresultstoday1.csv" -ExportPath "C:\temp" -ExportOriginalTests
|
||||||
|
# Export all results from CSV to the specified path along with the original tests
|
||||||
|
.LINK
|
||||||
|
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Export-M365SecurityAuditTable
|
||||||
|
#>
|
||||||
|
function Export-M365SecurityAuditTable {
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([PSCustomObject])]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = "ExportAllResultsFromAuditResults")]
|
||||||
|
[Parameter(Mandatory = $true, Position = 2, ParameterSetName = "OutputObjectFromAuditResultsSingle")]
|
||||||
|
[CISAuditResult[]]$AuditResults,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = "ExportAllResultsFromCsv")]
|
||||||
|
[Parameter(Mandatory = $true, Position = 2, ParameterSetName = "OutputObjectFromCsvSingle")]
|
||||||
|
[ValidateScript({ (Test-Path $_) -and ((Get-Item $_).PSIsContainer -eq $false) })]
|
||||||
|
[string]$CsvPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = "OutputObjectFromAuditResultsSingle")]
|
||||||
|
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = "OutputObjectFromCsvSingle")]
|
||||||
|
[ValidateSet("1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4")]
|
||||||
|
[string]$OutputTestNumber,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "ExportAllResultsFromAuditResults")]
|
||||||
|
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "ExportAllResultsFromCsv")]
|
||||||
|
[switch]$ExportAllTests,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = "ExportAllResultsFromAuditResults")]
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = "ExportAllResultsFromCsv")]
|
||||||
|
[string]$ExportPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromAuditResults")]
|
||||||
|
[Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromCsv")]
|
||||||
|
[switch]$ExportOriginalTests
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($PSCmdlet.ParameterSetName -like "ExportAllResultsFromCsv" -or $PSCmdlet.ParameterSetName -eq "OutputObjectFromCsvSingle") {
|
||||||
|
$AuditResults = Import-Csv -Path $CsvPath | ForEach-Object {
|
||||||
|
$params = @{
|
||||||
|
Rec = $_.Rec
|
||||||
|
Result = [bool]$_.Result
|
||||||
|
Status = $_.Status
|
||||||
|
Details = $_.Details
|
||||||
|
FailureReason = $_.FailureReason
|
||||||
|
}
|
||||||
|
Initialize-CISAuditResult @params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ExportAllTests) {
|
||||||
|
$TestNumbers = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4"
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = @()
|
||||||
|
|
||||||
|
$testsToProcess = if ($OutputTestNumber) { @($OutputTestNumber) } else { $TestNumbers }
|
||||||
|
|
||||||
|
foreach ($test in $testsToProcess) {
|
||||||
|
$auditResult = $AuditResults | Where-Object { $_.Rec -eq $test }
|
||||||
|
if (-not $auditResult) {
|
||||||
|
Write-Information "No audit results found for the test number $test."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($test) {
|
||||||
|
"6.1.2" {
|
||||||
|
$details = $auditResult.Details
|
||||||
|
if ($details -ne "No M365 E3 licenses found.") {
|
||||||
|
$csv = $details | ConvertFrom-Csv -Delimiter '|'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$csv = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $csv) {
|
||||||
|
foreach ($row in $csv) {
|
||||||
|
$row.AdminActionsMissing = (Get-Action -AbbreviatedActions $row.AdminActionsMissing.Split(',') -ReverseActionType Admin | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }) -join ','
|
||||||
|
$row.DelegateActionsMissing = (Get-Action -AbbreviatedActions $row.DelegateActionsMissing.Split(',') -ReverseActionType Delegate | Where-Object { $_ -notin @("MailItemsAccessed") }) -join ','
|
||||||
|
$row.OwnerActionsMissing = (Get-Action -AbbreviatedActions $row.OwnerActionsMissing.Split(',') -ReverseActionType Owner | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }) -join ','
|
||||||
|
}
|
||||||
|
$newObjectDetails = $csv
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$newObjectDetails = $details
|
||||||
|
}
|
||||||
|
$results += [PSCustomObject]@{ TestNumber = $test; Details = $newObjectDetails }
|
||||||
|
}
|
||||||
|
"6.1.3" {
|
||||||
|
$details = $auditResult.Details
|
||||||
|
if ($details -ne "No M365 E5 licenses found.") {
|
||||||
|
$csv = $details | ConvertFrom-Csv -Delimiter '|'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$csv = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $csv) {
|
||||||
|
foreach ($row in $csv) {
|
||||||
|
$row.AdminActionsMissing = (Get-Action -AbbreviatedActions $row.AdminActionsMissing.Split(',') -ReverseActionType Admin) -join ','
|
||||||
|
$row.DelegateActionsMissing = (Get-Action -AbbreviatedActions $row.DelegateActionsMissing.Split(',') -ReverseActionType Delegate) -join ','
|
||||||
|
$row.OwnerActionsMissing = (Get-Action -AbbreviatedActions $row.OwnerActionsMissing.Split(',') -ReverseActionType Owner) -join ','
|
||||||
|
}
|
||||||
|
$newObjectDetails = $csv
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$newObjectDetails = $details
|
||||||
|
}
|
||||||
|
$results += [PSCustomObject]@{ TestNumber = $test; Details = $newObjectDetails }
|
||||||
|
}
|
||||||
|
Default {
|
||||||
|
$details = $auditResult.Details
|
||||||
|
$csv = $details | ConvertFrom-Csv -Delimiter '|'
|
||||||
|
$results += [PSCustomObject]@{ TestNumber = $test; Details = $csv }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ExportPath) {
|
||||||
|
$timestamp = (Get-Date).ToString("yyyy.MM.dd_HH.mm.ss")
|
||||||
|
$exportedTests = @()
|
||||||
|
|
||||||
|
foreach ($result in $results) {
|
||||||
|
$testDef = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $result.TestNumber }
|
||||||
|
if ($testDef) {
|
||||||
|
$fileName = "$ExportPath\$($timestamp)_$($result.TestNumber).$($testDef.TestFileName -replace '\.ps1$').csv"
|
||||||
|
if ($result.Details.Count -eq 0) {
|
||||||
|
Write-Information "No results found for test number $($result.TestNumber)." -InformationAction Continue
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (($result.Details -ne "No M365 E3 licenses found.") -and ($result.Details -ne "No M365 E5 licenses found.")) {
|
||||||
|
$result.Details | Export-Csv -Path $fileName -NoTypeInformation
|
||||||
|
$exportedTests += $result.TestNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($exportedTests.Count -gt 0) {
|
||||||
|
Write-Information "The following tests were exported: $($exportedTests -join ', ')" -InformationAction Continue
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($ExportOriginalTests) {
|
||||||
|
Write-Information "No specified tests were included in the export other than the full audit results." -InformationAction Continue
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Information "No specified tests were included in the export." -InformationAction Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ExportOriginalTests) {
|
||||||
|
# Define the test numbers to check
|
||||||
|
$TestNumbersToCheck = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4"
|
||||||
|
|
||||||
|
# Check for large details and update the AuditResults array
|
||||||
|
$updatedAuditResults = Get-ExceededLengthResultDetail -AuditResults $AuditResults -TestNumbersToCheck $TestNumbersToCheck -ExportedTests $exportedTests -DetailsLengthLimit 30000 -PreviewLineCount 25
|
||||||
|
$originalFileName = "$ExportPath\$timestamp`_M365FoundationsAudit.csv"
|
||||||
|
$updatedAuditResults | Export-Csv -Path $originalFileName -NoTypeInformation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($OutputTestNumber) {
|
||||||
|
if ($results[0].Details) {
|
||||||
|
return $results[0].Details
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Information "No results found for test number $($OutputTestNumber)." -InformationAction Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Error "No valid operation specified. Please provide valid parameters."
|
||||||
|
}
|
||||||
|
}
|
@@ -25,7 +25,6 @@
|
|||||||
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Get-AdminRoleUserLicense
|
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Get-AdminRoleUserLicense
|
||||||
#>
|
#>
|
||||||
function Get-AdminRoleUserLicense {
|
function Get-AdminRoleUserLicense {
|
||||||
# Set output type to System.Collections.ArrayList
|
|
||||||
[OutputType([System.Collections.ArrayList])]
|
[OutputType([System.Collections.ArrayList])]
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param (
|
param (
|
||||||
@@ -42,33 +41,37 @@ function Get-AdminRoleUserLicense {
|
|||||||
$userIds = [System.Collections.ArrayList]::new()
|
$userIds = [System.Collections.ArrayList]::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
process {
|
||||||
$adminroles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Admin*" }
|
Write-Verbose "Retrieving all admin roles"
|
||||||
|
$adminRoleNames = (Get-MgDirectoryRole | Where-Object { $null -ne $_.RoleTemplateId }).DisplayName
|
||||||
|
|
||||||
foreach ($role in $adminroles) {
|
Write-Verbose "Filtering admin roles"
|
||||||
$usersInRole = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'"
|
$adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { ($adminRoleNames -contains $_.DisplayName) -and ($_.DisplayName -ne "Directory Synchronization Accounts") }
|
||||||
|
|
||||||
foreach ($user in $usersInRole) {
|
foreach ($role in $adminRoles) {
|
||||||
$userDetails = Get-MgUser -UserId $user.PrincipalId -Property "DisplayName, UserPrincipalName, Id, onPremisesSyncEnabled" -ErrorAction SilentlyContinue
|
Write-Verbose "Processing role: $($role.DisplayName)"
|
||||||
|
$roleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'"
|
||||||
|
|
||||||
|
foreach ($assignment in $roleAssignments) {
|
||||||
|
Write-Verbose "Processing role assignment for principal ID: $($assignment.PrincipalId)"
|
||||||
|
$userDetails = Get-MgUser -UserId $assignment.PrincipalId -Property "DisplayName, UserPrincipalName, Id, OnPremisesSyncEnabled" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
if ($userDetails) {
|
if ($userDetails) {
|
||||||
[void]($userIds.Add($user.PrincipalId))
|
Write-Verbose "Retrieved user details for: $($userDetails.UserPrincipalName)"
|
||||||
[void](
|
[void]($userIds.Add($userDetails.Id))
|
||||||
$adminRoleUsers.Add(
|
[void]($adminRoleUsers.Add([PSCustomObject]@{
|
||||||
[PSCustomObject]@{
|
RoleName = $role.DisplayName
|
||||||
RoleName = $role.DisplayName
|
UserName = $userDetails.DisplayName
|
||||||
UserName = $userDetails.DisplayName
|
UserPrincipalName = $userDetails.UserPrincipalName
|
||||||
UserPrincipalName = $userDetails.UserPrincipalName
|
UserId = $userDetails.Id
|
||||||
UserId = $userDetails.Id
|
HybridUser = [bool]$userDetails.OnPremisesSyncEnabled
|
||||||
HybridUser = $userDetails.onPremisesSyncEnabled
|
Licenses = $null # Initialize as $null
|
||||||
Licenses = $null # Initialize as $null
|
}))
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Retrieving licenses for admin role users"
|
||||||
foreach ($userId in $userIds.ToArray() | Select-Object -Unique) {
|
foreach ($userId in $userIds.ToArray() | Select-Object -Unique) {
|
||||||
$licenses = Get-MgUserLicenseDetail -UserId $userId -ErrorAction SilentlyContinue
|
$licenses = Get-MgUserLicenseDetail -UserId $userId -ErrorAction SilentlyContinue
|
||||||
if ($licenses) {
|
if ($licenses) {
|
||||||
@@ -80,7 +83,7 @@ function Get-AdminRoleUserLicense {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
End {
|
end {
|
||||||
Write-Host "Disconnecting from Microsoft Graph..." -ForegroundColor Green
|
Write-Host "Disconnecting from Microsoft Graph..." -ForegroundColor Green
|
||||||
Disconnect-MgGraph | Out-Null
|
Disconnect-MgGraph | Out-Null
|
||||||
return $adminRoleUsers
|
return $adminRoleUsers
|
||||||
|
102
source/Public/Get-MFAStatus.ps1
Normal file
102
source/Public/Get-MFAStatus.ps1
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Retrieves the MFA (Multi-Factor Authentication) status for Azure Active Directory users.
|
||||||
|
.DESCRIPTION
|
||||||
|
The Get-MFAStatus function connects to Microsoft Online Service and retrieves the MFA status for all Azure Active Directory users, excluding guest accounts. Optionally, you can specify a single user by their User Principal Name (UPN) to get their MFA status.
|
||||||
|
.PARAMETER UserId
|
||||||
|
The User Principal Name (UPN) of a specific user to retrieve MFA status for. If not provided, the function retrieves MFA status for all users.
|
||||||
|
.EXAMPLE
|
||||||
|
Get-MFAStatus
|
||||||
|
Retrieves the MFA status for all Azure Active Directory users.
|
||||||
|
.EXAMPLE
|
||||||
|
Get-MFAStatus -UserId "example@domain.com"
|
||||||
|
Retrieves the MFA status for the specified user with the UPN "example@domain.com".
|
||||||
|
.OUTPUTS
|
||||||
|
System.Object
|
||||||
|
Returns a sorted list of custom objects containing the following properties:
|
||||||
|
- UserPrincipalName
|
||||||
|
- DisplayName
|
||||||
|
- MFAState
|
||||||
|
- MFADefaultMethod
|
||||||
|
- MFAPhoneNumber
|
||||||
|
- PrimarySMTP
|
||||||
|
- Aliases
|
||||||
|
.NOTES
|
||||||
|
The function requires the MSOL module to be installed and connected to your tenant.
|
||||||
|
Ensure that you have the necessary permissions to read user and MFA status information.
|
||||||
|
.LINK
|
||||||
|
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Get-MFAStatus
|
||||||
|
#>
|
||||||
|
function Get-MFAStatus {
|
||||||
|
[OutputType([System.Object])]
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[string]$UserId
|
||||||
|
)
|
||||||
|
|
||||||
|
begin {
|
||||||
|
# Connect to Microsoft Online service
|
||||||
|
Import-Module MSOnline -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
process {
|
||||||
|
if (Get-Module MSOnline){
|
||||||
|
Connect-MsolService
|
||||||
|
Write-Host "Finding Azure Active Directory Accounts..."
|
||||||
|
# Get all users, excluding guests
|
||||||
|
$Users = if ($PSBoundParameters.ContainsKey('UserId')) {
|
||||||
|
Get-MsolUser -UserPrincipalName $UserId
|
||||||
|
} else {
|
||||||
|
Get-MsolUser -All | Where-Object { $_.UserType -ne "Guest" }
|
||||||
|
}
|
||||||
|
$Report = [System.Collections.Generic.List[Object]]::new() # Create output list
|
||||||
|
Write-Host "Processing $($Users.Count) accounts..."
|
||||||
|
ForEach ($User in $Users) {
|
||||||
|
$MFADefaultMethod = ($User.StrongAuthenticationMethods | Where-Object { $_.IsDefault -eq "True" }).MethodType
|
||||||
|
$MFAPhoneNumber = $User.StrongAuthenticationUserDetails.PhoneNumber
|
||||||
|
$PrimarySMTP = $User.ProxyAddresses | Where-Object { $_ -clike "SMTP*" } | ForEach-Object { $_ -replace "SMTP:", "" }
|
||||||
|
$Aliases = $User.ProxyAddresses | Where-Object { $_ -clike "smtp*" } | ForEach-Object { $_ -replace "smtp:", "" }
|
||||||
|
|
||||||
|
If ($User.StrongAuthenticationRequirements) {
|
||||||
|
$MFAState = $User.StrongAuthenticationRequirements.State
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$MFAState = 'Disabled'
|
||||||
|
}
|
||||||
|
|
||||||
|
If ($MFADefaultMethod) {
|
||||||
|
Switch ($MFADefaultMethod) {
|
||||||
|
"OneWaySMS" { $MFADefaultMethod = "Text code authentication phone" }
|
||||||
|
"TwoWayVoiceMobile" { $MFADefaultMethod = "Call authentication phone" }
|
||||||
|
"TwoWayVoiceOffice" { $MFADefaultMethod = "Call office phone" }
|
||||||
|
"PhoneAppOTP" { $MFADefaultMethod = "Authenticator app or hardware token" }
|
||||||
|
"PhoneAppNotification" { $MFADefaultMethod = "Microsoft authenticator app" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$MFADefaultMethod = "Not enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
$ReportLine = [PSCustomObject] @{
|
||||||
|
UserPrincipalName = $User.UserPrincipalName
|
||||||
|
DisplayName = $User.DisplayName
|
||||||
|
MFAState = $MFAState
|
||||||
|
MFADefaultMethod = $MFADefaultMethod
|
||||||
|
MFAPhoneNumber = $MFAPhoneNumber
|
||||||
|
PrimarySMTP = ($PrimarySMTP -join ',')
|
||||||
|
Aliases = ($Aliases -join ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
$Report.Add($ReportLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Processing complete."
|
||||||
|
return $Report | Select-Object UserPrincipalName, DisplayName, MFAState, MFADefaultMethod, MFAPhoneNumber, PrimarySMTP, Aliases | Sort-Object UserPrincipalName
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "You must first install MSOL using:`nInstall-Module MSOnline -Scope CurrentUser -Force"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -27,6 +27,8 @@
|
|||||||
If specified, the cmdlet will not disconnect from Microsoft 365 services after execution.
|
If specified, the cmdlet will not disconnect from Microsoft 365 services after execution.
|
||||||
.PARAMETER NoModuleCheck
|
.PARAMETER NoModuleCheck
|
||||||
If specified, the cmdlet will not check for the presence of required modules.
|
If specified, the cmdlet will not check for the presence of required modules.
|
||||||
|
.PARAMETER DoNotConfirmConnections
|
||||||
|
If specified, the cmdlet will not prompt for confirmation before proceeding with established connections and will disconnect from all of them.
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
PS> Invoke-M365SecurityAudit
|
PS> Invoke-M365SecurityAudit
|
||||||
Performs a security audit using default parameters.
|
Performs a security audit using default parameters.
|
||||||
@@ -174,7 +176,8 @@ function Invoke-M365SecurityAudit {
|
|||||||
# Common parameters for all parameter sets
|
# Common parameters for all parameter sets
|
||||||
[switch]$DoNotConnect,
|
[switch]$DoNotConnect,
|
||||||
[switch]$DoNotDisconnect,
|
[switch]$DoNotDisconnect,
|
||||||
[switch]$NoModuleCheck
|
[switch]$NoModuleCheck,
|
||||||
|
[switch]$DoNotConfirmConnections
|
||||||
)
|
)
|
||||||
|
|
||||||
Begin {
|
Begin {
|
||||||
@@ -240,11 +243,18 @@ function Invoke-M365SecurityAudit {
|
|||||||
$currentTestIndex = 0
|
$currentTestIndex = 0
|
||||||
|
|
||||||
# Establishing connections if required
|
# Establishing connections if required
|
||||||
$actualUniqueConnections = Get-UniqueConnection -Connections $requiredConnections
|
try {
|
||||||
if (!($DoNotConnect) -and $PSCmdlet.ShouldProcess("Establish connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Connect")) {
|
$actualUniqueConnections = Get-UniqueConnection -Connections $requiredConnections
|
||||||
Write-Information "Establishing connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')" -InformationAction Continue
|
if (!($DoNotConnect) -and $PSCmdlet.ShouldProcess("Establish connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Connect")) {
|
||||||
Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections
|
Write-Information "Establishing connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')" -InformationAction Continue
|
||||||
|
Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections -SkipConfirmation:$DoNotConfirmConnections
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "Execution aborted: $_" -ForegroundColor Red
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Write-Information "A total of $($totalTests) tests were selected to run..." -InformationAction Continue
|
Write-Information "A total of $($totalTests) tests were selected to run..." -InformationAction Continue
|
||||||
@@ -286,6 +296,16 @@ function Invoke-M365SecurityAudit {
|
|||||||
# Call the private function to calculate and display results
|
# Call the private function to calculate and display results
|
||||||
Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests
|
Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests
|
||||||
# Return all collected audit results
|
# Return all collected audit results
|
||||||
|
# Define the test numbers to check
|
||||||
|
$TestNumbersToCheck = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4"
|
||||||
|
|
||||||
|
# Check for large details in the audit results
|
||||||
|
$exceedingTests = Get-ExceededLengthResultDetail -AuditResults $allAuditResults -TestNumbersToCheck $TestNumbersToCheck -ReturnExceedingTestsOnly -DetailsLengthLimit 30000
|
||||||
|
if ($exceedingTests.Count -gt 0) {
|
||||||
|
Write-Information "The following tests exceeded the details length limit: $($exceedingTests -join ', ')" -InformationAction Continue
|
||||||
|
Write-Host "(Assuming the results were instantiated. Ex: `$object = invoke-M365SecurityAudit) Use the following command and adjust as neccesary to view the full details of the test results:" -ForegroundColor DarkCyan
|
||||||
|
Write-Host "Export-M365SecurityAuditTable -ExportAllTests -AuditResults `$object -ExportPath `"C:\temp`" -ExportOriginalTests" -ForegroundColor Green
|
||||||
|
}
|
||||||
return $allAuditResults.ToArray() | Sort-Object -Property Rec
|
return $allAuditResults.ToArray() | Sort-Object -Property Rec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
source/Public/Remove-RowsWithEmptyCSVStatus.ps1
Normal file
34
source/Public/Remove-RowsWithEmptyCSVStatus.ps1
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
function Remove-RowsWithEmptyCSVStatus {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$FilePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$WorksheetName
|
||||||
|
)
|
||||||
|
|
||||||
|
# Import the Excel file
|
||||||
|
$ExcelData = Import-Excel -Path $FilePath -WorksheetName $WorksheetName
|
||||||
|
|
||||||
|
# Check if CSV_Status column exists
|
||||||
|
if (-not $ExcelData.PSObject.Properties.Match("CSV_Status")) {
|
||||||
|
throw "CSV_Status column not found in the worksheet."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter rows where CSV_Status is not empty
|
||||||
|
$FilteredData = $ExcelData | Where-Object { $null -ne $_.CSV_Status -and $_.CSV_Status -ne '' }
|
||||||
|
|
||||||
|
# Get the original file name and directory
|
||||||
|
$OriginalFileName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath)
|
||||||
|
$Directory = [System.IO.Path]::GetDirectoryName($FilePath)
|
||||||
|
|
||||||
|
# Create a new file name for the filtered data
|
||||||
|
$NewFileName = "$OriginalFileName-Filtered.xlsx"
|
||||||
|
$NewFilePath = Join-Path -Path $Directory -ChildPath $NewFileName
|
||||||
|
|
||||||
|
# Export the filtered data to a new Excel file
|
||||||
|
$FilteredData | Export-Excel -Path $NewFilePath -WorksheetName $WorksheetName -Show
|
||||||
|
|
||||||
|
Write-Output "Filtered Excel file created at $NewFilePath"
|
||||||
|
}
|
@@ -1,90 +1,102 @@
|
|||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Synchronizes data between an Excel file and either a CSV file or an output object from Invoke-M365SecurityAudit, and optionally updates the Excel worksheet.
|
Synchronizes and updates data in an Excel worksheet with new information from a CSV file, including audit dates.
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
The Sync-CISExcelAndCsvData function merges data from a specified Excel file with data from either a CSV file or an output object from Invoke-M365SecurityAudit based on a common key. It can also update the Excel worksheet with the merged data. This function is particularly useful for updating Excel records with additional data from a CSV file or audit results while preserving the original formatting and structure of the Excel worksheet.
|
The Sync-CISExcelAndCsvData function merges and updates data in a specified Excel worksheet from a CSV file. This includes adding or updating fields for connection status, details, failure reasons, and the date of the update. It's designed to ensure that the Excel document maintains a running log of changes over time, ideal for tracking remediation status and audit history.
|
||||||
.PARAMETER ExcelPath
|
.PARAMETER ExcelPath
|
||||||
The path to the Excel file that contains the original data. This parameter is mandatory.
|
Specifies the path to the Excel file to be updated. This parameter is mandatory.
|
||||||
.PARAMETER WorksheetName
|
.PARAMETER CsvPath
|
||||||
The name of the worksheet within the Excel file that contains the data to be synchronized. This parameter is mandatory.
|
Specifies the path to the CSV file containing new data. This parameter is mandatory.
|
||||||
.PARAMETER CsvPath
|
.PARAMETER SheetName
|
||||||
The path to the CSV file containing data to be merged with the Excel data. This parameter is mandatory when using the CsvInput parameter set.
|
Specifies the name of the worksheet in the Excel file where data will be merged and updated. This parameter is mandatory.
|
||||||
.PARAMETER AuditResults
|
.EXAMPLE
|
||||||
An array of CISAuditResult objects from Invoke-M365SecurityAudit to be merged with the Excel data. This parameter is mandatory when using the ObjectInput parameter set. It can also accept pipeline input.
|
PS> Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -CsvPath "path\to\data.csv" -SheetName "AuditData"
|
||||||
.PARAMETER SkipUpdate
|
Updates the 'AuditData' worksheet in 'excel.xlsx' with data from 'data.csv', adding new information and the date of the update.
|
||||||
If specified, the function will return the merged data object without updating the Excel worksheet. This is useful for previewing the merged data.
|
.INPUTS
|
||||||
.EXAMPLE
|
System.String
|
||||||
PS> Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -WorksheetName "DataSheet" -CsvPath "path\to\data.csv"
|
The function accepts strings for file paths and worksheet names.
|
||||||
Merges data from 'data.csv' into 'excel.xlsx' on the 'DataSheet' worksheet and updates the worksheet with the merged data.
|
.OUTPUTS
|
||||||
.EXAMPLE
|
None
|
||||||
PS> $mergedData = Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -WorksheetName "DataSheet" -CsvPath "path\to\data.csv" -SkipUpdate
|
The function directly updates the Excel file and does not output any objects.
|
||||||
Retrieves the merged data object for preview without updating the Excel worksheet.
|
.NOTES
|
||||||
.EXAMPLE
|
- Ensure that the 'ImportExcel' module is installed and up to date to handle Excel file manipulations.
|
||||||
PS> $auditResults = Invoke-M365SecurityAudit -TenantAdminUrl "https://tenant-admin.url" -DomainName "example.com"
|
- It is recommended to back up the Excel file before running this function to avoid accidental data loss.
|
||||||
PS> Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -WorksheetName "DataSheet" -AuditResults $auditResults
|
- The CSV file should have columns that match expected headers like 'Connection', 'Details', 'FailureReason', and 'Status' for correct data mapping.
|
||||||
Merges data from the audit results into 'excel.xlsx' on the 'DataSheet' worksheet and updates the worksheet with the merged data.
|
.LINK
|
||||||
.EXAMPLE
|
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Sync-CISExcelAndCsvData
|
||||||
PS> $auditResults = Invoke-M365SecurityAudit -TenantAdminUrl "https://tenant-admin.url" -DomainName "example.com"
|
|
||||||
PS> $mergedData = Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -WorksheetName "DataSheet" -AuditResults $auditResults -SkipUpdate
|
|
||||||
Retrieves the merged data object for preview without updating the Excel worksheet.
|
|
||||||
.EXAMPLE
|
|
||||||
PS> Invoke-M365SecurityAudit -TenantAdminUrl "https://tenant-admin.url" -DomainName "example.com" | Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -WorksheetName "DataSheet"
|
|
||||||
Pipes the audit results into Sync-CISExcelAndCsvData to merge data into 'excel.xlsx' on the 'DataSheet' worksheet and updates the worksheet with the merged data.
|
|
||||||
.INPUTS
|
|
||||||
System.String, CISAuditResult[]
|
|
||||||
You can pipe CISAuditResult objects to Sync-CISExcelAndCsvData.
|
|
||||||
.OUTPUTS
|
|
||||||
Object[]
|
|
||||||
If the SkipUpdate switch is used, the function returns an array of custom objects representing the merged data.
|
|
||||||
.NOTES
|
|
||||||
- Ensure that the 'ImportExcel' module is installed and up to date.
|
|
||||||
- It is recommended to backup the Excel file before running this script to prevent accidental data loss.
|
|
||||||
- This function is part of the CIS Excel and CSV Data Management Toolkit.
|
|
||||||
.LINK
|
|
||||||
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Sync-CISExcelAndCsvData
|
|
||||||
#>
|
#>
|
||||||
|
|
||||||
function Sync-CISExcelAndCsvData {
|
function Sync-CISExcelAndCsvData {
|
||||||
[OutputType([void], [PSCustomObject[]])]
|
[OutputType([void])]
|
||||||
[CmdletBinding(DefaultParameterSetName = 'CsvInput')]
|
[CmdletBinding()]
|
||||||
param (
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[ValidateScript({ Test-Path $_ })]
|
|
||||||
[string]$ExcelPath,
|
[string]$ExcelPath,
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$WorksheetName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true, ParameterSetName = 'CsvInput')]
|
|
||||||
[ValidateScript({ Test-Path $_ })]
|
|
||||||
[string]$CsvPath,
|
[string]$CsvPath,
|
||||||
|
[string]$SheetName
|
||||||
[Parameter(Mandatory = $true, ParameterSetName = 'ObjectInput', ValueFromPipeline = $true)]
|
|
||||||
[CISAuditResult[]]$AuditResults,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[switch]$SkipUpdate
|
|
||||||
)
|
)
|
||||||
|
|
||||||
process {
|
# Import the CSV file
|
||||||
# Verify ImportExcel module is available
|
$csvData = Import-Csv -Path $CsvPath
|
||||||
$requiredModules = Get-RequiredModule -SyncFunction
|
|
||||||
foreach ($module in $requiredModules) {
|
|
||||||
Assert-ModuleAvailability -ModuleName $module.ModuleName -RequiredVersion $module.RequiredVersion -SubModuleName $module.SubModuleName
|
|
||||||
}
|
|
||||||
|
|
||||||
# Merge Excel and CSV data or Audit Results
|
# Get the current date in the specified format
|
||||||
if ($PSCmdlet.ParameterSetName -eq 'CsvInput') {
|
$currentDate = Get-Date -Format "yyyy-MM-ddTHH:mm:ss"
|
||||||
$mergedData = Merge-CISExcelAndCsvData -ExcelPath $ExcelPath -WorksheetName $WorksheetName -CsvPath $CsvPath
|
|
||||||
} else {
|
|
||||||
$mergedData = Merge-CISExcelAndCsvData -ExcelPath $ExcelPath -WorksheetName $WorksheetName -AuditResults $AuditResults
|
|
||||||
}
|
|
||||||
|
|
||||||
# Output the merged data if the user chooses to skip the update
|
# Load the Excel workbook
|
||||||
if ($SkipUpdate) {
|
$excelPackage = Open-ExcelPackage -Path $ExcelPath
|
||||||
return $mergedData
|
$worksheet = $excelPackage.Workbook.Worksheets[$SheetName]
|
||||||
} else {
|
|
||||||
# Update the Excel worksheet with the merged data
|
# Define and check new headers, including the date header
|
||||||
Update-CISExcelWorksheet -ExcelPath $ExcelPath -WorksheetName $WorksheetName -Data $mergedData
|
$lastCol = $worksheet.Dimension.End.Column
|
||||||
|
$newHeaders = @("CSV_Connection", "CSV_Status", "CSV_Date", "CSV_Details", "CSV_FailureReason")
|
||||||
|
$existingHeaders = $worksheet.Cells[1, 1, 1, $lastCol].Value
|
||||||
|
|
||||||
|
# Add new headers if they do not exist
|
||||||
|
foreach ($header in $newHeaders) {
|
||||||
|
if ($header -notin $existingHeaders) {
|
||||||
|
$lastCol++
|
||||||
|
$worksheet.Cells[1, $lastCol].Value = $header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
# Save changes made to add headers
|
||||||
|
$excelPackage.Save()
|
||||||
|
|
||||||
|
# Update the worksheet variable to include possible new columns
|
||||||
|
$worksheet = $excelPackage.Workbook.Worksheets[$SheetName]
|
||||||
|
|
||||||
|
# Mapping the headers to their corresponding column numbers
|
||||||
|
$headerMap = @{}
|
||||||
|
for ($col = 1; $col -le $worksheet.Dimension.End.Column; $col++) {
|
||||||
|
$headerMap[$worksheet.Cells[1, $col].Text] = $col
|
||||||
|
}
|
||||||
|
|
||||||
|
# For each record in CSV, find the matching row and update/add data
|
||||||
|
foreach ($row in $csvData) {
|
||||||
|
# Find the matching recommendation # row
|
||||||
|
$matchRow = $null
|
||||||
|
for ($i = 2; $i -le $worksheet.Dimension.End.Row; $i++) {
|
||||||
|
if ($worksheet.Cells[$i, $headerMap['Recommendation #']].Text -eq $row.rec) {
|
||||||
|
$matchRow = $i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update values if a matching row is found
|
||||||
|
if ($matchRow) {
|
||||||
|
foreach ($header in $newHeaders) {
|
||||||
|
if ($header -eq 'CSV_Date') {
|
||||||
|
$columnIndex = $headerMap[$header]
|
||||||
|
$worksheet.Cells[$matchRow, $columnIndex].Value = $currentDate
|
||||||
|
} else {
|
||||||
|
$csvKey = $header -replace 'CSV_', ''
|
||||||
|
$columnIndex = $headerMap[$header]
|
||||||
|
$worksheet.Cells[$matchRow, $columnIndex].Value = $row.$csvKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save the updated Excel file
|
||||||
|
$excelPackage.Save()
|
||||||
|
$excelPackage.Dispose()
|
||||||
|
}
|
@@ -18,8 +18,8 @@
|
|||||||
17,Test-RestrictTenantCreation.ps1,5.1.2.3,Ensure 'Restrict non-admin users from creating tenants' is set to 'Yes',E3,L1,0,Explicitly Not Mapped,FALSE,FALSE,FALSE,TRUE,Microsoft Graph
|
17,Test-RestrictTenantCreation.ps1,5.1.2.3,Ensure 'Restrict non-admin users from creating tenants' is set to 'Yes',E3,L1,0,Explicitly Not Mapped,FALSE,FALSE,FALSE,TRUE,Microsoft Graph
|
||||||
18,Test-PasswordHashSync.ps1,5.1.8.1,Ensure password hash sync is enabled for hybrid deployments,E3,L1,6.7,Centralize Access Control,FALSE,TRUE,TRUE,TRUE,Microsoft Graph
|
18,Test-PasswordHashSync.ps1,5.1.8.1,Ensure password hash sync is enabled for hybrid deployments,E3,L1,6.7,Centralize Access Control,FALSE,TRUE,TRUE,TRUE,Microsoft Graph
|
||||||
19,Test-AuditDisabledFalse.ps1,6.1.1,Ensure 'AuditDisabled' organizationally is set to 'False',E3,L1,8.2,Collect Audit Logs,TRUE,TRUE,TRUE,TRUE,Microsoft Graph
|
19,Test-AuditDisabledFalse.ps1,6.1.1,Ensure 'AuditDisabled' organizationally is set to 'False',E3,L1,8.2,Collect Audit Logs,TRUE,TRUE,TRUE,TRUE,Microsoft Graph
|
||||||
20,Test-MailboxAuditingE3.ps1,6.1.2,Ensure mailbox auditing for Office E3 users is Enabled,E3,L1,8.2,Collect audit logs.,TRUE,TRUE,TRUE,TRUE,AzureAD | EXO | Microsoft Graph
|
20,Test-MailboxAuditingE3.ps1,6.1.2,Ensure mailbox auditing for Office E3 users is Enabled,E3,L1,8.2,Collect audit logs.,TRUE,TRUE,TRUE,TRUE,EXO | Microsoft Graph
|
||||||
21,Test-MailboxAuditingE5.ps1,6.1.3,Ensure mailbox auditing for Office E5 users is Enabled,E5,L1,8.2,Collect audit logs.,TRUE,TRUE,TRUE,TRUE,AzureAD | EXO | Microsoft Graph
|
21,Test-MailboxAuditingE5.ps1,6.1.3,Ensure mailbox auditing for Office E5 users is Enabled,E5,L1,8.2,Collect audit logs.,TRUE,TRUE,TRUE,TRUE,EXO | Microsoft Graph
|
||||||
22,Test-BlockMailForwarding.ps1,6.2.1,Ensure all forms of mail forwarding are blocked and/or disabled,E3,L1,0,Explicitly Not Mapped,FALSE,FALSE,FALSE,TRUE,EXO
|
22,Test-BlockMailForwarding.ps1,6.2.1,Ensure all forms of mail forwarding are blocked and/or disabled,E3,L1,0,Explicitly Not Mapped,FALSE,FALSE,FALSE,TRUE,EXO
|
||||||
23,Test-NoWhitelistDomains.ps1,6.2.2,Ensure mail transport rules do not whitelist specific domains,E3,L1,0,Explicitly Not Mapped,FALSE,FALSE,FALSE,TRUE,EXO
|
23,Test-NoWhitelistDomains.ps1,6.2.2,Ensure mail transport rules do not whitelist specific domains,E3,L1,0,Explicitly Not Mapped,FALSE,FALSE,FALSE,TRUE,EXO
|
||||||
24,Test-IdentifyExternalEmail.ps1,6.2.3,Ensure email from external senders is identified,E3,L1,0,Explicitly Not Mapped,FALSE,FALSE,FALSE,TRUE,EXO
|
24,Test-IdentifyExternalEmail.ps1,6.2.3,Ensure email from external senders is identified,E3,L1,0,Explicitly Not Mapped,FALSE,FALSE,FALSE,TRUE,EXO
|
||||||
|
|
@@ -20,7 +20,12 @@ function Test-AdministrativeAccountCompliance {
|
|||||||
try {
|
try {
|
||||||
# Retrieve all admin roles
|
# Retrieve all admin roles
|
||||||
Write-Verbose "Retrieving all admin roles"
|
Write-Verbose "Retrieving all admin roles"
|
||||||
$adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Admin*" }
|
# Get the DisplayNames of all admin roles
|
||||||
|
$adminRoleNames = (Get-MgDirectoryRole | Where-Object { $null -ne $_.RoleTemplateId }).DisplayName
|
||||||
|
|
||||||
|
# Use the DisplayNames to filter the roles in Get-MgRoleManagementDirectoryRoleDefinition
|
||||||
|
$adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { ($adminRoleNames -contains $_.DisplayName) -and ($_.DisplayName -ne "Directory Synchronization Accounts")}
|
||||||
|
|
||||||
$adminRoleUsers = @()
|
$adminRoleUsers = @()
|
||||||
|
|
||||||
# Loop through each admin role to get role assignments and user details
|
# Loop through each admin role to get role assignments and user details
|
||||||
@@ -47,9 +52,10 @@ function Test-AdministrativeAccountCompliance {
|
|||||||
|
|
||||||
# Condition C: Check if the account has no other licenses
|
# Condition C: Check if the account has no other licenses
|
||||||
$hasInvalidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -notcontains $_ }
|
$hasInvalidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -notcontains $_ }
|
||||||
|
$invalidLicenses = $licenses.SkuPartNumber | Where-Object { $validLicenses -notcontains $_ }
|
||||||
$applicationAssignmentStatus = if ($hasInvalidLicense) { "Fail" } else { "Pass" }
|
$applicationAssignmentStatus = if ($hasInvalidLicense) { "Fail" } else { "Pass" }
|
||||||
|
|
||||||
Write-Verbose "User: $($userDetails.UserPrincipalName), Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Other Applications Assigned: $applicationAssignmentStatus"
|
Write-Verbose "User: $($userDetails.UserPrincipalName), Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Invalid Licenses: $($invalidLicenses -join ', ')"
|
||||||
|
|
||||||
# Collect user information
|
# Collect user information
|
||||||
$adminRoleUsers += [PSCustomObject]@{
|
$adminRoleUsers += [PSCustomObject]@{
|
||||||
@@ -95,13 +101,14 @@ function Test-AdministrativeAccountCompliance {
|
|||||||
$failureReasons = $failureReasons -join "`n"
|
$failureReasons = $failureReasons -join "`n"
|
||||||
$failureReason = if ($nonCompliantUsers) {
|
$failureReason = if ($nonCompliantUsers) {
|
||||||
"Non-Compliant Accounts: $($nonCompliantUsers.Count)"
|
"Non-Compliant Accounts: $($nonCompliantUsers.Count)"
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
"Compliant Accounts: $($uniqueAdminRoleUsers.Count)"
|
"Compliant Accounts: $($uniqueAdminRoleUsers.Count)"
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $nonCompliantUsers.Count -eq 0
|
$result = $nonCompliantUsers.Count -eq 0
|
||||||
$status = if ($result) { 'Pass' } else { 'Fail' }
|
$status = if ($result) { 'Pass' } else { 'Fail' }
|
||||||
$details = if ($nonCompliantUsers) { "Non-compliant accounts: `nUsername | Roles | Cloud-Only Status | Entra ID License Status | Other Applications Assigned Status`n$failureReasons" } else { "N/A" }
|
$details = if ($nonCompliantUsers) { "Username | Roles | Cloud-Only Status | EntraID P1/P2 License Status | Other Applications Assigned Status`n$failureReasons" } else { "N/A" }
|
||||||
|
|
||||||
Write-Verbose "Assessment completed. Result: $status"
|
Write-Verbose "Assessment completed. Result: $status"
|
||||||
|
|
||||||
|
@@ -64,7 +64,7 @@ function Test-BlockMailForwarding {
|
|||||||
if ($nonCompliantSpamPoliciesArray.Count -gt 0) {
|
if ($nonCompliantSpamPoliciesArray.Count -gt 0) {
|
||||||
# Fail Condition B
|
# Fail Condition B
|
||||||
$failureReasons += "Outbound spam policies allowing automatic forwarding found."
|
$failureReasons += "Outbound spam policies allowing automatic forwarding found."
|
||||||
$details += "Outbound Spam Policies Details:`nPolicy|AutoForwardingMode"
|
$details += "Policy|AutoForwardingMode"
|
||||||
$details += $nonCompliantSpamPoliciesArray | ForEach-Object {
|
$details += $nonCompliantSpamPoliciesArray | ForEach-Object {
|
||||||
"$($_.Name)|$($_.AutoForwardingMode)"
|
"$($_.Name)|$($_.AutoForwardingMode)"
|
||||||
}
|
}
|
||||||
|
@@ -29,105 +29,124 @@ function Test-MailboxAuditingE3 {
|
|||||||
# Dot source the class script if necessary
|
# Dot source the class script if necessary
|
||||||
#. .\source\Classes\CISAuditResult.ps1
|
#. .\source\Classes\CISAuditResult.ps1
|
||||||
|
|
||||||
$e3SkuPartNumbers = @("ENTERPRISEPACK", "OFFICESUBSCRIPTION")
|
$e3SkuPartNumber = "SPE_E3"
|
||||||
$AdminActions = @("ApplyRecord", "Copy", "Create", "FolderBind", "HardDelete", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules")
|
|
||||||
$DelegateActions = @("ApplyRecord", "Create", "FolderBind", "HardDelete", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateFolderPermissions", "UpdateInboxRules")
|
$actionDictionaries = Get-Action -Dictionaries
|
||||||
$OwnerActions = @("ApplyRecord", "Create", "HardDelete", "MailboxLogin", "Move", "MoveToDeletedItems", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules")
|
# E3 specific actions
|
||||||
|
$AdminActions = $actionDictionaries.AdminActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }
|
||||||
|
$DelegateActions = $actionDictionaries.DelegateActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed") }
|
||||||
|
$OwnerActions = $actionDictionaries.OwnerActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }
|
||||||
|
|
||||||
$allFailures = @()
|
$allFailures = @()
|
||||||
$allUsers = Get-AzureADUser -All $true
|
$founde3Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e3SkuPartNumber }
|
||||||
$processedUsers = @{} # Dictionary to track processed users
|
$processedUsers = @{} # Dictionary to track processed users
|
||||||
$recnum = "6.1.2"
|
$recnum = "6.1.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
process {
|
process {
|
||||||
try {
|
if ($founde3Sku.Count -ne 0) {
|
||||||
foreach ($user in $allUsers) {
|
$allUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde3Sku.SkuId) )" -All
|
||||||
if ($processedUsers.ContainsKey($user.UserPrincipalName)) {
|
$mailboxes = Get-EXOMailbox -PropertySets Audit
|
||||||
Write-Verbose "Skipping already processed user: $($user.UserPrincipalName)"
|
try {
|
||||||
continue
|
foreach ($user in $allUsers) {
|
||||||
}
|
if ($processedUsers.ContainsKey($user.UserPrincipalName)) {
|
||||||
|
Write-Verbose "Skipping already processed user: $($user.UserPrincipalName)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
$licenseDetails = Get-MgUserLicenseDetail -UserId $user.UserPrincipalName
|
|
||||||
$hasOfficeE3 = ($licenseDetails | Where-Object { $_.SkuPartNumber -in $e3SkuPartNumbers }).Count -gt 0
|
|
||||||
Write-Verbose "Evaluating user $($user.UserPrincipalName) for Office E3 license."
|
|
||||||
|
|
||||||
if ($hasOfficeE3) {
|
|
||||||
$userUPN = $user.UserPrincipalName
|
$userUPN = $user.UserPrincipalName
|
||||||
$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit
|
$mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $user.UserPrincipalName }
|
||||||
|
|
||||||
|
$missingAdminActions = @()
|
||||||
|
$missingDelegateActions = @()
|
||||||
|
$missingOwnerActions = @()
|
||||||
|
|
||||||
$missingActions = @()
|
|
||||||
if ($mailbox.AuditEnabled) {
|
if ($mailbox.AuditEnabled) {
|
||||||
foreach ($action in $AdminActions) {
|
foreach ($action in $AdminActions) {
|
||||||
# Condition B: Checking if the `AuditAdmin` actions include required actions
|
if ($mailbox.AuditAdmin -notcontains $action) {
|
||||||
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" }
|
$missingAdminActions += (Get-Action -Actions $action -ActionType "Admin")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
foreach ($action in $DelegateActions) {
|
foreach ($action in $DelegateActions) {
|
||||||
# Condition C: Checking if the `AuditDelegate` actions include required actions
|
if ($mailbox.AuditDelegate -notcontains $action) {
|
||||||
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" }
|
$missingDelegateActions += (Get-Action -Actions $action -ActionType "Delegate")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
foreach ($action in $OwnerActions) {
|
foreach ($action in $OwnerActions) {
|
||||||
# Condition D: Checking if the `AuditOwner` actions include required actions
|
if ($mailbox.AuditOwner -notcontains $action) {
|
||||||
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" }
|
$missingOwnerActions += (Get-Action -Actions $action -ActionType "Owner")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($missingActions.Count -gt 0) {
|
if ($missingAdminActions.Count -gt 0 -or $missingDelegateActions.Count -gt 0 -or $missingOwnerActions.Count -gt 0) {
|
||||||
$formattedActions = Format-MissingAction -missingActions $missingActions
|
$allFailures += "$userUPN|True|$($missingAdminActions -join ',')|$($missingDelegateActions -join ',')|$($missingOwnerActions -join ',')"
|
||||||
$allFailures += "$userUPN|True|$($formattedActions.Admin)|$($formattedActions.Delegate)|$($formattedActions.Owner)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Condition A: Checking if mailbox audit logging is enabled
|
$allFailures += "$userUPN|False|||" # Condition A for fail
|
||||||
$allFailures += "$userUPN|False|||"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mark the user as processed
|
# Mark the user as processed
|
||||||
$processedUsers[$user.UserPrincipalName] = $true
|
$processedUsers[$user.UserPrincipalName] = $true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# Prepare failure reasons and details based on compliance
|
# Prepare failure reasons and details based on compliance
|
||||||
$failureReasons = if ($allFailures.Count -eq 0) { "N/A" } else { "Audit issues detected." }
|
if ($allFailures.Count -eq 0) {
|
||||||
$details = if ($allFailures.Count -eq 0) {
|
$failureReasons = "N/A"
|
||||||
"All Office E3 users have correct mailbox audit settings."
|
}
|
||||||
}
|
else {
|
||||||
else {
|
$failureReasons = "Audit issues detected."
|
||||||
"UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing`n" + ($allFailures -join "`n")
|
}
|
||||||
}
|
$details = if ($allFailures.Count -eq 0) {
|
||||||
|
"All Office E3 users have correct mailbox audit settings."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
"UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing`n" + ($allFailures -join "`n")
|
||||||
|
}
|
||||||
|
|
||||||
# Populate the audit result
|
# Populate the audit result
|
||||||
|
$params = @{
|
||||||
|
Rec = $recnum
|
||||||
|
Result = $allFailures.Count -eq 0
|
||||||
|
Status = if ($allFailures.Count -eq 0) { "Pass" } else { "Fail" }
|
||||||
|
Details = $details
|
||||||
|
FailureReason = $failureReasons
|
||||||
|
}
|
||||||
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "An error occurred during the test: $_"
|
||||||
|
|
||||||
|
# Retrieve the description from the test definitions
|
||||||
|
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
||||||
|
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
||||||
|
|
||||||
|
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
||||||
|
|
||||||
|
# Call Initialize-CISAuditResult with error parameters
|
||||||
|
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
$params = @{
|
$params = @{
|
||||||
Rec = $recnum
|
Rec = $recnum
|
||||||
Result = $allFailures.Count -eq 0
|
Result = $false
|
||||||
Status = if ($allFailures.Count -eq 0) { "Pass" } else { "Fail" }
|
Status = "Fail"
|
||||||
Details = $details
|
Details = "No M365 E3 licenses found."
|
||||||
FailureReason = $failureReasons
|
FailureReason = "The audit is for M365 E3 licenses, but no such licenses were found."
|
||||||
}
|
}
|
||||||
$auditResult = Initialize-CISAuditResult @params
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
catch {
|
|
||||||
Write-Error "An error occurred during the test: $_"
|
|
||||||
|
|
||||||
# Retrieve the description from the test definitions
|
|
||||||
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
|
||||||
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
|
||||||
|
|
||||||
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
|
||||||
|
|
||||||
# Call Initialize-CISAuditResult with error parameters
|
|
||||||
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end {
|
end {
|
||||||
#$verbosePreference = 'Continue'
|
|
||||||
$detailsLength = $details.Length
|
$detailsLength = $details.Length
|
||||||
Write-Verbose "Character count of the details: $detailsLength"
|
Write-Verbose "Character count of the details: $detailsLength"
|
||||||
|
|
||||||
if ($detailsLength -gt 32767) {
|
if ($detailsLength -gt 32767) {
|
||||||
Write-Verbose "Warning: The character count exceeds the limit for Excel cells."
|
Write-Verbose "Warning: The character count exceeds the limit for Excel cells."
|
||||||
}
|
}
|
||||||
#$verbosePreference = 'SilentlyContinue'
|
|
||||||
return $auditResult
|
return $auditResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,104 +27,126 @@ function Test-MailboxAuditingE5 {
|
|||||||
# - Condition C: AuditDelegate actions do not include all of the following: ApplyRecord, Create, HardDelete, MailItemsAccessed, MoveToDeletedItems, SendAs, SendOnBehalf, SoftDelete, Update, UpdateFolderPermissions, UpdateInboxRules.
|
# - Condition C: AuditDelegate actions do not include all of the following: ApplyRecord, Create, HardDelete, MailItemsAccessed, MoveToDeletedItems, SendAs, SendOnBehalf, SoftDelete, Update, UpdateFolderPermissions, UpdateInboxRules.
|
||||||
# - Condition D: AuditOwner actions do not include all of the following: ApplyRecord, HardDelete, MailItemsAccessed, MoveToDeletedItems, Send, SoftDelete, Update, UpdateCalendarDelegation, UpdateFolderPermissions, UpdateInboxRules.
|
# - Condition D: AuditOwner actions do not include all of the following: ApplyRecord, HardDelete, MailItemsAccessed, MoveToDeletedItems, Send, SoftDelete, Update, UpdateCalendarDelegation, UpdateFolderPermissions, UpdateInboxRules.
|
||||||
|
|
||||||
$e5SkuPartNumbers = @("SPE_E5", "ENTERPRISEPREMIUM", "OFFICEE5")
|
$e5SkuPartNumber = "SPE_E5"
|
||||||
$AdminActions = @("ApplyRecord", "Copy", "Create", "FolderBind", "HardDelete", "MailItemsAccessed", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "Send", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules")
|
$founde5Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber }
|
||||||
$DelegateActions = @("ApplyRecord", "Create", "FolderBind", "HardDelete", "MailItemsAccessed", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateFolderPermissions", "UpdateInboxRules")
|
|
||||||
$OwnerActions = @("ApplyRecord", "Create", "HardDelete", "MailboxLogin", "Move", "MailItemsAccessed", "MoveToDeletedItems", "Send", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules")
|
$actionDictionaries = Get-Action -Dictionaries
|
||||||
|
$AdminActions = $actionDictionaries.AdminActions.Keys
|
||||||
|
$DelegateActions = $actionDictionaries.DelegateActions.Keys
|
||||||
|
$OwnerActions = $actionDictionaries.OwnerActions.Keys
|
||||||
|
|
||||||
$allFailures = @()
|
$allFailures = @()
|
||||||
$allUsers = Get-AzureADUser -All $true
|
$processedUsers = @{}
|
||||||
$processedUsers = @{} # Dictionary to track processed users
|
|
||||||
$recnum = "6.1.3"
|
$recnum = "6.1.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
process {
|
process {
|
||||||
try {
|
if (($founde5Sku.count) -ne 0) {
|
||||||
foreach ($user in $allUsers) {
|
$allUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde5Sku.SkuId) )" -All
|
||||||
if ($processedUsers.ContainsKey($user.UserPrincipalName)) {
|
$mailboxes = Get-EXOMailbox -PropertySets Audit
|
||||||
Write-Verbose "Skipping already processed user: $($user.UserPrincipalName)"
|
try {
|
||||||
continue
|
foreach ($user in $allUsers) {
|
||||||
}
|
if ($processedUsers.ContainsKey($user.UserPrincipalName)) {
|
||||||
|
Write-Verbose "Skipping already processed user: $($user.UserPrincipalName)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
$licenseDetails = Get-MgUserLicenseDetail -UserId $user.UserPrincipalName
|
$mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $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
|
$userUPN = $user.UserPrincipalName
|
||||||
$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit
|
|
||||||
|
|
||||||
$missingActions = @()
|
$missingAdminActions = @()
|
||||||
|
$missingDelegateActions = @()
|
||||||
|
$missingOwnerActions = @()
|
||||||
|
|
||||||
if ($mailbox.AuditEnabled) {
|
if ($mailbox.AuditEnabled) {
|
||||||
# Validate Admin actions
|
# Validate Admin actions
|
||||||
foreach ($action in $AdminActions) {
|
foreach ($action in $AdminActions) {
|
||||||
if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" } # Condition B
|
if ($mailbox.AuditAdmin -notcontains $action) {
|
||||||
|
$missingAdminActions += (Get-Action -Actions $action -ActionType "Admin") # Condition B
|
||||||
|
}
|
||||||
}
|
}
|
||||||
# Validate Delegate actions
|
# Validate Delegate actions
|
||||||
foreach ($action in $DelegateActions) {
|
foreach ($action in $DelegateActions) {
|
||||||
if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" } # Condition C
|
if ($mailbox.AuditDelegate -notcontains $action) {
|
||||||
|
$missingDelegateActions += (Get-Action -Actions $action -ActionType "Delegate") # Condition C
|
||||||
|
}
|
||||||
}
|
}
|
||||||
# Validate Owner actions
|
# Validate Owner actions
|
||||||
foreach ($action in $OwnerActions) {
|
foreach ($action in $OwnerActions) {
|
||||||
if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" } # Condition D
|
if ($mailbox.AuditOwner -notcontains $action) {
|
||||||
|
$missingOwnerActions += (Get-Action -Actions $action -ActionType "Owner") # Condition D
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($missingActions.Count -gt 0) {
|
if ($missingAdminActions.Count -gt 0 -or $missingDelegateActions.Count -gt 0 -or $missingOwnerActions.Count -gt 0) {
|
||||||
$formattedActions = Format-MissingAction -missingActions $missingActions
|
$allFailures += "$userUPN|True|$($missingAdminActions -join ',')|$($missingDelegateActions -join ',')|$($missingOwnerActions -join ',')"
|
||||||
$allFailures += "$userUPN|True|$($formattedActions.Admin)|$($formattedActions.Delegate)|$($formattedActions.Owner)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$allFailures += "$userUPN|False|||"
|
$allFailures += "$userUPN|False|||" # Condition A for fail
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mark the user as processed
|
# Mark the user as processed
|
||||||
$processedUsers[$user.UserPrincipalName] = $true
|
$processedUsers[$user.UserPrincipalName] = $true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# Prepare failure reasons and details based on compliance
|
# Prepare failure reasons and details based on compliance
|
||||||
$failureReasons = if ($allFailures.Count -eq 0) { "N/A" } else { "Audit issues detected." }
|
if ($allFailures.Count -eq 0) {
|
||||||
$details = if ($allFailures.Count -eq 0) {
|
$failureReasons = "N/A"
|
||||||
"All Office E5 users have correct mailbox audit settings." # Condition A for pass
|
}
|
||||||
}
|
else {
|
||||||
else {
|
$failureReasons = "Audit issues detected."
|
||||||
"UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing`n" + ($allFailures -join "`n") # Condition A for fail
|
}
|
||||||
|
$details = if ($allFailures.Count -eq 0) {
|
||||||
|
"All Office E5 users have correct mailbox audit settings." # Condition A for pass
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
"UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing`n" + ($allFailures -join "`n") # Condition A for fail
|
||||||
|
}
|
||||||
|
# $details = Initialize-LargeTestTable -lineCount 3000 # Adjust the lineCount to exceed 32,000 characters
|
||||||
|
# Populate the audit result
|
||||||
|
$params = @{
|
||||||
|
Rec = $recnum
|
||||||
|
Result = $allFailures.Count -eq 0
|
||||||
|
Status = if ($allFailures.Count -eq 0) { "Pass" } else { "Fail" }
|
||||||
|
Details = $details
|
||||||
|
FailureReason = $failureReasons
|
||||||
|
}
|
||||||
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "An error occurred during the test: $_"
|
||||||
|
|
||||||
# Populate the audit result
|
# Retrieve the description from the test definitions
|
||||||
|
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
||||||
|
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
||||||
|
|
||||||
|
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
||||||
|
|
||||||
|
# Call Initialize-CISAuditResult with error parameters
|
||||||
|
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
$params = @{
|
$params = @{
|
||||||
Rec = $recnum
|
Rec = $recnum
|
||||||
Result = $allFailures.Count -eq 0
|
Result = $false
|
||||||
Status = if ($allFailures.Count -eq 0) { "Pass" } else { "Fail" }
|
Status = "Fail"
|
||||||
Details = $details
|
Details = "No M365 E5 licenses found."
|
||||||
FailureReason = $failureReasons
|
FailureReason = "The audit is for M365 E5 licenses, but no such licenses were found."
|
||||||
}
|
}
|
||||||
$auditResult = Initialize-CISAuditResult @params
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
catch {
|
|
||||||
Write-Error "An error occurred during the test: $_"
|
|
||||||
|
|
||||||
# Retrieve the description from the test definitions
|
|
||||||
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
|
||||||
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
|
||||||
|
|
||||||
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
|
||||||
|
|
||||||
# Call Initialize-CISAuditResult with error parameters
|
|
||||||
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end {
|
end {
|
||||||
#$verbosePreference = 'Continue'
|
|
||||||
$detailsLength = $details.Length
|
$detailsLength = $details.Length
|
||||||
Write-Verbose "Character count of the details: $detailsLength"
|
Write-Verbose "Character count of the details: $detailsLength"
|
||||||
|
|
||||||
if ($detailsLength -gt 32767) {
|
if ($detailsLength -gt 32767) {
|
||||||
Write-Verbose "Warning: The character count exceeds the limit for Excel cells."
|
Write-Verbose "Warning: The character count exceeds the limit for Excel cells."
|
||||||
}
|
}
|
||||||
#$verbosePreference = 'SilentlyContinue'
|
|
||||||
return $auditResult
|
return $auditResult
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,19 +1,12 @@
|
|||||||
function Test-SafeAttachmentsPolicy {
|
function Test-SafeAttachmentsPolicy {
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
[OutputType([CISAuditResult])]
|
[OutputType([CISAuditResult])]
|
||||||
param (
|
param ()
|
||||||
# Aligned
|
|
||||||
# Parameters can be added if needed
|
|
||||||
)
|
|
||||||
|
|
||||||
begin {
|
begin {
|
||||||
# Dot source the class script if necessary
|
|
||||||
#. .\source\Classes\CISAuditResult.ps1
|
|
||||||
|
|
||||||
# Initialization code, if needed
|
|
||||||
$recnum = "2.1.4"
|
$recnum = "2.1.4"
|
||||||
|
|
||||||
<#
|
<#
|
||||||
Conditions for 2.1.4 (L2) Ensure Safe Attachments policy is enabled
|
Conditions for 2.1.4 (L2) Ensure Safe Attachments policy is enabled
|
||||||
|
|
||||||
Validate test for a pass:
|
Validate test for a pass:
|
||||||
@@ -31,58 +24,88 @@ function Test-SafeAttachmentsPolicy {
|
|||||||
- Condition B: The policy does not cover all recipients within the organization.
|
- Condition B: The policy does not cover all recipients within the organization.
|
||||||
- Condition C: The policy action is not set to "Dynamic Delivery" or "Quarantine".
|
- Condition C: The policy action is not set to "Dynamic Delivery" or "Quarantine".
|
||||||
- Condition D: The policy is disabled.
|
- Condition D: The policy is disabled.
|
||||||
#>
|
#>
|
||||||
}
|
}
|
||||||
|
|
||||||
process {
|
process {
|
||||||
try {
|
if (Get-Command Get-SafeAttachmentPolicy -ErrorAction SilentlyContinue) {
|
||||||
# 2.1.4 (L2) Ensure Safe Attachments policy is enabled
|
try {
|
||||||
|
# Retrieve all Safe Attachment policies where Enable is set to True
|
||||||
|
$safeAttachmentPolicies = Get-SafeAttachmentPolicy -ErrorAction SilentlyContinue | Where-Object { $_.Enable -eq $true }
|
||||||
|
# Check if any Safe Attachments policy is enabled (Condition A)
|
||||||
|
$result = $null -ne $safeAttachmentPolicies -and $safeAttachmentPolicies.Count -gt 0
|
||||||
|
|
||||||
# Retrieve all Safe Attachment policies where Enable is set to True
|
# Initialize details and failure reasons
|
||||||
$safeAttachmentPolicies = Get-SafeAttachmentPolicy | Where-Object { $_.Enable -eq $true }
|
$details = @()
|
||||||
|
$failureReasons = @()
|
||||||
|
|
||||||
# Condition A: Check if any Safe Attachments policy is enabled
|
foreach ($policy in $safeAttachmentPolicies) {
|
||||||
$result = $null -ne $safeAttachmentPolicies -and $safeAttachmentPolicies.Count -gt 0
|
# Initialize policy detail and failed status
|
||||||
|
$failed = $false
|
||||||
|
|
||||||
# Condition B, C, D: Additional checks can be added here if more detailed policy attributes are required
|
# Check if the policy action is set to "Dynamic Delivery" or "Quarantine" (Condition C)
|
||||||
|
if ($policy.Action -notin @("DynamicDelivery", "Quarantine")) {
|
||||||
|
$failureReasons += "Policy '$($policy.Name)' action is not set to 'Dynamic Delivery' or 'Quarantine'."
|
||||||
|
$failed = $true
|
||||||
|
}
|
||||||
|
|
||||||
# Determine details and failure reasons based on the presence of enabled policies
|
# Check if the policy is not disabled (Condition D)
|
||||||
$details = if ($result) {
|
if (-not $policy.Enable) {
|
||||||
"Enabled Safe Attachments Policies: $($safeAttachmentPolicies.Name -join ', ')"
|
$failureReasons += "Policy '$($policy.Name)' is disabled."
|
||||||
}
|
$failed = $true
|
||||||
else {
|
}
|
||||||
"No Safe Attachments Policies are enabled."
|
|
||||||
|
# Add policy details to the details array
|
||||||
|
$details += [PSCustomObject]@{
|
||||||
|
Policy = $policy.Name
|
||||||
|
Enabled = $policy.Enable
|
||||||
|
Action = $policy.Action
|
||||||
|
Failed = $failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# The result is a pass if there are no failure reasons
|
||||||
|
$result = $failureReasons.Count -eq 0
|
||||||
|
|
||||||
|
# Format details for output manually
|
||||||
|
$detailsString = "Policy|Enabled|Action|Failed`n" + ($details |
|
||||||
|
ForEach-Object {"$($_.Policy)|$($_.Enabled)|$($_.Action)|$($_.Failed)`n"}
|
||||||
|
)
|
||||||
|
$failureReasonsString = ($failureReasons | ForEach-Object { $_ }) -join ' '
|
||||||
|
|
||||||
|
# Create and populate the CISAuditResult object
|
||||||
|
$params = @{
|
||||||
|
Rec = $recnum
|
||||||
|
Result = $result
|
||||||
|
Status = if ($result) { "Pass" } else { "Fail" }
|
||||||
|
Details = $detailsString
|
||||||
|
FailureReason = if ($result) { "N/A" } else { $failureReasonsString }
|
||||||
|
}
|
||||||
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "An error occurred during the test: $_"
|
||||||
|
|
||||||
$failureReasons = if ($result) {
|
# Retrieve the description from the test definitions
|
||||||
"N/A"
|
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
||||||
}
|
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
||||||
else {
|
|
||||||
"Safe Attachments policy is not enabled."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create and populate the CISAuditResult object
|
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
||||||
|
|
||||||
|
# Call Initialize-CISAuditResult with error parameters
|
||||||
|
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
$params = @{
|
$params = @{
|
||||||
Rec = $recnum
|
Rec = $recnum
|
||||||
Result = $result
|
Result = $false
|
||||||
Status = if ($result) { "Pass" } else { "Fail" }
|
Status = "Fail"
|
||||||
Details = $details
|
Details = "No Safe Attachments policies found."
|
||||||
FailureReason = $failureReasons
|
FailureReason = "The audit needs Safe Attachment features available or required EXO commands will not be available otherwise."
|
||||||
}
|
}
|
||||||
$auditResult = Initialize-CISAuditResult @params
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
catch {
|
|
||||||
Write-Error "An error occurred during the test: $_"
|
|
||||||
|
|
||||||
# Retrieve the description from the test definitions
|
|
||||||
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
|
||||||
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
|
||||||
|
|
||||||
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
|
||||||
|
|
||||||
# Call Initialize-CISAuditResult with error parameters
|
|
||||||
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end {
|
end {
|
||||||
@@ -90,4 +113,3 @@ function Test-SafeAttachmentsPolicy {
|
|||||||
return $auditResult
|
return $auditResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -31,61 +31,71 @@ function Test-SafeAttachmentsTeams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process {
|
process {
|
||||||
try {
|
if (Get-Command Get-AtpPolicyForO365 -ErrorAction SilentlyContinue) {
|
||||||
# 2.1.5 (L2) Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled
|
try {
|
||||||
|
# 2.1.5 (L2) Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
# Retrieve the ATP policies for Office 365 and check Safe Attachments settings
|
# Condition A: Check Safe Attachments for SharePoint
|
||||||
$atpPolicies = Get-AtpPolicyForO365
|
# Condition B: Check Safe Attachments for OneDrive
|
||||||
|
# Condition C: Check Safe Attachments for Microsoft Teams
|
||||||
|
|
||||||
# Check if the required ATP policies are enabled
|
# Determine the result based on the ATP policy settings
|
||||||
$atpPolicyResult = $atpPolicies | Where-Object {
|
$result = $null -ne $atpPolicyResult
|
||||||
$_.EnableATPForSPOTeamsODB -eq $true -and
|
$details = if ($result) {
|
||||||
$_.EnableSafeDocs -eq $true -and
|
"ATP for SharePoint, OneDrive, and Teams is enabled with correct settings."
|
||||||
$_.AllowSafeDocsOpen -eq $false
|
}
|
||||||
|
else {
|
||||||
|
"ATP for SharePoint, OneDrive, and Teams is not enabled with correct settings."
|
||||||
|
}
|
||||||
|
|
||||||
|
$failureReasons = if ($result) {
|
||||||
|
"N/A"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
"ATP policy for SharePoint, OneDrive, and Microsoft Teams is not correctly configured."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create and populate the CISAuditResult object
|
||||||
|
$params = @{
|
||||||
|
Rec = $recnum
|
||||||
|
Result = $result
|
||||||
|
Status = if ($result) { "Pass" } else { "Fail" }
|
||||||
|
Details = $details
|
||||||
|
FailureReason = $failureReasons
|
||||||
|
}
|
||||||
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "An error occurred during the test: $_"
|
||||||
|
|
||||||
# Condition A: Check Safe Attachments for SharePoint
|
# Retrieve the description from the test definitions
|
||||||
# Condition B: Check Safe Attachments for OneDrive
|
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
||||||
# Condition C: Check Safe Attachments for Microsoft Teams
|
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
||||||
|
|
||||||
# Determine the result based on the ATP policy settings
|
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
||||||
$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."
|
|
||||||
}
|
|
||||||
|
|
||||||
$failureReasons = if ($result) {
|
# Call Initialize-CISAuditResult with error parameters
|
||||||
"N/A"
|
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
"ATP policy for SharePoint, OneDrive, and Microsoft Teams is not correctly configured."
|
else {
|
||||||
}
|
|
||||||
|
|
||||||
# Create and populate the CISAuditResult object
|
|
||||||
$params = @{
|
$params = @{
|
||||||
Rec = $recnum
|
Rec = $recnum
|
||||||
Result = $result
|
Result = $false
|
||||||
Status = if ($result) { "Pass" } else { "Fail" }
|
Status = "Fail"
|
||||||
Details = $details
|
Details = "No M365 E5 licenses found."
|
||||||
FailureReason = $failureReasons
|
FailureReason = "The audit is for M365 E5 licenses and the required EXO commands will not be available otherwise."
|
||||||
}
|
}
|
||||||
$auditResult = Initialize-CISAuditResult @params
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
catch {
|
|
||||||
Write-Error "An error occurred during the test: $_"
|
|
||||||
|
|
||||||
# Retrieve the description from the test definitions
|
|
||||||
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
|
||||||
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
|
||||||
|
|
||||||
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
|
||||||
|
|
||||||
# Call Initialize-CISAuditResult with error parameters
|
|
||||||
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end {
|
end {
|
||||||
|
@@ -40,62 +40,72 @@ function Test-SafeLinksOfficeApps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process {
|
process {
|
||||||
try {
|
if (Get-Command Get-SafeLinksPolicy -ErrorAction SilentlyContinue) {
|
||||||
# 2.1.1 (L2) Ensure Safe Links for Office Applications is Enabled
|
try {
|
||||||
|
# 2.1.1 (L2) Ensure Safe Links for Office Applications is Enabled
|
||||||
|
# Retrieve all Safe Links policies
|
||||||
|
$policies = Get-SafeLinksPolicy
|
||||||
|
# Initialize the details collection
|
||||||
|
$misconfiguredDetails = @()
|
||||||
|
|
||||||
# Retrieve all Safe Links policies
|
foreach ($policy in $policies) {
|
||||||
$policies = Get-SafeLinksPolicy
|
# Get the detailed configuration of each policy
|
||||||
|
$policyDetails = Get-SafeLinksPolicy -Identity $policy.Name
|
||||||
|
|
||||||
# Initialize the details collection
|
# Check each required property and record failures
|
||||||
$misconfiguredDetails = @()
|
# Condition A: Checking policy settings
|
||||||
|
$failures = @()
|
||||||
|
if ($policyDetails.EnableSafeLinksForEmail -ne $true) { $failures += "EnableSafeLinksForEmail: False" } # Email: On
|
||||||
|
if ($policyDetails.EnableSafeLinksForTeams -ne $true) { $failures += "EnableSafeLinksForTeams: False" } # Teams: On
|
||||||
|
if ($policyDetails.EnableSafeLinksForOffice -ne $true) { $failures += "EnableSafeLinksForOffice: False" } # Office 365 Apps: On
|
||||||
|
if ($policyDetails.TrackClicks -ne $true) { $failures += "TrackClicks: False" } # Click protection settings: On
|
||||||
|
if ($policyDetails.AllowClickThrough -ne $false) { $failures += "AllowClickThrough: True" } # Do not track when users click safe links: Off
|
||||||
|
|
||||||
foreach ($policy in $policies) {
|
# Only add details for policies that have misconfigurations
|
||||||
# Get the detailed configuration of each policy
|
if ($failures.Count -gt 0) {
|
||||||
$policyDetails = Get-SafeLinksPolicy -Identity $policy.Name
|
$misconfiguredDetails += "Policy: $($policy.Name); Failures: $($failures -join ', ')"
|
||||||
|
}
|
||||||
# Check each required property and record failures
|
|
||||||
# Condition A: Checking policy settings
|
|
||||||
$failures = @()
|
|
||||||
if ($policyDetails.EnableSafeLinksForEmail -ne $true) { $failures += "EnableSafeLinksForEmail: False" } # Email: On
|
|
||||||
if ($policyDetails.EnableSafeLinksForTeams -ne $true) { $failures += "EnableSafeLinksForTeams: False" } # Teams: On
|
|
||||||
if ($policyDetails.EnableSafeLinksForOffice -ne $true) { $failures += "EnableSafeLinksForOffice: False" } # Office 365 Apps: On
|
|
||||||
if ($policyDetails.TrackClicks -ne $true) { $failures += "TrackClicks: False" } # Click protection settings: On
|
|
||||||
if ($policyDetails.AllowClickThrough -ne $false) { $failures += "AllowClickThrough: True" } # Do not track when users click safe links: Off
|
|
||||||
|
|
||||||
# Only add details for policies that have misconfigurations
|
|
||||||
if ($failures.Count -gt 0) {
|
|
||||||
$misconfiguredDetails += "Policy: $($policy.Name); Failures: $($failures -join ', ')"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Prepare the final result
|
||||||
|
# Condition B: Ensuring no misconfigurations
|
||||||
|
$result = $misconfiguredDetails.Count -eq 0
|
||||||
|
$details = if ($result) { "All Safe Links policies are correctly configured." } else { $misconfiguredDetails -join ' | ' }
|
||||||
|
$failureReasons = if ($result) { "N/A" } else { "The following Safe Links policies settings do not meet the recommended configuration: $($misconfiguredDetails -join ' | ')" }
|
||||||
|
|
||||||
|
# Create and populate the CISAuditResult object
|
||||||
|
$params = @{
|
||||||
|
Rec = $recnum
|
||||||
|
Result = $result
|
||||||
|
Status = if ($result) { "Pass" } else { "Fail" }
|
||||||
|
Details = $details
|
||||||
|
FailureReason = $failureReasons
|
||||||
|
}
|
||||||
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "An error occurred during the test: $_"
|
||||||
|
|
||||||
# Prepare the final result
|
# Retrieve the description from the test definitions
|
||||||
# Condition B: Ensuring no misconfigurations
|
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
||||||
$result = $misconfiguredDetails.Count -eq 0
|
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
||||||
$details = if ($result) { "All Safe Links policies are correctly configured." } else { $misconfiguredDetails -join ' | ' }
|
|
||||||
$failureReasons = if ($result) { "N/A" } else { "The following Safe Links policies settings do not meet the recommended configuration: $($misconfiguredDetails -join ' | ')" }
|
|
||||||
|
|
||||||
# Create and populate the CISAuditResult object
|
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
||||||
|
|
||||||
|
# Call Initialize-CISAuditResult with error parameters
|
||||||
|
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
$params = @{
|
$params = @{
|
||||||
Rec = $recnum
|
Rec = $recnum
|
||||||
Result = $result
|
Result = $false
|
||||||
Status = if ($result) { "Pass" } else { "Fail" }
|
Status = "Fail"
|
||||||
Details = $details
|
Details = "No M365 E5 licenses found."
|
||||||
FailureReason = $failureReasons
|
FailureReason = "The audit is for M365 E5 licenses and the required EXO commands will not be available otherwise."
|
||||||
}
|
}
|
||||||
$auditResult = Initialize-CISAuditResult @params
|
$auditResult = Initialize-CISAuditResult @params
|
||||||
}
|
}
|
||||||
catch {
|
|
||||||
Write-Error "An error occurred during the test: $_"
|
|
||||||
|
|
||||||
# Retrieve the description from the test definitions
|
|
||||||
$testDefinition = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $recnum }
|
|
||||||
$description = if ($testDefinition) { $testDefinition.RecDescription } else { "Description not found" }
|
|
||||||
|
|
||||||
$script:FailedTests.Add([PSCustomObject]@{ Rec = $recnum; Description = $description; Error = $_ })
|
|
||||||
|
|
||||||
# Call Initialize-CISAuditResult with error parameters
|
|
||||||
$auditResult = Initialize-CISAuditResult -Rec $recnum -Failure
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end {
|
end {
|
||||||
|
212
test-gh.ps1
212
test-gh.ps1
@@ -1,212 +0,0 @@
|
|||||||
$repoOwner = "CriticalSolutionsNetwork"
|
|
||||||
$repoName = "M365FoundationsCISReport"
|
|
||||||
$directoryPath = ".\source\tests"
|
|
||||||
$projectName = "Test Validation Project"
|
|
||||||
|
|
||||||
# Function to create GitHub issues
|
|
||||||
function Create-GitHubIssue {
|
|
||||||
param (
|
|
||||||
[string]$title,
|
|
||||||
[string]$body,
|
|
||||||
[string]$project
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the issue and add it to the specified project
|
|
||||||
$issue = gh issue create --repo "$repoOwner/$repoName" --title "$title" --body "$body" --project "$project"
|
|
||||||
return $issue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load test definitions from CSV
|
|
||||||
$testDefinitionsPath = ".\source\helper\TestDefinitions.csv"
|
|
||||||
$testDefinitions = Import-Csv -Path $testDefinitionsPath
|
|
||||||
|
|
||||||
# Iterate over each .ps1 file in the directory
|
|
||||||
Get-ChildItem -Path $directoryPath -Filter "*.ps1" | ForEach-Object {
|
|
||||||
$fileName = $_.Name
|
|
||||||
$testDefinition = $testDefinitions | Where-Object { $_.TestFileName -eq $fileName }
|
|
||||||
|
|
||||||
if ($testDefinition) {
|
|
||||||
$rec = $testDefinition.Rec
|
|
||||||
$elevel = $testDefinition.ELevel
|
|
||||||
$profileLevel = $testDefinition.ProfileLevel
|
|
||||||
$ig1 = $testDefinition.IG1
|
|
||||||
$ig2 = $testDefinition.IG2
|
|
||||||
$ig3 = $testDefinition.IG3
|
|
||||||
$connection = $testDefinition.Connection
|
|
||||||
|
|
||||||
$issueTitle = "Rec: $rec - Validate $fileName, ELevel: $elevel, ProfileLevel: $profileLevel, IG1: $ig1, IG2: $ig2, IG3: $ig3, Connection: $connection"
|
|
||||||
$issueBody = @"
|
|
||||||
# Validation for $fileName
|
|
||||||
|
|
||||||
## Tasks
|
|
||||||
- [ ] Validate test for a pass
|
|
||||||
- Description of passing criteria:
|
|
||||||
- [ ] Validate test for a fail
|
|
||||||
- Description of failing criteria:
|
|
||||||
- [ ] Add notes and observations
|
|
||||||
- Placeholder for additional notes:
|
|
||||||
"@
|
|
||||||
|
|
||||||
# Create the issue using GitHub CLI
|
|
||||||
try {
|
|
||||||
Create-GitHubIssue -title "$issueTitle" -body "$issueBody" -project "$projectName"
|
|
||||||
Write-Output "Created issue for $fileName"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "Failed to create issue for $fileName`: $_"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Introduce a delay of 2 seconds
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Warning "No matching test definition found for $fileName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
######################################
|
|
||||||
$repoOwner = "CriticalSolutionsNetwork"
|
|
||||||
$repoName = "M365FoundationsCISReport"
|
|
||||||
|
|
||||||
# Function to update GitHub issue
|
|
||||||
function Update-GitHubTIssue {
|
|
||||||
param (
|
|
||||||
[int]$issueNumber,
|
|
||||||
[string]$title,
|
|
||||||
[string]$body,
|
|
||||||
[string]$owner,
|
|
||||||
[string]$repositoryName
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update the issue using Set-GitHubIssue
|
|
||||||
Set-GitHubIssue -OwnerName $owner -RepositoryName $repositoryName -Issue $issueNumber -Title $title -Body $body -Label @("documentation", "help wanted", "question") -Confirm:$false
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load test definitions from CSV
|
|
||||||
$testDefinitionsPath = ".\source\helper\TestDefinitions.csv"
|
|
||||||
$testDefinitions = Import-Csv -Path $testDefinitionsPath
|
|
||||||
|
|
||||||
# Fetch existing issues that start with "Rec:"
|
|
||||||
$existingIssues = Get-GitHubIssue -OwnerName 'CriticalSolutionsNetwork' -RepositoryName 'M365FoundationsCISReport'
|
|
||||||
|
|
||||||
# Create a list to hold matched issues
|
|
||||||
$matchedIssues = @()
|
|
||||||
$warnings = @()
|
|
||||||
|
|
||||||
# Iterate over each existing issue
|
|
||||||
$existingIssues | ForEach-Object {
|
|
||||||
$issueNumber = $_.Number
|
|
||||||
$issueTitle = $_.Title
|
|
||||||
$issueBody = $_.Body
|
|
||||||
|
|
||||||
# Extract the rec number from the issue title
|
|
||||||
if ($issueTitle -match "Rec: (\d+\.\d+\.\d+)") {
|
|
||||||
$rec = $matches[1]
|
|
||||||
|
|
||||||
# Find the matching test definition based on rec number
|
|
||||||
$testDefinition = $testDefinitions | Where-Object { $_.Rec -eq $rec }
|
|
||||||
|
|
||||||
if ($testDefinition) {
|
|
||||||
# Create the new issue body
|
|
||||||
$newIssueBody = @"
|
|
||||||
# Validation for $($testDefinition.TestFileName)
|
|
||||||
|
|
||||||
## Recommendation Details
|
|
||||||
- **Recommendation**: $($testDefinition.Rec)
|
|
||||||
- **Description**: $($testDefinition.RecDescription)
|
|
||||||
- **ELevel**: $($testDefinition.ELevel)
|
|
||||||
- **Profile Level**: $($testDefinition.ProfileLevel)
|
|
||||||
- **CIS Control**: $($testDefinition.CISControl)
|
|
||||||
- **CIS Description**: $($testDefinition.CISDescription)
|
|
||||||
- **Implementation Group 1**: $($testDefinition.IG1)
|
|
||||||
- **Implementation Group 2**: $($testDefinition.IG2)
|
|
||||||
- **Implementation Group 3**: $($testDefinition.IG3)
|
|
||||||
- **Automated**: $($testDefinition.Automated)
|
|
||||||
- **Connection**: $($testDefinition.Connection)
|
|
||||||
|
|
||||||
## [$($testDefinition.TestFileName)](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/blob/main/source/tests/$($testDefinition.TestFileName))
|
|
||||||
|
|
||||||
## Tasks
|
|
||||||
|
|
||||||
### Validate recommendation details
|
|
||||||
- [ ] Confirm that the recommendation details are accurate and complete as per the CIS benchmark.
|
|
||||||
|
|
||||||
### Validate test for a pass
|
|
||||||
- [ ] Confirm that the automated test results align with the manual audit steps outlined in the CIS benchmark.
|
|
||||||
- Specific conditions to check:
|
|
||||||
- Condition A: (Detail about what constitutes Condition A)
|
|
||||||
- Condition B: (Detail about what constitutes Condition B)
|
|
||||||
- Condition C: (Detail about what constitutes Condition C)
|
|
||||||
|
|
||||||
### Validate test for a fail
|
|
||||||
- [ ] Confirm that the failure conditions in the automated test are consistent with the manual audit results.
|
|
||||||
- Specific conditions to check:
|
|
||||||
- Condition A: (Detail about what constitutes Condition A)
|
|
||||||
- Condition B: (Detail about what constitutes Condition B)
|
|
||||||
- Condition C: (Detail about what constitutes Condition C)
|
|
||||||
|
|
||||||
### Add notes and observations
|
|
||||||
- [ ] Compare the automated audit results with the manual audit steps and provide detailed observations.
|
|
||||||
- Automated audit produced info consistent with the manual audit test results? (Yes/No)
|
|
||||||
- Without disclosing any sensitive information, document any discrepancies between the actual output and the expected output.
|
|
||||||
- Document any error messages, removing any sensitive information before submitting.
|
|
||||||
- Identify the specific function, line, or section of the script that failed, if known.
|
|
||||||
- Provide any additional context or observations that might help in troubleshooting.
|
|
||||||
|
|
||||||
If needed, the helpers folder in .\source\helpers contains a CSV to assist with locating the test definition.
|
|
||||||
"@
|
|
||||||
|
|
||||||
# Add to matched issues list
|
|
||||||
$matchedIssues += [PSCustomObject]@{
|
|
||||||
IssueNumber = $issueNumber
|
|
||||||
Title = $issueTitle
|
|
||||||
NewBody = $newIssueBody
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$warnings += "No matching test definition found for Rec: $rec"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$warnings += "No matching rec number found in issue title #$issueNumber"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Display matched issues for confirmation
|
|
||||||
if ($matchedIssues.Count -gt 0) {
|
|
||||||
Write-Output "Matched Issues:"
|
|
||||||
$matchedIssues | ForEach-Object {
|
|
||||||
Write-Output $_.Title
|
|
||||||
}
|
|
||||||
|
|
||||||
$confirmation = Read-Host "Do you want to proceed with updating these issues? (yes/no)"
|
|
||||||
|
|
||||||
if ($confirmation -eq 'yes') {
|
|
||||||
# Update the issues
|
|
||||||
$matchedIssues | ForEach-Object {
|
|
||||||
try {
|
|
||||||
Update-GitHubTIssue -issueNumber $_.IssueNumber -title $_.Title -body $_.NewBody -owner $repoOwner -repositoryName $repoName
|
|
||||||
Write-Output "Updated issue #$($_.IssueNumber)"
|
|
||||||
} catch {
|
|
||||||
Write-Error "Failed to update issue #$($_.IssueNumber): $_"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Introduce a delay of 2 seconds
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Output "Update canceled by user."
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Output "No matched issues found to update."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Display any warnings that were captured
|
|
||||||
if ($warnings.Count -gt 0) {
|
|
||||||
Write-Output "Warnings:"
|
|
||||||
$warnings | ForEach-Object {
|
|
||||||
Write-Output $_
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test command to verify GitHub access
|
|
||||||
Get-GitHubRepository -OwnerName 'CriticalSolutionsNetwork' -RepositoryName 'M365FoundationsCISReport'
|
|
||||||
|
|
||||||
|
|
27
tests/Unit/Private/Get-ExceededLengthResultDetail.tests.ps1
Normal file
27
tests/Unit/Private/Get-ExceededLengthResultDetail.tests.ps1
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
|
||||||
|
$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
|
||||||
|
($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
|
||||||
|
$(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
|
||||||
|
}).BaseName
|
||||||
|
|
||||||
|
|
||||||
|
Import-Module $ProjectName
|
||||||
|
|
||||||
|
InModuleScope $ProjectName {
|
||||||
|
Describe Get-PrivateFunction {
|
||||||
|
Context 'Default' {
|
||||||
|
BeforeEach {
|
||||||
|
$return = Get-PrivateFunction -PrivateData 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Returns a single object' {
|
||||||
|
($return | Measure-Object).Count | Should -Be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Returns a string based on the parameter PrivateData' {
|
||||||
|
$return | Should -Be 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -1,31 +0,0 @@
|
|||||||
BeforeAll {
|
|
||||||
$script:dscModuleName = 'M365FoundationsCISReport'
|
|
||||||
|
|
||||||
Import-Module -Name $script:dscModuleName
|
|
||||||
}
|
|
||||||
|
|
||||||
AfterAll {
|
|
||||||
# Unload the module being tested so that it doesn't impact any other tests.
|
|
||||||
Get-Module -Name $script:dscModuleName -All | Remove-Module -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
Describe Get-PrivateFunction {
|
|
||||||
Context 'When calling the function with string value' {
|
|
||||||
It 'Should return a single object' {
|
|
||||||
InModuleScope -ModuleName $dscModuleName {
|
|
||||||
$return = Get-PrivateFunction -PrivateData 'string'
|
|
||||||
|
|
||||||
($return | Measure-Object).Count | Should -Be 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
It 'Should return a string based on the parameter PrivateData' {
|
|
||||||
InModuleScope -ModuleName $dscModuleName {
|
|
||||||
$return = Get-PrivateFunction -PrivateData 'string'
|
|
||||||
|
|
||||||
$return | Should -Be 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
27
tests/Unit/Private/Initialize-LargeTestTable.tests.ps1
Normal file
27
tests/Unit/Private/Initialize-LargeTestTable.tests.ps1
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
|
||||||
|
$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
|
||||||
|
($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
|
||||||
|
$(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
|
||||||
|
}).BaseName
|
||||||
|
|
||||||
|
|
||||||
|
Import-Module $ProjectName
|
||||||
|
|
||||||
|
InModuleScope $ProjectName {
|
||||||
|
Describe Get-PrivateFunction {
|
||||||
|
Context 'Default' {
|
||||||
|
BeforeEach {
|
||||||
|
$return = Get-PrivateFunction -PrivateData 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Returns a single object' {
|
||||||
|
($return | Measure-Object).Count | Should -Be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Returns a string based on the parameter PrivateData' {
|
||||||
|
$return | Should -Be 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
71
tests/Unit/Public/Export-M365SecurityAuditTable.tests.ps1
Normal file
71
tests/Unit/Public/Export-M365SecurityAuditTable.tests.ps1
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
BeforeAll {
|
||||||
|
$script:moduleName = '<% $PLASTER_PARAM_ModuleName %>'
|
||||||
|
|
||||||
|
# If the module is not found, run the build task 'noop'.
|
||||||
|
if (-not (Get-Module -Name $script:moduleName -ListAvailable))
|
||||||
|
{
|
||||||
|
# Redirect all streams to $null, except the error stream (stream 2)
|
||||||
|
& "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-import the module using force to get any code changes between runs.
|
||||||
|
Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop'
|
||||||
|
|
||||||
|
$PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName
|
||||||
|
$PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName
|
||||||
|
$PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName
|
||||||
|
}
|
||||||
|
|
||||||
|
AfterAll {
|
||||||
|
$PSDefaultParameterValues.Remove('Mock:ModuleName')
|
||||||
|
$PSDefaultParameterValues.Remove('InModuleScope:ModuleName')
|
||||||
|
$PSDefaultParameterValues.Remove('Should:ModuleName')
|
||||||
|
|
||||||
|
Remove-Module -Name $script:moduleName
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe Get-Something {
|
||||||
|
|
||||||
|
Context 'Return values' {
|
||||||
|
BeforeEach {
|
||||||
|
$return = Get-Something -Data 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Returns a single object' {
|
||||||
|
($return | Measure-Object).Count | Should -Be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Pipeline' {
|
||||||
|
It 'Accepts values from the pipeline by value' {
|
||||||
|
$return = 'value1', 'value2' | Get-Something
|
||||||
|
|
||||||
|
$return[0] | Should -Be 'value1'
|
||||||
|
$return[1] | Should -Be 'value2'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Accepts value from the pipeline by property name' {
|
||||||
|
$return = 'value1', 'value2' | ForEach-Object {
|
||||||
|
[PSCustomObject]@{
|
||||||
|
Data = $_
|
||||||
|
OtherProperty = 'other'
|
||||||
|
}
|
||||||
|
} | Get-Something
|
||||||
|
|
||||||
|
|
||||||
|
$return[0] | Should -Be 'value1'
|
||||||
|
$return[1] | Should -Be 'value2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'ShouldProcess' {
|
||||||
|
It 'Supports WhatIf' {
|
||||||
|
(Get-Command Get-Something).Parameters.ContainsKey('WhatIf') | Should -Be $true
|
||||||
|
{ Get-Something -Data 'value' -WhatIf } | Should -Not -Throw
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
71
tests/Unit/Public/Get-MFAStatus.tests.ps1
Normal file
71
tests/Unit/Public/Get-MFAStatus.tests.ps1
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
BeforeAll {
|
||||||
|
$script:moduleName = '<% $PLASTER_PARAM_ModuleName %>'
|
||||||
|
|
||||||
|
# If the module is not found, run the build task 'noop'.
|
||||||
|
if (-not (Get-Module -Name $script:moduleName -ListAvailable))
|
||||||
|
{
|
||||||
|
# Redirect all streams to $null, except the error stream (stream 2)
|
||||||
|
& "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-import the module using force to get any code changes between runs.
|
||||||
|
Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop'
|
||||||
|
|
||||||
|
$PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName
|
||||||
|
$PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName
|
||||||
|
$PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName
|
||||||
|
}
|
||||||
|
|
||||||
|
AfterAll {
|
||||||
|
$PSDefaultParameterValues.Remove('Mock:ModuleName')
|
||||||
|
$PSDefaultParameterValues.Remove('InModuleScope:ModuleName')
|
||||||
|
$PSDefaultParameterValues.Remove('Should:ModuleName')
|
||||||
|
|
||||||
|
Remove-Module -Name $script:moduleName
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe Get-Something {
|
||||||
|
|
||||||
|
Context 'Return values' {
|
||||||
|
BeforeEach {
|
||||||
|
$return = Get-Something -Data 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Returns a single object' {
|
||||||
|
($return | Measure-Object).Count | Should -Be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Pipeline' {
|
||||||
|
It 'Accepts values from the pipeline by value' {
|
||||||
|
$return = 'value1', 'value2' | Get-Something
|
||||||
|
|
||||||
|
$return[0] | Should -Be 'value1'
|
||||||
|
$return[1] | Should -Be 'value2'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Accepts value from the pipeline by property name' {
|
||||||
|
$return = 'value1', 'value2' | ForEach-Object {
|
||||||
|
[PSCustomObject]@{
|
||||||
|
Data = $_
|
||||||
|
OtherProperty = 'other'
|
||||||
|
}
|
||||||
|
} | Get-Something
|
||||||
|
|
||||||
|
|
||||||
|
$return[0] | Should -Be 'value1'
|
||||||
|
$return[1] | Should -Be 'value2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'ShouldProcess' {
|
||||||
|
It 'Supports WhatIf' {
|
||||||
|
(Get-Command Get-Something).Parameters.ContainsKey('WhatIf') | Should -Be $true
|
||||||
|
{ Get-Something -Data 'value' -WhatIf } | Should -Not -Throw
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
71
tests/Unit/Public/Remove-RowsWithEmptyCSVStatus.tests.ps1
Normal file
71
tests/Unit/Public/Remove-RowsWithEmptyCSVStatus.tests.ps1
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
BeforeAll {
|
||||||
|
$script:moduleName = '<% $PLASTER_PARAM_ModuleName %>'
|
||||||
|
|
||||||
|
# If the module is not found, run the build task 'noop'.
|
||||||
|
if (-not (Get-Module -Name $script:moduleName -ListAvailable))
|
||||||
|
{
|
||||||
|
# Redirect all streams to $null, except the error stream (stream 2)
|
||||||
|
& "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-import the module using force to get any code changes between runs.
|
||||||
|
Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop'
|
||||||
|
|
||||||
|
$PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName
|
||||||
|
$PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName
|
||||||
|
$PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName
|
||||||
|
}
|
||||||
|
|
||||||
|
AfterAll {
|
||||||
|
$PSDefaultParameterValues.Remove('Mock:ModuleName')
|
||||||
|
$PSDefaultParameterValues.Remove('InModuleScope:ModuleName')
|
||||||
|
$PSDefaultParameterValues.Remove('Should:ModuleName')
|
||||||
|
|
||||||
|
Remove-Module -Name $script:moduleName
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe Get-Something {
|
||||||
|
|
||||||
|
Context 'Return values' {
|
||||||
|
BeforeEach {
|
||||||
|
$return = Get-Something -Data 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Returns a single object' {
|
||||||
|
($return | Measure-Object).Count | Should -Be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Pipeline' {
|
||||||
|
It 'Accepts values from the pipeline by value' {
|
||||||
|
$return = 'value1', 'value2' | Get-Something
|
||||||
|
|
||||||
|
$return[0] | Should -Be 'value1'
|
||||||
|
$return[1] | Should -Be 'value2'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Accepts value from the pipeline by property name' {
|
||||||
|
$return = 'value1', 'value2' | ForEach-Object {
|
||||||
|
[PSCustomObject]@{
|
||||||
|
Data = $_
|
||||||
|
OtherProperty = 'other'
|
||||||
|
}
|
||||||
|
} | Get-Something
|
||||||
|
|
||||||
|
|
||||||
|
$return[0] | Should -Be 'value1'
|
||||||
|
$return[1] | Should -Be 'value2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'ShouldProcess' {
|
||||||
|
It 'Supports WhatIf' {
|
||||||
|
(Get-Command Get-Something).Parameters.ContainsKey('WhatIf') | Should -Be $true
|
||||||
|
{ Get-Something -Data 'value' -WhatIf } | Should -Not -Throw
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user