diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e23c30..42cf719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ 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`. + +### 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. diff --git a/README.md b/README.md index ac523d4..4da28d8 100644 Binary files a/README.md and b/README.md differ diff --git a/docs/index.html b/docs/index.html index 1e46fa7..92b6eb2 100644 Binary files a/docs/index.html and b/docs/index.html differ 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" 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/Private/Merge-CISExcelAndCsvData.ps1 b/source/Private/Merge-CISExcelAndCsvData.ps1 deleted file mode 100644 index 7d2f039..0000000 --- a/source/Private/Merge-CISExcelAndCsvData.ps1 +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/source/Private/New-MergedObject.ps1 b/source/Private/New-MergedObject.ps1 deleted file mode 100644 index 50f7497..0000000 --- a/source/Private/New-MergedObject.ps1 +++ /dev/null @@ -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 -} diff --git a/source/Private/Update-CISExcelWorksheet.ps1 b/source/Private/Update-CISExcelWorksheet.ps1 deleted file mode 100644 index 9976f76..0000000 --- a/source/Private/Update-CISExcelWorksheet.ps1 +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/source/Private/Update-WorksheetCell.ps1 b/source/Private/Update-WorksheetCell.ps1 deleted file mode 100644 index 9708c1c..0000000 --- a/source/Private/Update-WorksheetCell.ps1 +++ /dev/null @@ -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++ - } -} diff --git a/source/Public/Export-M365SecurityAuditTable.ps1 b/source/Public/Export-M365SecurityAuditTable.ps1 index bc15887..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 #> @@ -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 + } } } } 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 +} diff --git a/source/Public/Invoke-M365SecurityAudit.ps1 b/source/Public/Invoke-M365SecurityAudit.ps1 index cfc5aed..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 { @@ -240,13 +243,20 @@ 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 -SkipConfirmation:$DoNotConfirmConnections + } + } + catch { + Write-Host "Execution aborted: $_" -ForegroundColor Red + break } + Write-Information "A total of $($totalTests) tests were selected to run..." -InformationAction Continue # Import the test functions $testFiles | ForEach-Object { diff --git a/source/Public/Sync-CISExcelAndCsvData.ps1 b/source/Public/Sync-CISExcelAndCsvData.ps1 index 8e91b95..7dfa467 100644 --- a/source/Public/Sync-CISExcelAndCsvData.ps1 +++ b/source/Public/Sync-CISExcelAndCsvData.ps1 @@ -1,90 +1,102 @@ <# -.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 + .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], [PSCustomObject[]])] - [CmdletBinding(DefaultParameterSetName = 'CsvInput')] - param ( - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-Path $_ })] + [OutputType([void])] + [CmdletBinding()] + param( [string]$ExcelPath, + [string]$CsvPath, + [string]$SheetName + ) - [Parameter(Mandatory = $true)] - [string]$WorksheetName, + # Import the CSV file + $csvData = Import-Csv -Path $CsvPath - [Parameter(Mandatory = $true, ParameterSetName = 'CsvInput')] - [ValidateScript({ Test-Path $_ })] - [string]$CsvPath, + # Get the current date in the specified format + $currentDate = Get-Date -Format "yyyy-MM-ddTHH:mm:ss" - [Parameter(Mandatory = $true, ParameterSetName = 'ObjectInput', ValueFromPipeline = $true)] - [CISAuditResult[]]$AuditResults, + # Load the Excel workbook + $excelPackage = Open-ExcelPackage -Path $ExcelPath + $worksheet = $excelPackage.Workbook.Worksheets[$SheetName] - [Parameter(Mandatory = $false)] - [switch]$SkipUpdate - ) + # 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 - 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 + # Add new headers if they do not exist + foreach ($header in $newHeaders) { + if ($header -notin $existingHeaders) { + $lastCol++ + $worksheet.Cells[1, $lastCol].Value = $header } + } - # 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 + # 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 + } } - # 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 + # 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 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' - } - } - } -} -