From 4dc996b2fb02aa90effafb98b735a9c7c35e5caa Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:09:04 -0500 Subject: [PATCH 01/16] Fix: MFA STATUS Function --- source/Public/Get-MFAStatus.ps1 | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/source/Public/Get-MFAStatus.ps1 b/source/Public/Get-MFAStatus.ps1 index b936e71..53f6112 100644 --- a/source/Public/Get-MFAStatus.ps1 +++ b/source/Public/Get-MFAStatus.ps1 @@ -44,7 +44,7 @@ function Get-MFAStatus { process { if (Get-Module MSOnline){ Connect-MsolService - Write-Host -Object "Finding Azure Active Directory Accounts..." + Write-Host "Finding Azure Active Directory Accounts..." # Get all users, excluding guests $Users = if ($PSBoundParameters.ContainsKey('UserId')) { Get-MsolUser -UserPrincipalName $UserId @@ -52,7 +52,7 @@ function Get-MFAStatus { Get-MsolUser -All | Where-Object { $_.UserType -ne "Guest" } } $Report = [System.Collections.Generic.List[Object]]::new() # Create output list - Write-Host -Object "Processing" $Users.Count "accounts..." + Write-Host "Processing $($Users.Count) accounts..." ForEach ($User in $Users) { $MFADefaultMethod = ($User.StrongAuthenticationMethods | Where-Object { $_.IsDefault -eq "True" }).MethodType $MFAPhoneNumber = $User.StrongAuthenticationUserDetails.PhoneNumber @@ -92,12 +92,11 @@ function Get-MFAStatus { $Report.Add($ReportLine) } - Write-Host -Object "Processing complete." + Write-Host "Processing complete." return $Report | Select-Object UserPrincipalName, DisplayName, MFAState, MFADefaultMethod, MFAPhoneNumber, PrimarySMTP, Aliases | Sort-Object UserPrincipalName } else { - Write-Host -Object "You must first install MSOL using:`nInstall-Module MSOnline -Scope CurrentUser -Force" + Write-Host "You must first install MSOL using:`nInstall-Module MSOnline -Scope CurrentUser -Force" } } - -} \ No newline at end of file +} From 411ee5d36fdcef05908ac2d63397757ca98c75c1 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:36:51 -0500 Subject: [PATCH 02/16] Fix: 6.1.2/3 csv output when no test was run --- .../Public/Export-M365SecurityAuditTable.ps1 | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/source/Public/Export-M365SecurityAuditTable.ps1 b/source/Public/Export-M365SecurityAuditTable.ps1 index bc15887..3aaa165 100644 --- a/source/Public/Export-M365SecurityAuditTable.ps1 +++ b/source/Public/Export-M365SecurityAuditTable.ps1 @@ -103,7 +103,12 @@ function Export-M365SecurityAuditTable { switch ($test) { "6.1.2" { $details = $auditResult.Details - $csv = $details | ConvertFrom-Csv -Delimiter '|' + if ($details -ne "No M365 E3 licenses found.") { + $csv = $details | ConvertFrom-Csv -Delimiter '|' + } + else { + $csv = $null + } if ($null -ne $csv) { foreach ($row in $csv) { @@ -120,7 +125,12 @@ function Export-M365SecurityAuditTable { } "6.1.3" { $details = $auditResult.Details - $csv = $details | ConvertFrom-Csv -Delimiter '|' + if ($details -ne "No M365 E5 licenses found.") { + $csv = $details | ConvertFrom-Csv -Delimiter '|' + } + else { + $csv = $null + } if ($null -ne $csv) { foreach ($row in $csv) { @@ -155,8 +165,10 @@ function Export-M365SecurityAuditTable { Write-Information "No results found for test number $($result.TestNumber)." -InformationAction Continue } else { - $result.Details | Export-Csv -Path $fileName -NoTypeInformation - $exportedTests += $result.TestNumber + 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 + } } } } From 99933f7655e033f1e3444ba70a434e341ce12291 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:45:56 -0500 Subject: [PATCH 03/16] add: option to disconnect with tenant notification --- source/Private/Connect-M365Suite.ps1 | 68 +++++++++++++++++++++- source/Public/Invoke-M365SecurityAudit.ps1 | 15 +++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/source/Private/Connect-M365Suite.ps1 b/source/Private/Connect-M365Suite.ps1 index 04fe394..ee7b7c4 100644 --- a/source/Private/Connect-M365Suite.ps1 +++ b/source/Private/Connect-M365Suite.ps1 @@ -2,19 +2,31 @@ function Connect-M365Suite { [OutputType([void])] [CmdletBinding()] param ( - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [string]$TenantAdminUrl, [Parameter(Mandatory)] - [string[]]$RequiredConnections + [string[]]$RequiredConnections, + + [Parameter(Mandatory = $false)] + [switch]$SkipConfirmation ) $VerbosePreference = "SilentlyContinue" + $tenantInfo = @() + $connectedServices = @() try { 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 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 } @@ -22,11 +34,25 @@ function Connect-M365Suite { Write-Host "Connecting to Microsoft Graph with scopes: Directory.Read.All, Domain.Read.All, Policy.Read.All, Organization.Read.All" -ForegroundColor Cyan try { 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 } catch { 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 + $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 } } @@ -34,20 +60,58 @@ function Connect-M365Suite { 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 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 } if ($RequiredConnections -contains "SPO") { Write-Host "Connecting to SharePoint Online..." -ForegroundColor Cyan 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 } if ($RequiredConnections -contains "Microsoft Teams" -or $RequiredConnections -contains "Microsoft Teams | EXO") { Write-Host "Connecting to Microsoft Teams..." -ForegroundColor Cyan 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 } + + # 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 { $VerbosePreference = "Continue" diff --git a/source/Public/Invoke-M365SecurityAudit.ps1 b/source/Public/Invoke-M365SecurityAudit.ps1 index cfc5aed..0596078 100644 --- a/source/Public/Invoke-M365SecurityAudit.ps1 +++ b/source/Public/Invoke-M365SecurityAudit.ps1 @@ -240,11 +240,18 @@ function Invoke-M365SecurityAudit { $currentTestIndex = 0 # Establishing connections if required - $actualUniqueConnections = Get-UniqueConnection -Connections $requiredConnections - if (!($DoNotConnect) -and $PSCmdlet.ShouldProcess("Establish connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Connect")) { - Write-Information "Establishing connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')" -InformationAction Continue - Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections + try { + $actualUniqueConnections = Get-UniqueConnection -Connections $requiredConnections + if (!($DoNotConnect) -and $PSCmdlet.ShouldProcess("Establish connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Connect")) { + Write-Information "Establishing connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')" -InformationAction Continue + Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections + } } + catch { + Write-Host "Execution aborted: $_" -ForegroundColor Red + break + } + Write-Information "A total of $($totalTests) tests were selected to run..." -InformationAction Continue From a97eda16622f1bc0a31d6aaa8bad25b4990028bc Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:51:10 -0500 Subject: [PATCH 04/16] add: Added DoNotConfirmConnections Switch to main function --- source/Public/Invoke-M365SecurityAudit.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/Public/Invoke-M365SecurityAudit.ps1 b/source/Public/Invoke-M365SecurityAudit.ps1 index 0596078..fbd72f7 100644 --- a/source/Public/Invoke-M365SecurityAudit.ps1 +++ b/source/Public/Invoke-M365SecurityAudit.ps1 @@ -27,6 +27,8 @@ If specified, the cmdlet will not disconnect from Microsoft 365 services after execution. .PARAMETER NoModuleCheck If specified, the cmdlet will not check for the presence of required modules. + .PARAMETER DoNotConfirmConnections + If specified, the cmdlet will not prompt for confirmation before proceeding with established connections and will disconnect from all of them. .EXAMPLE PS> Invoke-M365SecurityAudit Performs a security audit using default parameters. @@ -174,7 +176,8 @@ function Invoke-M365SecurityAudit { # Common parameters for all parameter sets [switch]$DoNotConnect, [switch]$DoNotDisconnect, - [switch]$NoModuleCheck + [switch]$NoModuleCheck, + [switch]$DoNotConfirmConnections ) Begin { @@ -244,7 +247,7 @@ function Invoke-M365SecurityAudit { $actualUniqueConnections = Get-UniqueConnection -Connections $requiredConnections if (!($DoNotConnect) -and $PSCmdlet.ShouldProcess("Establish connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Connect")) { Write-Information "Establishing connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')" -InformationAction Continue - Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections + Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections -SkipConfirmation:$DoNotConfirmConnections } } catch { From daadad391e3e4ea3320e5dc91c10a84af4cd2d71 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:51:29 -0500 Subject: [PATCH 05/16] docs: Update Export function help --- source/Public/Export-M365SecurityAuditTable.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/Public/Export-M365SecurityAuditTable.ps1 b/source/Public/Export-M365SecurityAuditTable.ps1 index 3aaa165..0bbe517 100644 --- a/source/Public/Export-M365SecurityAuditTable.ps1 +++ b/source/Public/Export-M365SecurityAuditTable.ps1 @@ -21,23 +21,23 @@ .OUTPUTS [PSCustomObject] .EXAMPLE - # Output object for a single test number from audit results Export-M365SecurityAuditTable -AuditResults $object -OutputTestNumber 6.1.2 + # Output object for a single test number from audit results .EXAMPLE - # Export all results from audit results to the specified path Export-M365SecurityAuditTable -ExportAllTests -AuditResults $object -ExportPath "C:\temp" + # Export all results from audit results to the specified path .EXAMPLE - # Output object for a single test number from CSV Export-M365SecurityAuditTable -CsvPath "C:\temp\auditresultstoday1.csv" -OutputTestNumber 6.1.2 + # Output object for a single test number from CSV .EXAMPLE - # Export all results from CSV to the specified path Export-M365SecurityAuditTable -ExportAllTests -CsvPath "C:\temp\auditresultstoday1.csv" -ExportPath "C:\temp" + # Export all results from CSV to the specified path .EXAMPLE - # Export all results from audit results to the specified path along with the original tests Export-M365SecurityAuditTable -ExportAllTests -AuditResults $object -ExportPath "C:\temp" -ExportOriginalTests + # Export all results from audit results to the specified path along with the original tests .EXAMPLE - # Export all results from CSV to the specified path along with the original tests Export-M365SecurityAuditTable -ExportAllTests -CsvPath "C:\temp\auditresultstoday1.csv" -ExportPath "C:\temp" -ExportOriginalTests + # Export all results from CSV to the specified path along with the original tests .LINK https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Export-M365SecurityAuditTable #> From aa76de66491c0e3b3abdf94988d5bfd00a9fa64d Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:53:31 -0500 Subject: [PATCH 06/16] docs: Update CHANGELOG --- CHANGELOG.md | 11 +++++++++++ helpers/Build-Help.ps1 | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e23c30..e11d2ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on and uses the types of changes according to [Keep a Change ### Added +- Added tenant output to connect function. +- Added skip tenant connection confirmation to main function. + +### Fixed + +- Fixed comment examples for `Export-M365SecurityAuditTable`. + +## [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. diff --git a/helpers/Build-Help.ps1 b/helpers/Build-Help.ps1 index 3a5d89a..a0904d4 100644 --- a/helpers/Build-Help.ps1 +++ b/helpers/Build-Help.ps1 @@ -4,7 +4,7 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1 <# - $ver = "v0.1.11" + $ver = "v0.1.12" git checkout main git pull origin main git tag -a $ver -m "Release version $ver refactor Update" From d6c500f953bd153b3a4146d85961fc66d3b6221e Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 19:19:07 -0500 Subject: [PATCH 07/16] fix: Fix merging csv when data present --- source/Private/Merge-CISExcelAndCsvData.ps1 | 8 ++------ source/Private/New-MergedObject.ps1 | 12 +++++++----- source/Private/Update-CISExcelWorksheet.ps1 | 3 +-- source/Private/Update-WorksheetCell.ps1 | 3 ++- source/Public/Sync-CISExcelAndCsvData.ps1 | 5 +---- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/source/Private/Merge-CISExcelAndCsvData.ps1 b/source/Private/Merge-CISExcelAndCsvData.ps1 index 7d2f039..64fc4ef 100644 --- a/source/Private/Merge-CISExcelAndCsvData.ps1 +++ b/source/Private/Merge-CISExcelAndCsvData.ps1 @@ -16,27 +16,23 @@ function Merge-CISExcelAndCsvData { ) 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 }) + $item } } - # Return the merged data return $mergedData } -} \ No newline at end of file +} diff --git a/source/Private/New-MergedObject.ps1 b/source/Private/New-MergedObject.ps1 index 50f7497..e371982 100644 --- a/source/Private/New-MergedObject.ps1 +++ b/source/Private/New-MergedObject.ps1 @@ -12,11 +12,13 @@ function New-MergedObject { $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 $property.Name -Value $property.Value -Force } - $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 + + $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Connection' -Value $CsvRow.Connection -Force + $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Status' -Value $CsvRow.Status -Force + $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Details' -Value $CsvRow.Details -Force + $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_FailureReason' -Value $CsvRow.FailureReason -Force + return $newObject } diff --git a/source/Private/Update-CISExcelWorksheet.ps1 b/source/Private/Update-CISExcelWorksheet.ps1 index 9976f76..d94a8fe 100644 --- a/source/Private/Update-CISExcelWorksheet.ps1 +++ b/source/Private/Update-CISExcelWorksheet.ps1 @@ -24,11 +24,10 @@ function Update-CISExcelWorksheet { 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 } -} \ No newline at end of file +} diff --git a/source/Private/Update-WorksheetCell.ps1 b/source/Private/Update-WorksheetCell.ps1 index 9708c1c..da4fb61 100644 --- a/source/Private/Update-WorksheetCell.ps1 +++ b/source/Private/Update-WorksheetCell.ps1 @@ -10,7 +10,8 @@ function Update-WorksheetCell { $firstItem = $Data[0] $colIndex = 1 foreach ($property in $firstItem.PSObject.Properties) { - if ($StartingRowIndex -eq 2 -and $Worksheet.Cells[1, $colIndex].Value -eq $null) { + # Update headers if they don't exist or if explicitly needed + if ($Worksheet.Cells[1, $colIndex].Value -ne $property.Name) { $Worksheet.Cells[1, $colIndex].Value = $property.Name } $colIndex++ diff --git a/source/Public/Sync-CISExcelAndCsvData.ps1 b/source/Public/Sync-CISExcelAndCsvData.ps1 index 8e91b95..df38696 100644 --- a/source/Public/Sync-CISExcelAndCsvData.ps1 +++ b/source/Public/Sync-CISExcelAndCsvData.ps1 @@ -66,25 +66,22 @@ function Sync-CISExcelAndCsvData { ) process { - # Verify ImportExcel module is available $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 if ($PSCmdlet.ParameterSetName -eq 'CsvInput') { $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 if ($SkipUpdate) { return $mergedData } else { - # Update the Excel worksheet with the merged data Update-CISExcelWorksheet -ExcelPath $ExcelPath -WorksheetName $WorksheetName -Data $mergedData } } } + From b07344bb71b4e61d9bf00d5d5703cc822babda81 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 19:57:31 -0500 Subject: [PATCH 08/16] fix: Fixed merging and added date: --- source/Private/Merge-CISExcelAndCsvData.ps1 | 29 ++++++++++++++++++--- source/Private/New-MergedObject.ps1 | 2 ++ source/Private/Update-CISExcelWorksheet.ps1 | 10 +++++++ source/Private/Update-WorksheetCell.ps1 | 4 +-- source/Public/Sync-CISExcelAndCsvData.ps1 | 1 - 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/source/Private/Merge-CISExcelAndCsvData.ps1 b/source/Private/Merge-CISExcelAndCsvData.ps1 index 64fc4ef..b5381f1 100644 --- a/source/Private/Merge-CISExcelAndCsvData.ps1 +++ b/source/Private/Merge-CISExcelAndCsvData.ps1 @@ -16,23 +16,44 @@ function Merge-CISExcelAndCsvData { ) 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 } - $mergedData = foreach ($item in $import) { + # Ensure headers are included in the merged data + $headers = @() + $firstItem = $import[0] + foreach ($property in $firstItem.PSObject.Properties) { + $headers += $property.Name + } + $headers += 'CSV_Connection', 'CSV_Status', 'CSV_Date', 'CSV_Details', 'CSV_FailureReason' + + $mergedData = @() + foreach ($item in $import) { $csvRow = $csvData | Where-Object { $_.Rec -eq $item.'recommendation #' } if ($csvRow) { - New-MergedObject -ExcelItem $item -CsvRow $csvRow + $mergedData += New-MergedObject -ExcelItem $item -CsvRow $csvRow } else { - $item + $mergedData += New-MergedObject -ExcelItem $item -CsvRow ([PSCustomObject]@{Connection=$null; Status=$null; Date=$null; Details=$null; FailureReason=$null}) } } - return $mergedData + # Create a new PSObject array with headers included + $result = @() + foreach ($item in $mergedData) { + $newItem = New-Object PSObject + foreach ($header in $headers) { + $newItem | Add-Member -MemberType NoteProperty -Name $header -Value $item.$header -Force + } + $result += $newItem + } + + return $result } } diff --git a/source/Private/New-MergedObject.ps1 b/source/Private/New-MergedObject.ps1 index e371982..aaf3c50 100644 --- a/source/Private/New-MergedObject.ps1 +++ b/source/Private/New-MergedObject.ps1 @@ -10,6 +10,7 @@ function New-MergedObject { ) $newObject = New-Object PSObject + $currentDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" foreach ($property in $ExcelItem.PSObject.Properties) { $newObject | Add-Member -MemberType NoteProperty -Name $property.Name -Value $property.Value -Force @@ -17,6 +18,7 @@ function New-MergedObject { $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Connection' -Value $CsvRow.Connection -Force $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Status' -Value $CsvRow.Status -Force + $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Date' -Value $currentDate -Force $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Details' -Value $CsvRow.Details -Force $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_FailureReason' -Value $CsvRow.FailureReason -Force diff --git a/source/Private/Update-CISExcelWorksheet.ps1 b/source/Private/Update-CISExcelWorksheet.ps1 index d94a8fe..a7ad633 100644 --- a/source/Private/Update-CISExcelWorksheet.ps1 +++ b/source/Private/Update-CISExcelWorksheet.ps1 @@ -24,6 +24,16 @@ function Update-CISExcelWorksheet { throw "Worksheet '$WorksheetName' not found in '$ExcelPath'" } + # Ensure headers are set + $firstItem = $Data[0] + $colIndex = 1 + foreach ($property in $firstItem.PSObject.Properties) { + if ($worksheet.Cells[1, $colIndex].Value -eq $null -or $worksheet.Cells[1, $colIndex].Value -ne $property.Name) { + $worksheet.Cells[1, $colIndex].Value = $property.Name + } + $colIndex++ + } + # Update the worksheet with the provided data Update-WorksheetCell -Worksheet $worksheet -Data $Data -StartingRowIndex $StartingRowIndex diff --git a/source/Private/Update-WorksheetCell.ps1 b/source/Private/Update-WorksheetCell.ps1 index da4fb61..d92f044 100644 --- a/source/Private/Update-WorksheetCell.ps1 +++ b/source/Private/Update-WorksheetCell.ps1 @@ -10,8 +10,8 @@ function Update-WorksheetCell { $firstItem = $Data[0] $colIndex = 1 foreach ($property in $firstItem.PSObject.Properties) { - # Update headers if they don't exist or if explicitly needed - if ($Worksheet.Cells[1, $colIndex].Value -ne $property.Name) { + if ($StartingRowIndex -eq 2 -and $Worksheet.Cells[1, $colIndex].Value -eq $null) { + # Add header if it's not present $Worksheet.Cells[1, $colIndex].Value = $property.Name } $colIndex++ diff --git a/source/Public/Sync-CISExcelAndCsvData.ps1 b/source/Public/Sync-CISExcelAndCsvData.ps1 index df38696..7e9a00e 100644 --- a/source/Public/Sync-CISExcelAndCsvData.ps1 +++ b/source/Public/Sync-CISExcelAndCsvData.ps1 @@ -84,4 +84,3 @@ function Sync-CISExcelAndCsvData { } } } - From 07bd30a27f76c9ced5ee25089953277a0cbe070b Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 17 Jun 2024 20:10:59 -0500 Subject: [PATCH 09/16] fix: Fixed merging and added dateto columns without a status --- source/Private/New-MergedObject.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/Private/New-MergedObject.ps1 b/source/Private/New-MergedObject.ps1 index aaf3c50..a3ff349 100644 --- a/source/Private/New-MergedObject.ps1 +++ b/source/Private/New-MergedObject.ps1 @@ -18,7 +18,11 @@ function New-MergedObject { $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Connection' -Value $CsvRow.Connection -Force $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Status' -Value $CsvRow.Status -Force - $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Date' -Value $currentDate -Force + if ($CsvRow.Status -ne $null) { + $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Date' -Value $currentDate -Force + } else { + $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Date' -Value $null -Force + } $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Details' -Value $CsvRow.Details -Force $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_FailureReason' -Value $CsvRow.FailureReason -Force From 3e5f9b3ac5718017da503ca96e01a3973b856ef4 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:44:50 -0500 Subject: [PATCH 10/16] fix: Fixed merging and added date to columns without a status --- source/Private/Merge-CISExcelAndCsvData.ps1 | 11 ++++++++--- source/Private/Update-CISExcelWorksheet.ps1 | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/source/Private/Merge-CISExcelAndCsvData.ps1 b/source/Private/Merge-CISExcelAndCsvData.ps1 index b5381f1..1b9d038 100644 --- a/source/Private/Merge-CISExcelAndCsvData.ps1 +++ b/source/Private/Merge-CISExcelAndCsvData.ps1 @@ -26,6 +26,9 @@ function Merge-CISExcelAndCsvData { $AuditResults } + # Extract recommendation numbers from the CSV + $csvRecs = $csvData | Select-Object -ExpandProperty Rec + # Ensure headers are included in the merged data $headers = @() $firstItem = $import[0] @@ -36,11 +39,13 @@ function Merge-CISExcelAndCsvData { $mergedData = @() foreach ($item in $import) { - $csvRow = $csvData | Where-Object { $_.Rec -eq $item.'recommendation #' } - if ($csvRow) { + # Check if the recommendation number exists in the CSV + $recNum = $item.'recommendation #' + if ($csvRecs -contains $recNum) { + $csvRow = $csvData | Where-Object { $_.Rec -eq $recNum } $mergedData += New-MergedObject -ExcelItem $item -CsvRow $csvRow } else { - $mergedData += New-MergedObject -ExcelItem $item -CsvRow ([PSCustomObject]@{Connection=$null; Status=$null; Date=$null; Details=$null; FailureReason=$null}) + $mergedData += $item } } diff --git a/source/Private/Update-CISExcelWorksheet.ps1 b/source/Private/Update-CISExcelWorksheet.ps1 index a7ad633..b4af9cc 100644 --- a/source/Private/Update-CISExcelWorksheet.ps1 +++ b/source/Private/Update-CISExcelWorksheet.ps1 @@ -35,7 +35,8 @@ function Update-CISExcelWorksheet { } # Update the worksheet with the provided data - Update-WorksheetCell -Worksheet $worksheet -Data $Data -StartingRowIndex $StartingRowIndex + $validRows = $Data | Where-Object { $_.'recommendation #' -ne $null } + Update-WorksheetCell -Worksheet $worksheet -Data $validRows -StartingRowIndex $StartingRowIndex # Save and close the Excel package Close-ExcelPackage $excelPackage From b78cb17bc14e70c0d5da15b336d72a283258dac3 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:10:22 -0500 Subject: [PATCH 11/16] fix: Fixed overwrite of manual audit rows --- source/Private/New-MergedObject.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Private/New-MergedObject.ps1 b/source/Private/New-MergedObject.ps1 index a3ff349..e3ca0d0 100644 --- a/source/Private/New-MergedObject.ps1 +++ b/source/Private/New-MergedObject.ps1 @@ -10,7 +10,7 @@ function New-MergedObject { ) $newObject = New-Object PSObject - $currentDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $currentDate = Get-Date -Format "yyyy-MM-ddTHH:mm:ss" foreach ($property in $ExcelItem.PSObject.Properties) { $newObject | Add-Member -MemberType NoteProperty -Name $property.Name -Value $property.Value -Force From 0c280094989ec33ad32e2d6f4ac7390eef482e15 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:16:01 -0500 Subject: [PATCH 12/16] fix: Refactor sync function to one simple function --- source/Private/Merge-CISExcelAndCsvData.ps1 | 64 --------- source/Private/New-MergedObject.ps1 | 30 ----- source/Private/Update-CISExcelWorksheet.ps1 | 44 ------ source/Private/Update-WorksheetCell.ps1 | 30 ----- source/Public/Sync-CISExcelAndCsvData.ps1 | 140 +++++++++----------- 5 files changed, 63 insertions(+), 245 deletions(-) delete mode 100644 source/Private/Merge-CISExcelAndCsvData.ps1 delete mode 100644 source/Private/New-MergedObject.ps1 delete mode 100644 source/Private/Update-CISExcelWorksheet.ps1 delete mode 100644 source/Private/Update-WorksheetCell.ps1 diff --git a/source/Private/Merge-CISExcelAndCsvData.ps1 b/source/Private/Merge-CISExcelAndCsvData.ps1 deleted file mode 100644 index 1b9d038..0000000 --- a/source/Private/Merge-CISExcelAndCsvData.ps1 +++ /dev/null @@ -1,64 +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 - } - - # Extract recommendation numbers from the CSV - $csvRecs = $csvData | Select-Object -ExpandProperty Rec - - # Ensure headers are included in the merged data - $headers = @() - $firstItem = $import[0] - foreach ($property in $firstItem.PSObject.Properties) { - $headers += $property.Name - } - $headers += 'CSV_Connection', 'CSV_Status', 'CSV_Date', 'CSV_Details', 'CSV_FailureReason' - - $mergedData = @() - foreach ($item in $import) { - # Check if the recommendation number exists in the CSV - $recNum = $item.'recommendation #' - if ($csvRecs -contains $recNum) { - $csvRow = $csvData | Where-Object { $_.Rec -eq $recNum } - $mergedData += New-MergedObject -ExcelItem $item -CsvRow $csvRow - } else { - $mergedData += $item - } - } - - # Create a new PSObject array with headers included - $result = @() - foreach ($item in $mergedData) { - $newItem = New-Object PSObject - foreach ($header in $headers) { - $newItem | Add-Member -MemberType NoteProperty -Name $header -Value $item.$header -Force - } - $result += $newItem - } - - return $result - } -} diff --git a/source/Private/New-MergedObject.ps1 b/source/Private/New-MergedObject.ps1 deleted file mode 100644 index e3ca0d0..0000000 --- a/source/Private/New-MergedObject.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -function New-MergedObject { - [CmdletBinding()] - [OutputType([PSCustomObject])] - param ( - [Parameter(Mandatory = $true)] - [psobject]$ExcelItem, - - [Parameter(Mandatory = $true)] - [psobject]$CsvRow - ) - - $newObject = New-Object PSObject - $currentDate = Get-Date -Format "yyyy-MM-ddTHH:mm:ss" - - foreach ($property in $ExcelItem.PSObject.Properties) { - $newObject | Add-Member -MemberType NoteProperty -Name $property.Name -Value $property.Value -Force - } - - $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Connection' -Value $CsvRow.Connection -Force - $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Status' -Value $CsvRow.Status -Force - if ($CsvRow.Status -ne $null) { - $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Date' -Value $currentDate -Force - } else { - $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Date' -Value $null -Force - } - $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_Details' -Value $CsvRow.Details -Force - $newObject | Add-Member -MemberType NoteProperty -Name 'CSV_FailureReason' -Value $CsvRow.FailureReason -Force - - return $newObject -} diff --git a/source/Private/Update-CISExcelWorksheet.ps1 b/source/Private/Update-CISExcelWorksheet.ps1 deleted file mode 100644 index b4af9cc..0000000 --- a/source/Private/Update-CISExcelWorksheet.ps1 +++ /dev/null @@ -1,44 +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'" - } - - # Ensure headers are set - $firstItem = $Data[0] - $colIndex = 1 - foreach ($property in $firstItem.PSObject.Properties) { - if ($worksheet.Cells[1, $colIndex].Value -eq $null -or $worksheet.Cells[1, $colIndex].Value -ne $property.Name) { - $worksheet.Cells[1, $colIndex].Value = $property.Name - } - $colIndex++ - } - - # Update the worksheet with the provided data - $validRows = $Data | Where-Object { $_.'recommendation #' -ne $null } - Update-WorksheetCell -Worksheet $worksheet -Data $validRows -StartingRowIndex $StartingRowIndex - - # Save and close the Excel package - Close-ExcelPackage $excelPackage - } -} diff --git a/source/Private/Update-WorksheetCell.ps1 b/source/Private/Update-WorksheetCell.ps1 deleted file mode 100644 index d92f044..0000000 --- a/source/Private/Update-WorksheetCell.ps1 +++ /dev/null @@ -1,30 +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) { - # Add header if it's not present - $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++ - } -} diff --git a/source/Public/Sync-CISExcelAndCsvData.ps1 b/source/Public/Sync-CISExcelAndCsvData.ps1 index 7e9a00e..c6d9ccc 100644 --- a/source/Public/Sync-CISExcelAndCsvData.ps1 +++ b/source/Public/Sync-CISExcelAndCsvData.ps1 @@ -1,86 +1,72 @@ -<# -.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. -.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. -.PARAMETER ExcelPath -The path to the Excel file that contains the original data. This parameter is mandatory. -.PARAMETER WorksheetName -The name of the worksheet within the Excel file that contains the data to be synchronized. This parameter is mandatory. -.PARAMETER CsvPath -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. -.PARAMETER AuditResults -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. -.PARAMETER SkipUpdate -If specified, the function will return the merged data object without updating the Excel worksheet. This is useful for previewing the merged data. -.EXAMPLE -PS> Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -WorksheetName "DataSheet" -CsvPath "path\to\data.csv" -Merges data from 'data.csv' into 'excel.xlsx' on the 'DataSheet' worksheet and updates the worksheet with the merged data. -.EXAMPLE -PS> $mergedData = Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -WorksheetName "DataSheet" -CsvPath "path\to\data.csv" -SkipUpdate -Retrieves the merged data object for preview without updating the Excel worksheet. -.EXAMPLE -PS> $auditResults = Invoke-M365SecurityAudit -TenantAdminUrl "https://tenant-admin.url" -DomainName "example.com" -PS> Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -WorksheetName "DataSheet" -AuditResults $auditResults -Merges data from the audit results into 'excel.xlsx' on the 'DataSheet' worksheet and updates the worksheet with the merged data. -.EXAMPLE -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 { - [OutputType([void], [PSCustomObject[]])] - [CmdletBinding(DefaultParameterSetName = 'CsvInput')] - param ( - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-Path $_ })] + param( [string]$ExcelPath, - - [Parameter(Mandatory = $true)] - [string]$WorksheetName, - - [Parameter(Mandatory = $true, ParameterSetName = 'CsvInput')] - [ValidateScript({ Test-Path $_ })] [string]$CsvPath, - - [Parameter(Mandatory = $true, ParameterSetName = 'ObjectInput', ValueFromPipeline = $true)] - [CISAuditResult[]]$AuditResults, - - [Parameter(Mandatory = $false)] - [switch]$SkipUpdate + [string]$SheetName ) - process { - $requiredModules = Get-RequiredModule -SyncFunction - foreach ($module in $requiredModules) { - Assert-ModuleAvailability -ModuleName $module.ModuleName -RequiredVersion $module.RequiredVersion -SubModuleName $module.SubModuleName - } + # Import the CSV file + $csvData = Import-Csv -Path $CsvPath - if ($PSCmdlet.ParameterSetName -eq 'CsvInput') { - $mergedData = Merge-CISExcelAndCsvData -ExcelPath $ExcelPath -WorksheetName $WorksheetName -CsvPath $CsvPath - } else { - $mergedData = Merge-CISExcelAndCsvData -ExcelPath $ExcelPath -WorksheetName $WorksheetName -AuditResults $AuditResults - } + # Get the current date in the specified format + $currentDate = Get-Date -Format "yyyy-MM-ddTHH:mm:ss" - if ($SkipUpdate) { - return $mergedData - } else { - Update-CISExcelWorksheet -ExcelPath $ExcelPath -WorksheetName $WorksheetName -Data $mergedData + # Load the Excel workbook + $excelPackage = Open-ExcelPackage -Path $ExcelPath + $worksheet = $excelPackage.Workbook.Worksheets[$SheetName] + + # Define and check new headers, including the date header + $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() +} \ No newline at end of file From 0125d4261d05cb3b6aa03019c909fe42a0fc486c Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:03:37 -0500 Subject: [PATCH 13/16] add: Comment based help to new sync function and output type added --- source/Public/Sync-CISExcelAndCsvData.ps1 | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/source/Public/Sync-CISExcelAndCsvData.ps1 b/source/Public/Sync-CISExcelAndCsvData.ps1 index c6d9ccc..7dfa467 100644 --- a/source/Public/Sync-CISExcelAndCsvData.ps1 +++ b/source/Public/Sync-CISExcelAndCsvData.ps1 @@ -1,4 +1,34 @@ +<# + .SYNOPSIS + Synchronizes and updates data in an Excel worksheet with new information from a CSV file, including audit dates. + .DESCRIPTION + 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 + Specifies the path to the Excel file to be updated. This parameter is mandatory. + .PARAMETER CsvPath + Specifies the path to the CSV file containing new data. This parameter is mandatory. + .PARAMETER SheetName + Specifies the name of the worksheet in the Excel file where data will be merged and updated. This parameter is mandatory. + .EXAMPLE + PS> Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -CsvPath "path\to\data.csv" -SheetName "AuditData" + Updates the 'AuditData' worksheet in 'excel.xlsx' with data from 'data.csv', adding new information and the date of the update. + .INPUTS + System.String + The function accepts strings for file paths and worksheet names. + .OUTPUTS + None + The function directly updates the Excel file and does not output any objects. + .NOTES + - Ensure that the 'ImportExcel' module is installed and up to date to handle Excel file manipulations. + - It is recommended to back up the Excel file before running this function to avoid accidental data loss. + - The CSV file should have columns that match expected headers like 'Connection', 'Details', 'FailureReason', and 'Status' for correct data mapping. + .LINK + https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Sync-CISExcelAndCsvData +#> + function Sync-CISExcelAndCsvData { + [OutputType([void])] + [CmdletBinding()] param( [string]$ExcelPath, [string]$CsvPath, From b2eaee54e1d285e7f4369c7fb120b2106f09153f Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:04:52 -0500 Subject: [PATCH 14/16] docs: Update Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e11d2ca..42cf719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ The format is based on and uses the types of changes according to [Keep a Change - Fixed comment examples for `Export-M365SecurityAuditTable`. +### Changed + +- Updated `Sync-CISExcelAndCsvData` to be one function. + ## [0.1.12] - 2024-06-17 ### Added From a6720dbc5e1e0d775c903e5c7e86d97f15108af2 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:06:02 -0500 Subject: [PATCH 15/16] docs: Deleted tests for functions that no longer exist --- tests/Unit/Private/New-MergedObject.tests.ps1 | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 tests/Unit/Private/New-MergedObject.tests.ps1 diff --git a/tests/Unit/Private/New-MergedObject.tests.ps1 b/tests/Unit/Private/New-MergedObject.tests.ps1 deleted file mode 100644 index 4a2aa69..0000000 --- a/tests/Unit/Private/New-MergedObject.tests.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -$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' - } - } - } -} - From 3ecd8bb8af34d4ba824d5ba4b6be384fc6ccae36 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:08:15 -0500 Subject: [PATCH 16/16] docs: Update README and HTML Help --- README.md | Bin 39314 -> 35932 bytes docs/index.html | Bin 97760 -> 93256 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/README.md b/README.md index ac523d48984f98f137cd25be0e1dde26d13e44fa..4da28d8248171fe4a60bb34877001e4b4e6fc60d 100644 GIT binary patch delta 1777 zcmah}Uu;ul6#vSyc641g`tP>h4Q_{7rwkkd5r)obM61(4n@C3m<8C^(j!h=B##0O(Effrx=2R`|v@x}PC5YPE;yS1VtO?toceZTWN z=XcKez6EpLkLTB2xnwsN?YCX9xAx*G-$>(2f`3iIDMp|NUc_zhMqC~3qw`UG?LQPM zh@{8}L$pgwHTsUPh=3Zz^0`jQ)}jO*Wi-AE?m|lG{ule0#9vVuKUIZ|tr=w(z6jXy zf!YO++KOn1y=89r`GMV7R-LvH-0{uen%YPFoOum*4XVPGHhpcE>&)NZ@Yvvfr@{O- z{IN?~=BYQY+Y3Sy$7!9T zZ=AkavSdw!E()~b9ZkGULt>$lWLe&E+DnQAVX|V3cBg4o5Gj1x9>TAK0rVVoIe0HE z()i=ZX1~rW!c7p9#z-DKY>z&KyPG}cQtbNS~o0)F$X_-8htpo$Bq}A9sI2y z7F$IOzf3FmtH)!$tM%Fzid_rO_|SUDiHF^894Knu38LegXhd&V#JJw>#Zqx6dZN4W zcT;Dz%hkE=dYL!{KRJmz?BYEk(JG92K`d z^ddO#!cwULU$wWCV|HS8Xd{B#-OVwoZEmG2LpaeL+OJb@xnBmI<%uOu=mde+~e;5SbY+a7GF(}axB%yYn5$B8*DPGat?il>VKMfy^N z1ofRtPpd4g3J5oQy;*UR>bX z$By~EHVv8i!w8=CG+Vy9m?DmG%KL};$Kc!G%dqMc?Xh>F%swTx^4#;-WGEVE$g|1( z8tw2mPPRtK*C@87kmsKBOVW9Q{&UhVP4P~UrHKf?C~XnJV;K<#Xsu5F%dxeI+% z9kAAw8bt*6jw4P=+-hGq!`w=JE2GSpgflegCvPED&kG|mh~?8cES}j}ugPJ)1^+-l zadM9elnqDF?|JZ_BbJpbLd@0;<^1pUjqo%$tP2`J>3x4=ggh!Qz@KNfsH~A|v-&>N gT;3fO&6OyX$hGf7)%_AlsZMKK#OQ@>xOx7@KVGB9Jpcdz delta 2896 zcmcImU2Icj82%2hu7je{kFfvt2g)da>EY+xASXPGP`mV~l(psZ_MJD8Ip z8w?p<5chatj6VYtFAOngNlb{D?#4SbFU;_4Iu2 z`@KKU^Ss~JAKqRzm0fmy+LC?Oa;F?2J&KENcUgs6sgB4WzSUbX+7}7UKJxWG38T72~b9z;%6MUMs@5vx6%<)1FDy{xO$Pc<9rUH(dMd1szI)b z)u)o!_@o!7yR5j84twfz>#{3JxHH_Dq+d++!#lANcSh~_s^dC^iR5SNCs8*F$C-Fg<-_fZ~(2`OI`ES-7N5|#}c8rE#nXJmkd1781 z%(`l{CTJ(U7_pWw7AHFpvTIYTSK!*&Anxw6;*YT^G)+{xq-#OQr+SP&1S_d9jUTJM zXdZ9JCEGLj{&Wb_n{2S2t6z1BcG*QQG*0-pEE<6^O-y~cj)a8_vzT1?nB&b+C#KJ} z89DA%v`mdzVH*#6=SVAdPY6Bg7;d$@Sb8~&+p)<O{aX*%LqB3opsZ2Kv)ml(Z=ZQhr2kC9V_Smkco1Z-1W zlntvENMc;;;a-uHkDnrjIIYrAoGbD)dl<9nH6o-TV?C;ZYKMA7bumJ}u@+V9ab?_( z8NX#!v79uU2WJn~VjyfkEF|{x&&N127DIdrhIztrSkop#pW`E0)S;kZN+BuO(r<_r z(ihqxhry5x!%Seq(v6;$#)k85tsb-8HMnlKx_s&o(Hi3J zJc2o-=+QXUZIO8?2`L_(_$^%5QA|iwy`ZA%Ft_ejeMRXPm^@n3x#oH(F9AQzPYMi& z!|QRiJAiw&Zv5K4*0~^1xIFP8hA(VhPC#TvaQi~gpJvH2{U*Vt)bhZHfHUyF{_JvQ z!k25EC(Mi~k~(NWo4l{)R`$@zigv1DLo`BU{w!;yrHk)vTrp3+y@oAD(B+CSa?$=g zflN{YC(zifBkVYU+ZPA%*N9v9sX$?Or!ih4kpLR1bVMfuuEND6IIF#k$g$*S8g-X8 zmYGszf6y*@*T~s8NAXWDffYzg!M&q$feESoZNL)Co0lk}hv4ddBfy#8rBLlt)!_uKn6l`8AMK zQ8Y|?{}qZ^mtTuvyH)r482JG*xsscEcsgoOZnu|8CI6dPeZi4Y0?k^JU@3}<5abM(J)_aCfPz7PNa diff --git a/docs/index.html b/docs/index.html index 1e46fa71d3c7efebb33fd233f2112558e8909e25..92b6eb2e0ce6e87f216d7c2d6a0f2e0f65ff9d01 100644 GIT binary patch delta 2291 zcmaJ?YfRf!6u*bZcq0gvGFn=;xjHDYLY+7#;jw{08Dzks63I$i9$Q~&3pirJNC=tx zAk&-dCd4hBMyHz+$=_tg?2F(0FcCE_%OpV)KVVp#izdbxALrcLLg&B_{r~U(ywC6a z?&lK$_^f45Ew0x-$IG8M^hWytcXwEMY1<@k zaWwI0dkcyA&5lWN)&W-j+c7)e+0l;5vr_S9=d5&Catuia$KT4~m9Dqq4nzDzM~WCq z0p)SCW1L5OtN476igu@amM`^Y^AqC-E~G=;`%;3K^9{5%*hvj zDb@xCPG)M$BR}hT@43hL%JivD>i z=>mgR`f-h==r9Ygqxg%kW_h(=~K2;jGwxf0Ep6Lq6fVn$Rw zF)I(+DQ_3bJj{(Yem0EK2|PvE2%bDFj3+00c~KTd8QI0-`^IUgqV- zTf22mI-yknGnr*156_l8NL0N@0rgmYvv4jSku`hx|(zJBXrBmISHAWUj&R` zQMscgD9x%uw_fZOJLZdiU5*ojY@j0=O;B7wlki|xH-Zv_ojZq%Ur=yGFn^d)b#AmG z00|r=8H#<4VM8&*;X^IDQV!4~zOceN>G|L%dU3lF3X>6a8c7kQ$t;+VRVk9h=*lnG7l=t4 z-*e{CN0hr0sTn|fx4760jZ-wpX5_$ed~aevyN)WOE3PsU zpVj80W#hw?gkJ0_;Yn>Lx(9UaUdD1{|M*l9i1)<)ko2Q{dJ4bHNz6ijS4NAxAQpBQ zg_|2ACgqF2m$a#Kob?tgne5cAv0&2pAZhuB;_#z zv+(diz_rb)a@NRJ!qDh|uq z2-ez4Si6kI$WkQ?H=`_ZN8(&j6JWy$TVmAUb+!w7BoG=NtdbKrX`bBGfqTq~Lv@lc3Ki-Neh2_ZO& zb3JgfE?MPdJ!~UfeLp|ZsH|u0G{<_;P8Z%CXu~ej7x4wcsOQlTG@KyL>Um47-)RTJ zf<|aN%sL+{>;@hw#!I+bT}+y{Mu^p z4YlGFrQ3#P?ed$QZGlUj+7*5`T?j=du6}`S#&tGar^8(~IdCV2LYDk)TXCda4q^gC zocCfT-ZXH%icogKoz#?+0gSN=4N{!FvQZa4J6S)DqEoOmB%3;DHD*DTrcjk&2oh{T(CWD_W{b~FJY24sWS#d57pE<}5;Bc*#LXn01VNd)5ECoMjs1{fG|g^A z$h*+_3O}r{=_3n#t;-VkEL<~bCs=EeV0%WerIH1T=y>*c7G z8apt;Rylqb;@?4;IZynY&yAu?&qISTOR8;Z%_9t8;=c0w+31$( zK|&x{wje-l2rn_zfo=!IrUJfS_W0po0T*K?9xBe}@QeTv1zPaZ2G$(c%79|L)@&*? zv5HjGhfaLe@C75oip+H&n0?F*xnIVssG@F3|IG-O26Gqk5U*%Zd4;o|wzTbfKd%ogiEnW=>%d@rj;D|u&bqu!gh)%^Q(Hlf z!H+xV!@?T|*g8B1cHA(;P6-qYT~FJ+HQ~svGhFN|<{sfF;T6G%4k4l!7fN`B-i6hs zT$Fk%)bk-kly(cfnOjhME;u(5`}*V-Nj)nthjOG_BSsk56n#BOK9_N>LRLTZ_&R)| z^MtxE#ZSFJ$C}V)4^n~}kQ=F5j9&=UI{TTKMI3v(lphMB=8zDtV}=Q1jj{mguroki zW4x%fasznkbHeJ-uF~gS;<<9ZAVOiVl4rU|vGUmTbo5H?z+a2>DGaBJU-R)?`Ax?) z!i>kKxSP*os4=PG^1yWMq$34Q`M8<|mf9-VyQDI4Zs4Ia;Xubqn2vZteYN5rWtK7P z;pR-=J72w<^avhU9NKJOQ5D+%3(;^&;VluT=AIh2TqY`;o0+h5?U)mFge)Yb6N_fR z%uSdZo&QQ(Uy64sE>+8hXC=M$;z}Dth{a-M7?1QFDo;qhv_>yF!nh`tbQVz70IYRL zM&$$MU!d@FkVOuoKNbfFG)Yjei1+W YrcuAg0%IEWv9XyvGx(UfimTK92K{C}yZ`_I