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, + [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 { - $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 } + } - 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 + } } - if ($SkipUpdate) { - return $mergedData - } else { - 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