Modules/Outputs/Reports/20-OfficeOutputs.ps1
|
function ConvertTo-RangerXmlText { param( [AllowNull()] $Value ) if ($null -eq $Value) { return '' } return [System.Security.SecurityElement]::Escape([string]$Value) } function ConvertTo-RangerOfficeText { param( [AllowNull()] $Value ) if ($null -eq $Value) { return '' } if ($Value -is [bool]) { return $(if ($Value) { 'Yes' } else { 'No' }) } if ($Value -is [datetime]) { return $Value.ToString('u').TrimEnd('Z').Trim() } if ($Value -is [System.Collections.IDictionary]) { return ((@($Value.Keys | ForEach-Object { '{0}={1}' -f $_, (ConvertTo-RangerOfficeText -Value $Value[$_]) })) -join '; ') } if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { return ((@($Value | ForEach-Object { ConvertTo-RangerOfficeText -Value $_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) -join '; ') } return [string]$Value } function Get-RangerObjectValue { param( [AllowNull()] $InputObject, [Parameter(Mandatory = $true)] [string[]]$CandidateNames ) foreach ($name in $CandidateNames) { if ($null -eq $InputObject) { continue } if ($InputObject -is [System.Collections.IDictionary] -and $InputObject.Contains($name)) { return $InputObject[$name] } $property = $InputObject.PSObject.Properties[$name] if ($property) { return $property.Value } } return $null } function Split-RangerWrappedText { param( [AllowNull()] [string]$Text, [int]$Width = 96 ) if ([string]::IsNullOrEmpty($Text)) { return @('') } $lines = New-Object System.Collections.Generic.List[string] foreach ($rawLine in (($Text -replace "`r", '') -split "`n")) { if ($rawLine.Length -le $Width) { $lines.Add($rawLine) continue } $remaining = $rawLine.TrimEnd() while ($remaining.Length -gt $Width) { $window = $remaining.Substring(0, $Width) $breakIndex = $window.LastIndexOf(' ') if ($breakIndex -lt ([math]::Floor($Width / 2))) { $breakIndex = $Width } $lines.Add($remaining.Substring(0, $breakIndex).TrimEnd()) $remaining = $remaining.Substring($breakIndex).TrimStart() } $lines.Add($remaining) } return @($lines) } function Get-RangerReportPlainTextLines { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Report, [int]$WrapWidth = 96 ) $lines = New-Object System.Collections.Generic.List[string] $lines.Add($Report.Title) $lines.Add(('=' * [math]::Max(8, $Report.Title.Length))) $lines.Add('') $lines.Add("Cluster: $($Report.ClusterName)") $lines.Add("Mode: $($Report.Mode)") $lines.Add("Ranger Version: $($Report.Version)") $lines.Add("Generated: $($Report.GeneratedAt)") $lines.Add('') $lines.Add('Table of Contents') $lines.Add('-----------------') foreach ($heading in @($Report.TableOfContents)) { $lines.Add("- $heading") } $lines.Add('') foreach ($section in @($Report.Sections)) { $lines.Add($section.heading) $lines.Add(('-' * [math]::Max(6, $section.heading.Length))) foreach ($entry in @($section.body)) { $lines.Add("- $entry") } $lines.Add('') } $lines.Add('Recommendations') $lines.Add('---------------') if (@($Report.Recommendations).Count -eq 0) { $lines.Add('- No recommendations were surfaced for this output tier.') } else { foreach ($recommendation in @($Report.Recommendations)) { $lines.Add(("- [{0}] {1}: {2}" -f $recommendation.severity.ToUpperInvariant(), $recommendation.title, $recommendation.recommendation)) } } $lines.Add('') $lines.Add('Findings') $lines.Add('--------') if (@($Report.Findings).Count -eq 0) { $lines.Add('- No findings were recorded for this output tier.') } else { foreach ($finding in @($Report.Findings)) { $lines.Add(("[{0}] {1}" -f $finding.severity.ToUpperInvariant(), $finding.title)) foreach ($wrappedLine in @(Split-RangerWrappedText -Text $finding.description -Width $WrapWidth)) { $lines.Add(" $wrappedLine") } if ($finding.currentState) { $lines.Add(" Current state: $($finding.currentState)") } if ($finding.recommendation) { $lines.Add(" Recommendation: $($finding.recommendation)") } if (@($finding.affectedComponents).Count -gt 0) { $lines.Add((" Affected components: {0}" -f (@($finding.affectedComponents) -join ', '))) } $lines.Add('') } } $wrapped = New-Object System.Collections.Generic.List[string] foreach ($line in @($lines)) { foreach ($wrappedLine in @(Split-RangerWrappedText -Text $line -Width $WrapWidth)) { $wrapped.Add($wrappedLine) } } return @($wrapped) } function Write-RangerZipEntry { param( [Parameter(Mandatory = $true)] [System.IO.Compression.ZipArchive]$Archive, [Parameter(Mandatory = $true)] [string]$EntryPath, [Parameter(Mandatory = $true)] [AllowEmptyString()] [string]$Content ) $entry = $Archive.CreateEntry($EntryPath) $stream = $entry.Open() try { $writer = New-Object System.IO.StreamWriter($stream, [System.Text.UTF8Encoding]::new($false)) try { $writer.Write($Content) } finally { $writer.Dispose() } } finally { $stream.Dispose() } } function New-RangerDocxParagraphXml { param( [Parameter(Mandatory = $true)] [string]$Text, [string]$Style = 'Normal' ) $escaped = ConvertTo-RangerXmlText -Value $Text $styleXml = if ($Style -and $Style -ne 'Normal') { '<w:pPr><w:pStyle w:val="{0}"/></w:pPr>' -f $Style } else { '' } '<w:p>{0}<w:r><w:t xml:space="preserve">{1}</w:t></w:r></w:p>' -f $styleXml, $escaped } function Write-RangerDocxReport { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Report, [Parameter(Mandatory = $true)] [string]$Path ) if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Force } $paragraphs = New-Object System.Collections.Generic.List[string] $paragraphs.Add((New-RangerDocxParagraphXml -Text $Report.Title -Style 'Title')) $paragraphs.Add((New-RangerDocxParagraphXml -Text "Cluster: $($Report.ClusterName)")) $paragraphs.Add((New-RangerDocxParagraphXml -Text "Mode: $($Report.Mode)")) $paragraphs.Add((New-RangerDocxParagraphXml -Text "Ranger Version: $($Report.Version)")) $paragraphs.Add((New-RangerDocxParagraphXml -Text "Generated: $($Report.GeneratedAt)")) $paragraphs.Add((New-RangerDocxParagraphXml -Text 'Table of Contents' -Style 'Heading1')) foreach ($heading in @($Report.TableOfContents)) { $paragraphs.Add((New-RangerDocxParagraphXml -Text "- $heading" -Style 'ListParagraph')) } foreach ($section in @($Report.Sections)) { $paragraphs.Add((New-RangerDocxParagraphXml -Text $section.heading -Style 'Heading1')) foreach ($entry in @($section.body)) { $paragraphs.Add((New-RangerDocxParagraphXml -Text "- $entry" -Style 'ListParagraph')) } } $paragraphs.Add((New-RangerDocxParagraphXml -Text 'Recommendations' -Style 'Heading1')) if (@($Report.Recommendations).Count -eq 0) { $paragraphs.Add((New-RangerDocxParagraphXml -Text '- No recommendations were surfaced for this output tier.' -Style 'ListParagraph')) } else { foreach ($recommendation in @($Report.Recommendations)) { $paragraphs.Add((New-RangerDocxParagraphXml -Text ("- [{0}] {1}: {2}" -f $recommendation.severity.ToUpperInvariant(), $recommendation.title, $recommendation.recommendation) -Style 'ListParagraph')) } } $paragraphs.Add((New-RangerDocxParagraphXml -Text 'Findings' -Style 'Heading1')) if (@($Report.Findings).Count -eq 0) { $paragraphs.Add((New-RangerDocxParagraphXml -Text '- No findings were recorded for this output tier.' -Style 'ListParagraph')) } else { foreach ($finding in @($Report.Findings)) { $paragraphs.Add((New-RangerDocxParagraphXml -Text ("[{0}] {1}" -f $finding.severity.ToUpperInvariant(), $finding.title) -Style 'Heading2')) $paragraphs.Add((New-RangerDocxParagraphXml -Text $finding.description)) if ($finding.currentState) { $paragraphs.Add((New-RangerDocxParagraphXml -Text "Current state: $($finding.currentState)")) } if ($finding.recommendation) { $paragraphs.Add((New-RangerDocxParagraphXml -Text "Recommendation: $($finding.recommendation)")) } if (@($finding.affectedComponents).Count -gt 0) { $paragraphs.Add((New-RangerDocxParagraphXml -Text ("Affected components: {0}" -f (@($finding.affectedComponents) -join ', ')))) } } } $documentXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" mc:Ignorable="w14 wp14"> <w:body> $($paragraphs -join "`n ") <w:sectPr> <w:pgSz w:w="12240" w:h="15840"/> <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="708" w:footer="708" w:gutter="0"/> </w:sectPr> </w:body> </w:document> "@ $stylesXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"> <w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/></w:style> <w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:rPr><w:b/><w:sz w:val="32"/></w:rPr></w:style> <w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:rPr><w:b/><w:sz w:val="28"/></w:rPr></w:style> <w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:rPr><w:b/><w:sz w:val="24"/></w:rPr></w:style> <w:style w:type="paragraph" w:styleId="ListParagraph"><w:name w:val="List Paragraph"/><w:basedOn w:val="Normal"/><w:ind w:left="360"/></w:style> </w:styles> "@ $contentTypesXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/> <Default Extension="xml" ContentType="application/xml"/> <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/> <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/> <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/> <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/> </Types> "@ $rootRelsXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/> <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/> <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/> </Relationships> "@ $documentRelsXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/> </Relationships> "@ $coreXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <dc:title>$(ConvertTo-RangerXmlText -Value $Report.Title)</dc:title> <dc:creator>AzureLocalRanger</dc:creator> <cp:lastModifiedBy>AzureLocalRanger</cp:lastModifiedBy> <dcterms:created xsi:type="dcterms:W3CDTF">$((Get-Date).ToUniversalTime().ToString('s'))Z</dcterms:created> <dcterms:modified xsi:type="dcterms:W3CDTF">$((Get-Date).ToUniversalTime().ToString('s'))Z</dcterms:modified> </cp:coreProperties> "@ $appXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"> <Application>AzureLocalRanger</Application> </Properties> "@ $archive = [System.IO.Compression.ZipFile]::Open($Path, [System.IO.Compression.ZipArchiveMode]::Create) try { Write-RangerZipEntry -Archive $archive -EntryPath '[Content_Types].xml' -Content $contentTypesXml Write-RangerZipEntry -Archive $archive -EntryPath '_rels/.rels' -Content $rootRelsXml Write-RangerZipEntry -Archive $archive -EntryPath 'docProps/core.xml' -Content $coreXml Write-RangerZipEntry -Archive $archive -EntryPath 'docProps/app.xml' -Content $appXml Write-RangerZipEntry -Archive $archive -EntryPath 'word/document.xml' -Content $documentXml Write-RangerZipEntry -Archive $archive -EntryPath 'word/styles.xml' -Content $stylesXml Write-RangerZipEntry -Archive $archive -EntryPath 'word/_rels/document.xml.rels' -Content $documentRelsXml } finally { $archive.Dispose() } } function ConvertTo-RangerPdfLiteral { param( [AllowNull()] [string]$Text ) if ($null -eq $Text) { return '' } return (($Text -replace '\\', '\\\\') -replace '\(', '\\(' -replace '\)', '\\)') } function Write-RangerPdfReport { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Report, [Parameter(Mandatory = $true)] [string]$Path ) $allLines = @(Get-RangerReportPlainTextLines -Report $Report -WrapWidth 92) $linesPerPage = 50 $pageSets = New-Object System.Collections.Generic.List[object] for ($index = 0; $index -lt $allLines.Count; $index += $linesPerPage) { $remaining = $allLines.Count - $index $take = [math]::Min($linesPerPage, $remaining) $pageSets.Add(@($allLines[$index..($index + $take - 1)])) } if ($pageSets.Count -eq 0) { $pageSets.Add(@('AzureLocalRanger report')) } $objectBodies = @{} $pageObjectIds = New-Object System.Collections.Generic.List[int] $fontObjectId = (3 + ($pageSets.Count * 2)) $nextObjectId = 3 foreach ($pageLines in $pageSets) { $contentBuilder = New-Object System.Text.StringBuilder [void]$contentBuilder.AppendLine('BT') [void]$contentBuilder.AppendLine('/F1 10 Tf') [void]$contentBuilder.AppendLine('50 780 Td') [void]$contentBuilder.AppendLine('14 TL') foreach ($line in @($pageLines)) { $literal = ConvertTo-RangerPdfLiteral -Text $(if ([string]::IsNullOrEmpty($line)) { ' ' } else { $line }) [void]$contentBuilder.AppendLine("($literal) Tj") [void]$contentBuilder.AppendLine('T*') } [void]$contentBuilder.AppendLine('ET') $contentStream = $contentBuilder.ToString() $contentObjectId = $nextObjectId $pageObjectId = $nextObjectId + 1 $nextObjectId += 2 $objectBodies[$contentObjectId] = "<< /Length $([System.Text.Encoding]::ASCII.GetByteCount($contentStream)) >>`nstream`n$contentStream`nendstream" $objectBodies[$pageObjectId] = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 $fontObjectId 0 R >> >> /Contents $contentObjectId 0 R >>" $pageObjectIds.Add($pageObjectId) } $objectBodies[1] = '<< /Type /Catalog /Pages 2 0 R >>' $objectBodies[2] = "<< /Type /Pages /Count $($pageObjectIds.Count) /Kids [$((@($pageObjectIds) | ForEach-Object { "$_ 0 R" }) -join ' ')] >>" $objectBodies[$fontObjectId] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>' $builder = New-Object System.Text.StringBuilder [void]$builder.Append("%PDF-1.4`n%Ranger`n") $offsets = New-Object System.Collections.Generic.List[int] $offsets.Add(0) for ($objectId = 1; $objectId -le $fontObjectId; $objectId++) { $offsets.Add([System.Text.Encoding]::ASCII.GetByteCount($builder.ToString())) [void]$builder.Append("$objectId 0 obj`n$($objectBodies[$objectId])`nendobj`n") } $xrefOffset = [System.Text.Encoding]::ASCII.GetByteCount($builder.ToString()) [void]$builder.Append("xref`n0 $($fontObjectId + 1)`n") [void]$builder.Append("0000000000 65535 f `n") for ($objectId = 1; $objectId -le $fontObjectId; $objectId++) { [void]$builder.Append(([string]::Format('{0:0000000000} 00000 n `n', $offsets[$objectId]))) } [void]$builder.Append("trailer`n<< /Size $($fontObjectId + 1) /Root 1 0 R >>`nstartxref`n$xrefOffset`n%%EOF") [System.IO.File]::WriteAllText($Path, $builder.ToString(), [System.Text.Encoding]::ASCII) } function ConvertTo-RangerExcelColumnName { param( [Parameter(Mandatory = $true)] [int]$Index ) $name = '' $current = $Index while ($current -gt 0) { $remainder = ($current - 1) % 26 $name = [char](65 + $remainder) + $name $current = [math]::Floor(($current - 1) / 26) } return $name } function Get-RangerExcelSheetDefinitions { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest ) $summary = Get-RangerManifestSummary -Manifest $Manifest $overviewRows = @( [ordered]@{ Metric = 'Cluster'; Value = $summary.ClusterName } [ordered]@{ Metric = 'Mode'; Value = $Manifest.run.mode } [ordered]@{ Metric = 'Generated'; Value = $Manifest.run.endTimeUtc } [ordered]@{ Metric = 'Nodes'; Value = $summary.NodeCount } [ordered]@{ Metric = 'VMs'; Value = $summary.VmCount } [ordered]@{ Metric = 'Azure resources'; Value = $summary.AzureResourceCount } [ordered]@{ Metric = 'Successful collectors'; Value = $summary.SuccessfulCollectors } [ordered]@{ Metric = 'Partial collectors'; Value = $summary.PartialCollectors } [ordered]@{ Metric = 'Failed collectors'; Value = $summary.FailedCollectors } ) $nodeRows = @( @($Manifest.domains.clusterNode.nodes) | ForEach-Object { [ordered]@{ Node = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('name', 'node')) State = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('state', 'status')) Manufacturer = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('manufacturer')) Model = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('model')) Serial = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('serialNumber', 'serial')) CPU = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('processorModel', 'cpuModel')) MemoryGiB = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('memoryGiB', 'memoryGb')) OS = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('osVersion', 'operatingSystem')) } } ) $storageRows = @( @($Manifest.domains.storage.physicalDisks) | ForEach-Object { [ordered]@{ Disk = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('friendlyName', 'name')) MediaType = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('mediaType')) HealthStatus = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('healthStatus')) Operational = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('operationalStatus')) SizeGiB = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('sizeGiB')) Usage = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('usage')) Serial = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('serialNumber', 'serial')) Slot = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('slot', 'slotNumber')) } } ) $networkRows = @( @($Manifest.domains.networking.adapters) | ForEach-Object { [ordered]@{ Node = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('node')) Adapter = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('name', 'interfaceAlias')) Status = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('status', 'state')) LinkSpeedGbps = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('linkSpeedGbps', 'linkSpeedGb')) MacAddress = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('macAddress')) VirtualSwitch = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('virtualSwitch', 'vswitch')) RdmaEnabled = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('rdmaEnabled')) DriverVersion = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('driverVersion')) } } ) $vmRows = @( @($Manifest.domains.virtualMachines.inventory) | ForEach-Object { [ordered]@{ VM = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('name')) Host = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('host', 'computerName')) State = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('state', 'status')) Generation = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('generation')) VcpuCount = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('processorCount', 'vcpuCount')) MemoryStartup = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('memoryStartupGb', 'memoryStartupGiB', 'memoryStartupMb')) Checkpoints = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('checkpointCount')) WorkloadFamily = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('workloadFamily')) } } ) $azureRows = @( @($Manifest.domains.azureIntegration.resources) | ForEach-Object { [ordered]@{ Name = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('Name', 'name')) ResourceType = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('ResourceType', 'resourceType', 'Type', 'type')) Location = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('Location', 'location')) ResourceGroup = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('ResourceGroupName', 'resourceGroup')) Subscription = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('SubscriptionId', 'subscriptionId')) Id = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('ResourceId', 'Id', 'id')) } } ) $findingRows = @( @($Manifest.findings) | ForEach-Object { [ordered]@{ Severity = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('severity')) Title = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('title')) Description = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('description')) CurrentState = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('currentState')) Recommendation = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('recommendation')) AffectedComponents = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $_ -CandidateNames @('affectedComponents')) } } ) $collectorRows = @( @($Manifest.collectors.Keys | Sort-Object | ForEach-Object { $collector = $Manifest.collectors[$_] [ordered]@{ Collector = $_ Status = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $collector -CandidateNames @('status')) TargetScope = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $collector -CandidateNames @('targetScope')) DurationMs = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $collector -CandidateNames @('durationMs', 'durationMilliseconds')) Evidence = ConvertTo-RangerOfficeText -Value (Get-RangerObjectValue -InputObject $collector -CandidateNames @('rawEvidencePath')) } }) ) return @( [ordered]@{ Name = 'Overview'; Columns = @('Metric', 'Value'); Rows = $overviewRows } [ordered]@{ Name = 'Nodes'; Columns = @('Node', 'State', 'Manufacturer', 'Model', 'Serial', 'CPU', 'MemoryGiB', 'OS'); Rows = $nodeRows } [ordered]@{ Name = 'Storage'; Columns = @('Disk', 'MediaType', 'HealthStatus', 'Operational', 'SizeGiB', 'Usage', 'Serial', 'Slot'); Rows = $storageRows } [ordered]@{ Name = 'Networking'; Columns = @('Node', 'Adapter', 'Status', 'LinkSpeedGbps', 'MacAddress', 'VirtualSwitch', 'RdmaEnabled', 'DriverVersion'); Rows = $networkRows } [ordered]@{ Name = 'VirtualMachines'; Columns = @('VM', 'Host', 'State', 'Generation', 'VcpuCount', 'MemoryStartup', 'Checkpoints', 'WorkloadFamily'); Rows = $vmRows } [ordered]@{ Name = 'AzureResources'; Columns = @('Name', 'ResourceType', 'Location', 'ResourceGroup', 'Subscription', 'Id'); Rows = $azureRows } [ordered]@{ Name = 'Findings'; Columns = @('Severity', 'Title', 'Description', 'CurrentState', 'Recommendation', 'AffectedComponents'); Rows = $findingRows } [ordered]@{ Name = 'Collectors'; Columns = @('Collector', 'Status', 'TargetScope', 'DurationMs', 'Evidence'); Rows = $collectorRows } ) } function Write-RangerExcelWorkbook { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [string]$Path ) if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Force } $sheetDefinitions = @(Get-RangerExcelSheetDefinitions -Manifest $Manifest) $worksheetEntries = New-Object System.Collections.Generic.List[object] $sheetId = 1 foreach ($sheetDefinition in $sheetDefinitions) { $columns = @($sheetDefinition.Columns) $rows = @($sheetDefinition.Rows) $maxRow = [math]::Max(2, $rows.Count + 1) $lastColumnName = ConvertTo-RangerExcelColumnName -Index $columns.Count $sheetDataRows = New-Object System.Collections.Generic.List[string] $headerCells = New-Object System.Collections.Generic.List[string] for ($columnIndex = 0; $columnIndex -lt $columns.Count; $columnIndex++) { $cellReference = '{0}1' -f (ConvertTo-RangerExcelColumnName -Index ($columnIndex + 1)) $headerCells.Add(('<c r="{0}" t="inlineStr" s="1"><is><t xml:space="preserve">{1}</t></is></c>' -f $cellReference, (ConvertTo-RangerXmlText -Value $columns[$columnIndex]))) } $sheetDataRows.Add(('<row r="1">{0}</row>' -f ($headerCells -join ''))) for ($rowIndex = 0; $rowIndex -lt $rows.Count; $rowIndex++) { $row = $rows[$rowIndex] $cellXml = New-Object System.Collections.Generic.List[string] for ($columnIndex = 0; $columnIndex -lt $columns.Count; $columnIndex++) { $columnName = $columns[$columnIndex] $value = ConvertTo-RangerOfficeText -Value $(if ($row -is [System.Collections.IDictionary] -and $row.Contains($columnName)) { $row[$columnName] } else { $null }) $cellReference = '{0}{1}' -f (ConvertTo-RangerExcelColumnName -Index ($columnIndex + 1)), ($rowIndex + 2) $cellXml.Add(('<c r="{0}" t="inlineStr"><is><t xml:space="preserve">{1}</t></is></c>' -f $cellReference, (ConvertTo-RangerXmlText -Value $value))) } $sheetDataRows.Add(('<row r="{0}">{1}</row>' -f ($rowIndex + 2), ($cellXml -join ''))) } $worksheetXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> <dimension ref="A1:$lastColumnName$maxRow"/> <sheetViews> <sheetView workbookViewId="0"> <pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/> </sheetView> </sheetViews> <sheetFormatPr defaultRowHeight="15"/> <sheetData> $($sheetDataRows -join "`n ") </sheetData> <autoFilter ref="A1:$lastColumnName$maxRow"/> </worksheet> "@ $worksheetEntries.Add([ordered]@{ Name = $sheetDefinition.Name Path = "xl/worksheets/sheet$sheetId.xml" RelId = "rId$sheetId" Xml = $worksheetXml SheetId = $sheetId }) $sheetId++ } $sheetListXml = @($worksheetEntries | ForEach-Object { '<sheet name="{0}" sheetId="{1}" r:id="{2}"/>' -f (ConvertTo-RangerXmlText -Value $_.Name), $_.SheetId, $_.RelId }) -join "`n " $workbookXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> <sheets> $sheetListXml </sheets> </workbook> "@ $workbookRelsXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> $((@($worksheetEntries | ForEach-Object { '<Relationship Id="{0}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{1}.xml"/>' -f $_.RelId, $_.SheetId }) + '<Relationship Id="rId99" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>') -join "`n ") </Relationships> "@ $stylesXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> <fonts count="2"> <font><sz val="11"/><name val="Calibri"/></font> <font><b/><sz val="11"/><name val="Calibri"/><color rgb="FFFFFFFF"/></font> </fonts> <fills count="3"> <fill><patternFill patternType="none"/></fill> <fill><patternFill patternType="gray125"/></fill> <fill><patternFill patternType="solid"><fgColor rgb="FF0F4C81"/><bgColor indexed="64"/></patternFill></fill> </fills> <borders count="2"> <border><left/><right/><top/><bottom/><diagonal/></border> <border><left style="thin"/><right style="thin"/><top style="thin"/><bottom style="thin"/><diagonal/></border> </borders> <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs> <cellXfs count="2"> <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/> <xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1"/> </cellXfs> <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles> </styleSheet> "@ $contentTypesXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/> <Default Extension="xml" ContentType="application/xml"/> <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/> <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/> $(@($worksheetEntries | ForEach-Object { '<Override PartName="/xl/worksheets/sheet{0}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' -f $_.SheetId }) -join "`n ") </Types> "@ $rootRelsXml = @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/> </Relationships> "@ $archive = [System.IO.Compression.ZipFile]::Open($Path, [System.IO.Compression.ZipArchiveMode]::Create) try { Write-RangerZipEntry -Archive $archive -EntryPath '[Content_Types].xml' -Content $contentTypesXml Write-RangerZipEntry -Archive $archive -EntryPath '_rels/.rels' -Content $rootRelsXml Write-RangerZipEntry -Archive $archive -EntryPath 'xl/workbook.xml' -Content $workbookXml Write-RangerZipEntry -Archive $archive -EntryPath 'xl/_rels/workbook.xml.rels' -Content $workbookRelsXml Write-RangerZipEntry -Archive $archive -EntryPath 'xl/styles.xml' -Content $stylesXml foreach ($worksheetEntry in $worksheetEntries) { Write-RangerZipEntry -Archive $archive -EntryPath $worksheetEntry.Path -Content $worksheetEntry.Xml } } finally { $archive.Dispose() } } |