Modules/M365DSCReport.psm1
function New-M365DSCConfigurationToHTML { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [System.String] $ConfigurationPath, [Parameter(Mandatory = $true)] [System.String] $OutputPath ) $TemplateFile = Get-Item $ConfigurationPath $parsedContent = ConvertTo-DSCObject -Path $ConfigurationPath $TemplateName = $TemplateFile.Name.Split('.')[0] $fullHTML = "<h1>" + $TemplateName + "</h1>" $fullHTML += "<div style='width:100%;text-align:center;'>" $fullHTML += "<h2>Template Details</h2>" foreach ($resource in $parsedContent) { $partHTML = "<div width='100%' style='text-align:center;'><table width='80%' style='margin-left:auto; margin-right:auto;'>" $partHTML += "<tr><th rowspan='" + ($resource.Keys.Count) + "' width='20%'>" $partHTML += "<img src='" + (Get-IconPath -ResourceName $resource.ResourceName) + "' />" $partHTML += "</th>" $partHTML += "<th colspan='2' style='background-color:silver;text-align:center;' width='80%'>" $partHTML += "<strong>" + $resource.ResourceName + "</strong>" $partHTML += "</th></tr>" foreach ($property in $resource.Keys) { if ($property -ne "ResourceName" -and $property -ne "GlobalAdminAccount") { $partHTML += "<tr><td width='40%' style='padding:5px;text-align:right;border:1px solid black;'><strong>" + $property + "</strong></td>" $value = "Null" if ($null -ne $resource.$property) { $value = ($resource.$property).ToString().Replace("$","") } $partHTML += "<td width='40%' style='padding:5px;border:1px solid black;'>" + $value + "</td></tr>" } } $partHTML += "</table></div><br />" $fullHtml += $partHTML } $fullHtml | Out-File $OutputPath } function Get-IconPath { [CmdletBinding()] [OutputType([System.String])] param( [Parameter(Mandatory = $true)] [System.String] $ResourceName ) if ($ResourceName.StartsWith("AAD")) { return "http://microsoft365dsc.com/Images/AzureAD.jpg" } elseif ($ResourceName.StartsWith("EXO")) { return "http://microsoft365dsc.com/Images/Exchange.jpg" } elseif ($ResourceName.StartsWith("O365")) { return "http://microsoft365dsc.com/Images/Office365.jpg" } elseif ($ResourceName.StartsWith("OD")) { return "http://microsoft365dsc.com/Images/OneDrive.jpg" } elseif ($ResourceName.StartsWith("PP")) { return "http://microsoft365dsc.com/Images/PowerApps.jpg" } elseif ($ResourceName.StartsWith("SC")) { return "http://microsoft365dsc.com/Images/SecurityAndCompliance.png" } elseif ($ResourceName.StartsWith("SPO")) { return "http://microsoft365dsc.com/Images/SharePoint.jpg" } elseif ($ResourceName.StartsWith("Teams")) { return "http://microsoft365dsc.com/Images/Teams.jpg" } return $null } function New-M365DSCConfigurationToExcel { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [System.String] $ConfigurationPath, [Parameter(Mandatory = $true)] [System.String] $OutputPath ) $excel = New-Object -ComObject excel.application $excel.visible = $True $workbook = $excel.Workbooks.Add() $report= $workbook.Worksheets.Item(1) $report.Name = 'Report' $report.Cells.Item(1,1) = "Component Name" $report.Cells.Item(1,1).Font.Size = 18 $report.Cells.Item(1,1).Font.Bold=$True $report.Cells.Item(1,2) = "Property" $report.Cells.Item(1,2).Font.Size = 18 $report.Cells.Item(1,2).Font.Bold=$True $report.Cells.Item(1,3) = "Value" $report.Cells.Item(1,3).Font.Size = 18 $report.Cells.Item(1,3).Font.Bold=$True $report.Range("A1:C1").Borders.Weight = -4138 $row = 2 $parsedContent = ConvertTo-DSCObject -Path $ConfigurationPath foreach ($resource in $parsedContent) { $beginRow = $row foreach ($property in $resource.Keys) { if ($property -ne "ResourceName" -and $property -ne "GlobalAdminAccount") { $report.Cells.Item($row,1) = $resource.ResourceName $report.Cells.Item($row,2) = $property try { if ([System.String]::IsNullOrEmpty($resource.$property)) { $report.Cells.Item($row,3) = "null" } else { $value = ($resource.$property).ToString().Replace("$","") $value = $value.Replace("@","") $value = $value.Replace("(","") $value = $value.Replace(")","") $report.Cells.Item($row,3) = $value } $report.Cells.Item($row,3).HorizontalAlignment = -4131 } catch { Write-Verbose -Message $_ Add-M365DSCEvent -Message $_ -EntryType 'Error' ` -EventID 1 -Source $($MyInvocation.MyCommand.Source) } if ($property -in @("Identity", "Name", "IsSingleInstance")) { $OriginPropertyName = $report.Cells.Item($beginRow, 2).Text $OriginPropertyValue = $report.Cells.Item($beginRow, 3).Text $CurrentPropertyName = $report.Cells.Item($row, 2).Text $CurrentPropertyValue = $report.Cells.Item($row, 3).Text $report.Cells.Item($beginRow, 2) = $CurrentPropertyName $report.Cells.Item($beginRow, 3) = $CurrentPropertyValue $report.Cells.Item($row, 2) = $OriginPropertyName $report.Cells.Item($row, 3) = $OriginPropertyValue $report.Cells($beginRow,1).Font.ColorIndex = 10 $report.Cells($beginRow,2).Font.ColorIndex = 10 $report.Cells($beginRow,3).Font.ColorIndex = 10 $report.Cells($beginRow,1).Font.Bold = $true $report.Cells($beginRow,2).Font.Bold = $true $report.Cells($beginRow,3).Font.Bold = $true } $row++ } } $rangeValue = "A$beginRow" + ":" + "C$row" $report.Range($rangeValue).Borders[8].Weight = -4138 } $usedRange = $report.UsedRange $usedRange.EntireColumn.AutoFit() | Out-Null $workbook.SaveAs($OutputPath) #$excel.Quit() } function New-M365DSCReportFromConfiguration { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [ValidateSet('Excel', 'HTML')] [System.String] $Type, [Parameter(Mandatory = $true)] [System.String] $ConfigurationPath, [Parameter(Mandatory = $true)] [System.String] $OutputPath ) #region Telemetry $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new() $data.Add("Event", "Report") $data.Add("Type", $Type) Add-M365DSCTelemetryEvent -Data $data #endregion switch ($Type) { "Excel" { New-M365DSCConfigurationToExcel -ConfigurationPath $ConfigurationPath -OutputPath $OutputPath } "HTML" { New-M365DSCConfigurationToHTML -ConfigurationPath $ConfigurationPath -OutputPath $OutputPath } } } function Compare-M365DSCConfigurations { [CmdletBinding()] [OutputType([System.Array])] param ( [Parameter(Mandatory = $true)] [System.String] $Source, [Parameter(Mandatory = $true)] [System.String] $Destination, [Parameter()] [System.Boolean] $CaptureTelemetry = $true ) if ($CaptureTelemetry) { #region Telemetry $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new() $data.Add("Event", "Compare") Add-M365DSCTelemetryEvent -Data $data #endregion } [Array] $Delta = @() [Array] $SourceObject = ConvertTo-DSCObject -Path $Source [Array] $DestinationObject = ConvertTo-DSCObject -Path $Destination # Loop through all items in the source array $i = 1 foreach ($sourceResource in $SourceObject) { [array]$key = Get-M365DSCResourceKey -Resource $sourceResource Write-Progress -Activity "Scanning Source $Source...[$i/$($SourceObject.Count)]" -PercentComplete ($i/($SourceObject.Count)*100) [array]$destinationResource = $DestinationObject | Where-Object -FilterScript {$_.ResourceName -eq $sourceResource.ResourceName -and $_.($key[0]) -eq $sourceResource.($key[0])} # Filter on the second key if ($key.Count -gt 1) { [array]$destinationResource = $destinationResource | Where-Object -FilterScript {$_.ResourceName -eq $sourceResource.ResourceName -and $_.($key[1]) -eq $sourceResource.($key[1])} } if ($null -eq $destinationResource) { $drift = @{ ResourceName = $sourceResource.ResourceName Key = $key[0] KeyValue = $sourceResource.($key[0]) Properties = @(@{ ParameterName = 'Ensure' ValueInSource = 'Present' ValueInDestination = 'Absent' }) } $Delta += ,$drift $drift = $null } else { [System.Collections.Hashtable]$destinationResource = $destinationResource[0] # The resource instance exists in both the source and the destination. Compare each property; foreach ($propertyName in $sourceResource.Keys) { # Needs to be a separate nested if statement otherwise the ReferenceObject an be null and it will error out; if((-not [System.String]::IsNullOrEmpty($sourceResource.$propertyName) -and ` -not [System.String]::IsNullOrEmpty($destinationResource.$propertyName)) -and ` $null -ne (Compare-Object -ReferenceObject ($sourceResource.$propertyName)` -DifferenceObject ($destinationResource.$propertyName))) { if ($null -eq $drift) { $drift = @{ ResourceName = $sourceResource.ResourceName Key = $key[0] KeyValue = $sourceResource.($key[0]) Properties = @(@{ ParameterName = $propertyName ValueInSource = $sourceResource.$propertyName ValueInDestination = $destinationResource.$propertyName }) } if ($destinationResource.Contains("_metadata_$($propertyName)")) { $Metadata = $destinationResource."_metadata_$($propertyName)" $Level = $Metadata.Split('|')[0].Replace("### ", "") $Information = $Metadata.Split('|')[1] $drift.Properties[0].Add("_Metadata_Level", $Level) $drift.Properties[0].Add("_Metadata_Info", $Information) } } else { $newDrift = @{ ParameterName = $propertyName ValueInSource = $sourceResource.$propertyName ValueInDestination = $destinationResource.$propertyName } if ($destinationResource.Contains("_metadata_$($propertyName)")) { $Metadata = $destinationResource."_metadata_$($propertyName)" $Level = $Metadata.Split('|')[0].Replace("### ", "") $Information = $Metadata.Split('|')[1] $newDrift.Add("_Metadata_Level", $Level) $newDrift.Add("_Metadata_Info", $Information) } $drift.Properties += $newDrift } } } # Do the scan the other way around because there's a chance that the property, if null, wasn't part of the source # object. By scanning against the destination we will catch properties that are not null on the source but not null in destination; foreach ($propertyName in $destinationResource.Keys) { if (-not $sourceResource.Contains($propertyName)) { if ($null -eq $drift) { $drift = @{ ResourceName = $sourceResource.ResourceName Key = $key[0] KeyValue = $sourceResource.($key[0]) Properties = @(@{ ParameterName = $propertyName ValueInSource = $null ValueInDestination = $destinationResource.$propertyName }) } } else { $drift.Properties += @{ ParameterName = $propertyName ValueInSource = $null ValueInDestination = $destinationResource.$propertyName } } } } if ($null -ne $drift) { $Delta += ,$drift $drift = $null } } $i++ } Write-Progress -Activity "Scanning Source..." -Completed # Loop through all items in the destination array $i = 1 foreach ($destinationResource in $DestinationObject) { [System.Collections.HashTable]$currentDestinationResource = ([array]$destinationResource)[0] $key = Get-M365DSCResourceKey -Resource $currentDestinationResource Write-Progress -Activity "Scanning Destination $Destination...[$i/$($DestinationObject.Count)]" -PercentComplete ($i/($DestinationObject.Count)*100) $sourceResource = $SourceObject | Where-Object -FilterScript {$_.ResourceName -eq $currentDestinationResource.ResourceName -and $_.($key[0]) -eq $currentDestinationResource.($key[0])} # Filter on the second key if ($key.Count -gt 1) { [array]$sourceResource = $sourceResource | Where-Object -FilterScript {$_.ResourceName -eq $currentDestinationResource.ResourceName -and $_.($key[1]) -eq $currentDestinationResource.($key[1])} } if ($null -eq $sourceResource) { $drift = @{ ResourceName = $currentDestinationResource.ResourceName Key = $key[0] KeyValue = $currentDestinationResource.($key[0]) Properties = @(@{ ParameterName = 'Ensure' ValueInSource = 'Absent' ValueInDestination = 'Present' }) } $Delta += ,$drift $drift = $null } $i++ } Write-Progress -Activity "Scanning Destination..." -Completed return $Delta } function Get-M365DSCResourceKey { [CmdletBinding()] [OutputType([System.Object[]])] param( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $Resource ) if ($Resource.Contains("IsSingleInstance")) { return @("IsSingleInstance") } elseif ($Resource.Contains("DisplayName")) { if ($Resource.ResourceName -eq 'AADMSGroup' -and -not [System.String]::IsNullOrEmpty($Resource.Id)) { return @("Id") } return @("DisplayName") } elseif ($Resource.Contains("Identity")) { return @("Identity") } elseif ($Resource.Contains("Name")) { return @("Name") } elseif ($Resource.Contains("Url")) { return @("Url") } elseif ($Resource.Contains("Organization")) { return @("Organization") } elseif ($Resource.Contains("CDNType")) { return @("CDNType") } elseif ($Resource.Contains("Action") -and $Resource.ResourceName -eq 'SCComplianceSearchAction') { return @("SearchName", "Action") } elseif ($Resource.Contains("Workload") -and $Resource.ResourceName -eq 'SCAuditConfigurationPolicy') { return @("Workload") } elseif ($Resource.Contains("Title") -and $Resource.ResourceName -eq 'SPOSiteDesign') { return @("Title") } elseif ($Resource.Contains("SiteDesignTitle")) { return @("SiteDesignTitle") } elseif ($Resource.Contains("Key") -and $Resource.ResourceName -eq 'SPOStorageEntity') { return @("Key") } } function New-M365DSCDeltaReport { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.String] $Source, [Parameter(Mandatory = $true)] [System.String] $Destination, [Parameter(Mandatory=$true)] [System.String] $OutputPath, [Parameter()] [System.Boolean] $DriftOnly = $false, [Parameter()] [System.Boolean] $IsBlueprintAssessment = $false, [Parameter()] [System.String] $HeaderFilePath ) #region Telemetry $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new() $data.Add("Event", "DeltaReport") Add-M365DSCTelemetryEvent -Data $data #endregion Write-Verbose -Message 'Obtaining Delta between the source and destination configurations' $Delta = Compare-M365DSCConfigurations -Source $Source -Destination $Destination -CaptureTelemetry $false $reportSB = [System.Text.StringBuilder]::new() #region Custom Header if (-not [System.String]::IsNullOrEmpty($HeaderFilePath)) { try { $headerContent = Get-Content $HeaderFilePath [void]$reportSB.AppendLine($headerContent) } catch { Write-Verbose -Message $_ Add-M365DSCEvent -Message $_ -EntryType 'Error' ` -EventID 1 -Source $($MyInvocation.MyCommand.Source) } } #endregion $ReportTitle = "Microsoft365DSC - Delta Report" if ($IsBlueprintAssessment) { $ReportTitle = "Microsoft365DSC - Blueprint Assessment Report" [void]$reportSB.AppendLine("<h1>Blueprint Assessment Report</h1>") } else { [void]$reportSB.AppendLine("<h1>Delta Report</h1>") [void]$reportSB.AppendLine("<p><strong>Comparing </strong>$Source <strong>to</strong> $Destination</p>") } [void]$reportSB.AppendLine("<html><head><meta charset='utf-8'><title>$ReportTitle</title></head><body>") [void]$reportSB.AppendLine("<div style='width:100%;text-align:center;'>") [void]$reportSB.AppendLine("<img src='http://Microsoft365DSC.com/Images/Promo.png' alt='Microsoft365DSC Slogan' width='500' />") [void]$ReportSB.AppendLine("</div>") [array]$resourcesMissingInSource = $Delta | Where-Object -FilterScript {$_.Properties.ParameterName -eq 'Ensure' -and ` $_.Properties.ValueInSource -eq 'Absent'} [array]$resourcesMissingInDestination = $Delta | Where-Object -FilterScript {$_.Properties.ParameterName -eq 'Ensure' -and ` $_.Properties.ValueInDestination -eq 'Absent'} [array]$resourcesInDrift = $Delta | Where-Object -FilterScript {$_.Properties.ParameterName -ne 'Ensure'} if ($resourcesMissingInSource.Count -eq 0 -and $resourcesMissingInDestination.Count -eq 0 -and ` $resourcesInDrift.Count -eq 0) { [void]$reportSB.AppendLine("<p><strong>No discrepancies have been found!</strong></p>") } elseif (-not $DriftOnly) { [void]$reportSB.AppendLine("<h2>Table of Contents</h2>") [void]$reportSB.AppendLine("<ul>") if ($resourcesMissingInSource.Count -gt 0) { [void]$reportSB.AppendLine("<li><a href='#Source'>Resources Missing in the Source</a>") [void]$reportSB.AppendLine(" <strong>(</strong>$($resourcesMissingInSource.Count)<strong>)</strong></li>") } if ($resourcesMissingInDestination.Count -gt 0) { [void]$reportSB.AppendLine("<li><a href='#Destination'>Resources Missing in the Destination</a>") [void]$reportSB.AppendLine(" <strong>(</strong>$($resourcesMissingInDestination.Count)<strong>)</strong></li>") } if ($resourcesInDrift.Count -gt 0) { [void]$reportSB.AppendLine("<li><a href='#Drift'>Resources Configured Differently</a>") [void]$reportSB.AppendLine(" <strong>(</strong>$($resourcesInDrift.Count)<strong>)</strong></li>") } [void]$reportSB.AppendLine("</ul>") } if ($resourcesMissingInSource.Count -gt 0 -and -not $DriftOnly) { [void]$reportSB.AppendLine("<br /><hr /><br />") [void]$reportSB.AppendLine("<a id='Source'></a><h2>Resources that are Missing in the Source</h2>") foreach ($resource in $resourcesMissingInSource) { [void]$reportSB.AppendLine("<table width='100%' cellspacing='0' cellpadding='5'>") [void]$reportSB.AppendLine("<tr>") [void]$reportSB.Append("<th style='width:25%;text-align:center;vertical-align:middle;border-left:1px solid black;") [void]$reportSB.Append("border-top:1px solid black;border-bottom:1px solid black;'>") $iconPath = Get-IconPath -ResourceName $resource.ResourceName [void]$reportSB.AppendLine("<img src='$iconPath' />") [void]$reportSB.AppendLine("</th>"); [void]$reportSB.AppendLine("<th style='border:1px solid black;text-align:center;'>") [void]$reportSB.AppendLine("<h3>$($resource.ResourceName) - $($resource.Key) = $($resource.KeyValue)</h3>") [void]$reportSB.AppendLine("</th>") [void]$reportSB.AppendLine("</tr>") [void]$reportSB.AppendLine("</table>") } } if ($resourcesMissingInDestination.Count -gt 0 -and -not $DriftOnly) { [void]$reportSB.AppendLine("<br /><hr /><br />") [void]$reportSB.AppendLine("<a id='Destination'></a><h2>Resources that are Missing in the Destination</h2>") foreach ($resource in $resourcesMissingInDestination) { [void]$reportSB.AppendLine("<table width='100%' cellspacing='0' cellpadding='5'>") [void]$reportSB.AppendLine("<tr>") [void]$reportSB.Append("<th style='width:25%;text-align:center;vertical-align:middle;border-left:1px solid black;") [void]$reportSB.Append("border-top:1px solid black;border-bottom:1px solid black;'>") $iconPath = Get-IconPath -ResourceName $resource.ResourceName [void]$reportSB.AppendLine("<img src='$iconPath' />") [void]$reportSB.AppendLine("</th>"); [void]$reportSB.AppendLine("<th style='border:1px solid black;text-align:center;'>") [void]$reportSB.AppendLine("<h3>$($resource.ResourceName) - $($resource.Key) = $($resource.KeyValue)</h3>") [void]$reportSB.AppendLine("</th>") [void]$reportSB.AppendLine("</tr>") [void]$reportSB.AppendLine("</table>") } } if ($resourcesInDrift.Count -gt 0) { [void]$reportSB.AppendLine("<br /><hr /><br />") [void]$reportSB.AppendLine("<a id='Drift'></a><h2>Resources that are Configured Differently</h2>") foreach ($resource in $resourcesInDrift) { [void]$reportSB.AppendLine("<table width='100%' cellspacing='0' cellpadding='5'>") [void]$reportSB.AppendLine("<tr>") [void]$reportSB.Append("<th style='width:25%;text-align:center;vertical-align:middle;border:1px solid black;;' ") [void]$reportSB.Append("rowspan='" + ($resource.Properties.Count + 2) + "'>") $iconPath = Get-IconPath -ResourceName $resource.ResourceName [void]$reportSB.AppendLine("<img src='$iconPath' />") [void]$reportSB.AppendLine("</th>"); [void]$reportSB.AppendLine("<th style='border:1px solid black;text-align:center;vertical-align:middle;background-color:#CCC' colspan='3'>") [void]$reportSB.AppendLine("<h3>$($resource.ResourceName) - $($resource.Key) = $($resource.KeyValue)</h3>") [void]$reportSB.AppendLine("</th></tr>") [void]$reportSB.AppendLine("<tr>") $SourceLabel = "Source Value" $DestinationLabel = "Destination Value" if ($IsBlueprintAssessment) { $SourceLabel = "Tenant's Current Value" $DestinationLabel = "Blueprint's Value" } [void]$reportSB.AppendLine("<td style='text-align:center;border:1px solid black;background-color:#EEE;' width='45%'><strong>Property</strong></td>") [void]$reportSB.AppendLine("<td style='text-align:center;border:1px solid black;background-color:#EEE;' width='15%'><strong>$SourceLabel</strong></td>") [void]$reportSB.AppendLine("<td style='text-align:center;border:1px solid black;background-color:#EEE;' width='15%'><strong>$DestinationLabel</strong></td>") [void]$reportSB.AppendLine("</tr>") foreach ($drift in $resource.Properties) { if ($drift.ParameterName -notlike '_metadata_*') { $cellStyle = '' $emoticon = '' if ($drift._Metadata_Level -eq 'L1') { $cellStyle = "background-color:#F6CECE;" $emoticon = '🟥' } elseif ($drift._Metadata_Level -eq 'L2') { $cellStyle = "background-color:#F7F8E0;" $emoticon = '🟨' } elseif ($drift._Metadata_Level -eq 'L3') { $cellStyle = "background-color:#FFFFFF;" $emoticon = '🟦' } [void]$reportSB.AppendLine("<tr>") [void]$reportSB.AppendLine("<td style='border:1px solid black;text-align:right;' width='45%'>") [void]$reportSB.AppendLine("$($drift.ParameterName)</td>") [void]$reportSB.AppendLine("<td style='border:1px solid black;$cellStyle' width='15%'>") [void]$reportSB.AppendLine("$($drift.ValueInSource)</td>") [void]$reportSB.AppendLine("<td style='border:1px solid black;' width='15%'>") [void]$reportSB.AppendLine("$($drift.ValueInDestination)</td>") [void]$reportSB.AppendLine("</tr>") if ($null -ne $drift._Metadata_Level) { [void]$reportSB.AppendLine("<tr><td colspan='3' style='border:1px solid black;'>$emoticon $($drift._Metadata_Info)</td></tr>") } } } [void]$reportSB.AppendLine("</table><hr/>") } } [void]$reportSB.AppendLine("</body></html>") $reportSB.ToString() | Out-File $OutputPath Invoke-Item $OutputPath } |