Modules/Outputs/Diagrams/10-Diagrams.ps1
|
function Invoke-RangerDiagramGeneration { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [string]$PackageRoot, [string[]]$Formats = @('svg'), [string]$Mode ) $diagramsRoot = Join-Path -Path $PackageRoot -ChildPath 'diagrams' New-Item -ItemType Directory -Path $diagramsRoot -Force | Out-Null $artifacts = New-Object System.Collections.ArrayList $prefix = Get-RangerArtifactPrefix -Manifest $Manifest foreach ($definition in (Get-RangerDiagramDefinitions)) { if (-not (Test-RangerShouldRenderDiagram -Manifest $Manifest -Definition $definition -Mode $Mode)) { [void]$artifacts.Add((New-RangerArtifactRecord -Type 'diagram' -RelativePath (Join-Path 'diagrams' ((Get-RangerSafeName -Value $definition.Name) + '.drawio')) -Status skipped -Audience ($definition.Audience -join ',') -Reason 'Selection rules did not include this diagram for the current manifest.')) continue } if (-not (Test-RangerDiagramHasRequiredData -Manifest $Manifest -Definition $definition)) { [void]$artifacts.Add((New-RangerArtifactRecord -Type 'diagram' -RelativePath (Join-Path 'diagrams' ((Get-RangerSafeName -Value $definition.Name) + '.drawio')) -Status skipped -Audience ($definition.Audience -join ',') -Reason 'Required manifest domains were missing.')) continue } $model = Get-RangerDiagramModel -Manifest $Manifest -Definition $definition $baseName = "{0}-{1}" -f $prefix, (Get-RangerSafeName -Value $definition.Name) $drawIoPath = Join-Path -Path $diagramsRoot -ChildPath ($baseName + '.drawio') (ConvertTo-RangerDrawIoXml -Manifest $Manifest -Definition $definition -Model $model) | Set-Content -Path $drawIoPath -Encoding UTF8 [void]$artifacts.Add((New-RangerArtifactRecord -Type 'diagram-drawio' -RelativePath ([System.IO.Path]::GetRelativePath($PackageRoot, $drawIoPath)) -Status generated -Audience ($definition.Audience -join ','))) if ('svg' -in $Formats) { $svgPath = Join-Path -Path $diagramsRoot -ChildPath ($baseName + '.svg') (ConvertTo-RangerSvgDiagram -Manifest $Manifest -Definition $definition -Model $model) | Set-Content -Path $svgPath -Encoding UTF8 [void]$artifacts.Add((New-RangerArtifactRecord -Type 'diagram-svg' -RelativePath ([System.IO.Path]::GetRelativePath($PackageRoot, $svgPath)) -Status generated -Audience ($definition.Audience -join ','))) } } return @($artifacts) } function Test-RangerShouldRenderDiagram { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [object]$Definition, [string]$Mode ) if ($Definition.Tier -eq 'baseline') { return $true } if ($Mode -eq 'as-built') { return $true } $name = $Definition.Name switch ($name) { 'topology-variant-map' { return $Manifest.topology.deploymentType -in @('rack-aware', 'multi-rack', 'switchless') } 'identity-secret-flow' { return $Manifest.topology.identityMode -eq 'local-key-vault' } 'monitoring-telemetry-flow' { return Test-RangerDomainPopulated -Value $Manifest.domains.monitoring } 'connectivity-dependency-map' { return Test-RangerDomainPopulated -Value $Manifest.domains.networking.proxy } 'identity-access-surface' { return Test-RangerDomainPopulated -Value $Manifest.domains.identitySecurity } 'monitoring-health-heatmap' { return @($Manifest.findings | Where-Object { $_.severity -in @('critical', 'warning') }).Count -gt 0 } 'oem-firmware-posture' { return Test-RangerDomainPopulated -Value $Manifest.domains.hardware } 'backup-recovery-map' { return @($Manifest.domains.azureIntegration.backup).Count -gt 0 } 'management-plane-tooling' { return Test-RangerDomainPopulated -Value $Manifest.domains.managementTools } 'workload-family-placement' { return @($Manifest.domains.virtualMachines.workloadFamilies).Count -gt 0 } 'fabric-map' { return $Manifest.topology.deploymentType -eq 'rack-aware' } 'disconnected-control-plane' { return $Manifest.topology.controlPlaneMode -eq 'disconnected' } default { return $false } } } function Test-RangerDiagramHasRequiredData { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [object]$Definition ) foreach ($required in @($Definition.Required)) { if (-not (Test-RangerDomainPopulated -Value $Manifest.domains[$required])) { return $false } } return $true } function Get-RangerDiagramModel { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [object]$Definition ) $nodes = New-Object System.Collections.ArrayList $edges = New-Object System.Collections.ArrayList $clusterName = if (-not [string]::IsNullOrWhiteSpace($Manifest.target.clusterName)) { $Manifest.target.clusterName } else { $Manifest.target.environmentLabel } [void]$nodes.Add([ordered]@{ id = 'cluster'; label = $clusterName; kind = 'cluster'; detail = $Manifest.topology.deploymentType; group = 'platform' }) switch ($Definition.Name) { 'physical-architecture' { foreach ($node in @($Manifest.domains.clusterNode.nodes)) { $nodeId = 'node-' + (Get-RangerSafeName -Value $node.name) [void]$nodes.Add([ordered]@{ id = $nodeId; label = $node.name; kind = 'node'; detail = ('{0} | {1}' -f $node.state, $node.model); group = 'platform' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $nodeId; label = $node.state }) } foreach ($endpoint in @($Manifest.domains.oemIntegration.endpoints)) { $endpointId = 'bmc-' + (Get-RangerSafeName -Value $endpoint.host) [void]$nodes.Add([ordered]@{ id = $endpointId; label = $endpoint.host; kind = 'bmc'; detail = $endpoint.node; group = 'management' }) [void]$edges.Add([ordered]@{ source = $endpointId; target = ('node-' + (Get-RangerSafeName -Value $endpoint.node)); label = 'manages' }) } } 'logical-network-topology' { foreach ($network in @($Manifest.domains.clusterNode.networks)) { $networkId = 'network-' + (Get-RangerSafeName -Value $network.name) [void]$nodes.Add([ordered]@{ id = $networkId; label = $network.name; kind = 'network'; detail = ('role {0}' -f $network.role); group = 'network' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $networkId; label = $network.role }) } foreach ($adapter in @($Manifest.domains.networking.adapters | Select-Object -First 10)) { $adapterId = 'adapter-' + (Get-RangerSafeName -Value $adapter.name) [void]$nodes.Add([ordered]@{ id = $adapterId; label = $adapter.name; kind = 'adapter'; detail = ('{0} | {1}' -f $adapter.status, $adapter.linkSpeed); group = 'network' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $adapterId; label = 'adapter' }) } } 'storage-architecture' { foreach ($pool in @($Manifest.domains.storage.pools)) { $poolId = 'pool-' + (Get-RangerSafeName -Value $pool.friendlyName) [void]$nodes.Add([ordered]@{ id = $poolId; label = $pool.friendlyName; kind = 'storage'; detail = ('{0} | {1} GiB' -f $pool.healthStatus, [math]::Round(($pool.size / 1GB), 2)); group = 'storage' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $poolId; label = $pool.healthStatus }) } foreach ($csv in @($Manifest.domains.storage.csvs)) { $csvId = 'csv-' + (Get-RangerSafeName -Value $csv.name) [void]$nodes.Add([ordered]@{ id = $csvId; label = $csv.name; kind = 'volume'; detail = $csv.state; group = 'storage' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $csvId; label = $csv.state }) } foreach ($disk in @($Manifest.domains.storage.physicalDisks | Select-Object -First 8)) { $diskId = 'disk-' + (Get-RangerSafeName -Value $disk.friendlyName) [void]$nodes.Add([ordered]@{ id = $diskId; label = $disk.friendlyName; kind = 'disk'; detail = ('{0} | {1}' -f $disk.mediaType, $disk.healthStatus); group = 'storage' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $diskId; label = 'physical disk' }) } } 'vm-placement-map' { foreach ($vm in @($Manifest.domains.virtualMachines.inventory)) { $vmId = 'vm-' + (Get-RangerSafeName -Value $vm.name) [void]$nodes.Add([ordered]@{ id = $vmId; label = $vm.name; kind = 'vm'; detail = ('{0} | {1} MB' -f $vm.state, $vm.memoryAssignedMb); group = 'workload' }) [void]$edges.Add([ordered]@{ source = ('node-' + (Get-RangerSafeName -Value $vm.hostNode)); target = $vmId; label = $vm.state }) } } 'azure-arc-integration' { foreach ($resource in @($Manifest.domains.azureIntegration.resources | Select-Object -First 10)) { $resourceId = 'az-' + (Get-RangerSafeName -Value $resource.name) [void]$nodes.Add([ordered]@{ id = $resourceId; label = $resource.name; kind = 'azure'; detail = ('{0} | {1}' -f $resource.resourceType, $resource.location); group = 'azure' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $resourceId; label = $resource.resourceType }) } } 'workload-services-map' { foreach ($family in @($Manifest.domains.virtualMachines.workloadFamilies)) { $familyId = 'workload-' + (Get-RangerSafeName -Value $family.name) [void]$nodes.Add([ordered]@{ id = $familyId; label = $family.name; kind = 'workload'; detail = ('count {0}' -f $family.count); group = 'workload' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $familyId; label = $family.count }) } foreach ($service in @($Manifest.domains.azureIntegration.services | Select-Object -First 8)) { $serviceId = 'service-' + (Get-RangerSafeName -Value $service.name) [void]$nodes.Add([ordered]@{ id = $serviceId; label = $service.name; kind = 'service'; detail = ('count {0}' -f $service.count); group = 'azure' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $serviceId; label = $service.category }) } } 'topology-variant-map' { foreach ($marker in @($Manifest.topology.variantMarkers | Select-Object -Unique)) { $markerId = 'variant-' + (Get-RangerSafeName -Value $marker) [void]$nodes.Add([ordered]@{ id = $markerId; label = $marker; kind = 'variant'; detail = 'applies'; group = 'platform' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $markerId; label = 'variant' }) } } 'identity-secret-flow' { foreach ($node in @($Manifest.domains.identitySecurity.nodes)) { $identityId = 'identity-' + (Get-RangerSafeName -Value $node.node) [void]$nodes.Add([ordered]@{ id = $identityId; label = $node.node; kind = 'identity'; detail = ('domain joined: {0}' -f $node.partOfDomain); group = 'identity' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $identityId; label = 'trust' }) } foreach ($policy in @($Manifest.domains.azureIntegration.policy | Select-Object -First 5)) { $policyId = 'policy-' + (Get-RangerSafeName -Value $policy.name) [void]$nodes.Add([ordered]@{ id = $policyId; label = $policy.name; kind = 'policy'; detail = $policy.enforcementMode; group = 'azure' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $policyId; label = 'policy' }) } } 'monitoring-telemetry-flow' { $monitoringResources = @(@($Manifest.domains.monitoring.ama) + @($Manifest.domains.monitoring.dcr) + @($Manifest.domains.monitoring.alerts) | Select-Object -First 12) foreach ($resource in $monitoringResources) { $resourceId = 'monitor-' + (Get-RangerSafeName -Value $resource.name) [void]$nodes.Add([ordered]@{ id = $resourceId; label = $resource.name; kind = 'monitor'; detail = $resource.resourceType; group = 'management' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $resourceId; label = 'telemetry' }) } } 'connectivity-dependency-map' { foreach ($proxy in @($Manifest.domains.networking.proxy | Select-Object -First 10)) { $proxyId = 'proxy-' + (Get-RangerSafeName -Value $proxy.node) [void]$nodes.Add([ordered]@{ id = $proxyId; label = $proxy.node; kind = 'connectivity'; detail = $proxy.value; group = 'network' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $proxyId; label = 'proxy path' }) } foreach ($profile in @($Manifest.domains.networking.firewall | Select-Object -First 10)) { $profileId = 'fw-' + (Get-RangerSafeName -Value $profile.node) [void]$nodes.Add([ordered]@{ id = $profileId; label = $profile.node; kind = 'firewall'; detail = ('profiles {0}' -f @($profile.profiles).Count); group = 'network' }) [void]$edges.Add([ordered]@{ source = $profileId; target = 'cluster'; label = 'host firewall' }) } } 'identity-access-surface' { foreach ($adminSet in @($Manifest.domains.identitySecurity.localAdmins | Select-Object -First 10)) { $adminId = 'admins-' + (Get-RangerSafeName -Value $adminSet.node) [void]$nodes.Add([ordered]@{ id = $adminId; label = $adminSet.node; kind = 'identity'; detail = ('admin members {0}' -f @($adminSet.members).Count); group = 'identity' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $adminId; label = 'local admin surface' }) } } 'monitoring-health-heatmap' { foreach ($node in @($Manifest.domains.clusterNode.nodes)) { $findingCount = @($Manifest.findings | Where-Object { @($_.affectedComponents) -contains $node.name }).Count [void]$nodes.Add([ordered]@{ id = 'heat-' + (Get-RangerSafeName -Value $node.name); label = $node.name; kind = 'heat'; detail = ('findings {0}' -f $findingCount); group = 'management'; severity = if ($findingCount -gt 1) { 'warning' } else { 'good' } }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = ('heat-' + (Get-RangerSafeName -Value $node.name)); label = 'health' }) } } 'oem-firmware-posture' { foreach ($node in @($Manifest.domains.hardware.nodes)) { $hardwareId = 'hw-' + (Get-RangerSafeName -Value $node.node) [void]$nodes.Add([ordered]@{ id = $hardwareId; label = $node.node; kind = 'hardware'; detail = ('bios {0}' -f $node.biosVersion); group = 'hardware' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $hardwareId; label = 'hardware' }) } foreach ($mgmt in @($Manifest.domains.oemIntegration.managementPosture | Select-Object -First 10)) { $mgmtId = 'oem-' + (Get-RangerSafeName -Value $mgmt.node) [void]$nodes.Add([ordered]@{ id = $mgmtId; label = $mgmt.node; kind = 'bmc'; detail = $mgmt.managerFirmwareVersion; group = 'hardware' }) [void]$edges.Add([ordered]@{ source = $mgmtId; target = ('hw-' + (Get-RangerSafeName -Value $mgmt.node)); label = 'firmware posture' }) } } 'backup-recovery-map' { $recoveryResources = @(@($Manifest.domains.azureIntegration.backup) + @($Manifest.domains.azureIntegration.update) | Select-Object -First 10) foreach ($resource in $recoveryResources) { $resourceId = 'recover-' + (Get-RangerSafeName -Value $resource.name) [void]$nodes.Add([ordered]@{ id = $resourceId; label = $resource.name; kind = 'recovery'; detail = $resource.resourceType; group = 'azure' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $resourceId; label = 'continuity' }) } } 'management-plane-tooling' { foreach ($tool in @($Manifest.domains.managementTools.tools | Select-Object -First 10)) { $toolId = 'tool-' + (Get-RangerSafeName -Value $tool.name) [void]$nodes.Add([ordered]@{ id = $toolId; label = $tool.name; kind = 'management'; detail = $tool.status; group = 'management' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $toolId; label = 'management' }) } } 'workload-family-placement' { foreach ($family in @($Manifest.domains.virtualMachines.workloadFamilies)) { $familyId = 'family-' + (Get-RangerSafeName -Value $family.name) [void]$nodes.Add([ordered]@{ id = $familyId; label = $family.name; kind = 'workload'; detail = ('family count {0}' -f $family.count); group = 'workload' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $familyId; label = 'family' }) } foreach ($placement in @($Manifest.domains.virtualMachines.placement | Select-Object -First 10)) { $placementNode = 'place-' + (Get-RangerSafeName -Value $placement.vm) [void]$nodes.Add([ordered]@{ id = $placementNode; label = $placement.vm; kind = 'vm'; detail = $placement.hostNode; group = 'workload' }) [void]$edges.Add([ordered]@{ source = ('family-' + (Get-RangerSafeName -Value (($Manifest.domains.virtualMachines.workloadFamilies | Select-Object -First 1).name))); target = $placementNode; label = $placement.state }) } } 'fabric-map' { foreach ($faultDomain in @($Manifest.domains.clusterNode.faultDomains | Select-Object -First 10)) { $faultId = 'fd-' + (Get-RangerSafeName -Value $faultDomain.name) [void]$nodes.Add([ordered]@{ id = $faultId; label = $faultDomain.name; kind = 'fabric'; detail = $faultDomain.location; group = 'network' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $faultId; label = $faultDomain.faultDomainType }) } } 'disconnected-control-plane' { [void]$nodes.Add([ordered]@{ id = 'ops'; label = 'Disconnected operations'; kind = 'control'; detail = 'local control plane'; group = 'management' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = 'ops'; label = 'operates' }) foreach ($node in @($Manifest.domains.identitySecurity.nodes | Select-Object -First 10)) { $nodeId = 'local-' + (Get-RangerSafeName -Value $node.node) [void]$nodes.Add([ordered]@{ id = $nodeId; label = $node.node; kind = 'identity'; detail = ('domain joined: {0}' -f $node.partOfDomain); group = 'identity' }) [void]$edges.Add([ordered]@{ source = 'ops'; target = $nodeId; label = 'trust boundary' }) } } default { foreach ($required in @($Definition.Required)) { $requiredId = 'domain-' + (Get-RangerSafeName -Value $required) [void]$nodes.Add([ordered]@{ id = $requiredId; label = $required; kind = 'domain'; detail = $Definition.Title; group = 'platform' }) [void]$edges.Add([ordered]@{ source = 'cluster'; target = $requiredId; label = $Definition.Title }) } } } return [ordered]@{ nodes = @($nodes) edges = @($edges) } } function ConvertTo-RangerDrawIoXml { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [object]$Definition, [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Model ) $cells = New-Object System.Collections.Generic.List[string] $cells.Add('<mxCell id="0" />') $cells.Add('<mxCell id="1" parent="0" />') $groupColumns = [ordered]@{ platform = 40; storage = 320; network = 600; workload = 880; azure = 1160; management = 1440; identity = 1720; hardware = 2000 } $groupOffsets = @{} foreach ($node in @($Model.nodes)) { $labelText = if ($node.detail) { '{0}
{1}' -f $node.label, $node.detail } else { [string]$node.label } $label = [System.Security.SecurityElement]::Escape($labelText) $style = switch ($node.kind) { 'cluster' { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dbeafe;strokeColor=#2563eb;' } 'node' { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;' } 'azure' { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#cffafe;strokeColor=#0f766e;' } 'storage' { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#e2e8f0;strokeColor=#475569;' } 'network' { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fde68a;strokeColor=#ca8a04;' } 'vm' { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fef3c7;strokeColor=#d97706;' } 'management' { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#ede9fe;strokeColor=#7c3aed;' } 'identity' { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fee2e2;strokeColor=#dc2626;' } default { 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8fafc;strokeColor=#64748b;' } } $group = if ($node.group) { [string]$node.group } else { 'platform' } if (-not $groupColumns.Contains($group)) { $group = 'platform' } if (-not $groupOffsets.ContainsKey($group)) { $groupOffsets[$group] = 40 } $x = $groupColumns[$group] $y = $groupOffsets[$group] $cells.Add(('<mxCell id="{0}" value="{1}" style="{2}" vertex="1" parent="1"><mxGeometry x="{3}" y="{4}" width="180" height="60" as="geometry" /></mxCell>' -f $node.id, $label, $style, $x, $y)) $groupOffsets[$group] = $y + 90 } $edgeIndex = 0 foreach ($edge in @($Model.edges)) { $edgeIndex++ $cells.Add(('<mxCell id="edge-{0}" value="{1}" style="endArrow=block;html=1;rounded=0;" edge="1" parent="1" source="{2}" target="{3}"><mxGeometry relative="1" as="geometry" /></mxCell>' -f $edgeIndex, [System.Security.SecurityElement]::Escape([string]$edge.label), $edge.source, $edge.target)) } @" <mxfile host="app.diagrams.net" modified="$($Manifest.run.endTimeUtc)" agent="AzureLocalRanger" version="$($Manifest.run.toolVersion)"> <diagram name="$([System.Security.SecurityElement]::Escape($Definition.Title))"> <mxGraphModel dx="1330" dy="844" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="900" math="0" shadow="0"> <root> $($cells -join [Environment]::NewLine) </root> </mxGraphModel> </diagram> </mxfile> "@ } function ConvertTo-RangerSvgDiagram { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [object]$Definition, [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Model ) $boxes = New-Object System.Collections.Generic.List[string] $lines = New-Object System.Collections.Generic.List[string] $labels = New-Object System.Collections.Generic.List[string] $groupColumns = [ordered]@{ platform = 20; storage = 280; network = 540; workload = 800; azure = 1060; management = 1320; identity = 1580; hardware = 1840 } $groupOffsets = @{} $positions = @{} foreach ($node in @($Model.nodes)) { if ([string]::IsNullOrWhiteSpace($node.id)) { continue } $group = if ($node.group) { [string]$node.group } else { 'platform' } if (-not $groupColumns.Contains($group)) { $group = 'platform' } if (-not $groupOffsets.ContainsKey($group)) { $groupOffsets[$group] = 70 } $x = $groupColumns[$group] $y = $groupOffsets[$group] $positions[$node.id] = [ordered]@{ x = $x; y = $y } $fill = switch ($node.kind) { 'cluster' { '#dbeafe' } 'node' { '#dcfce7' } 'azure' { '#cffafe' } 'vm' { '#fef3c7' } 'storage' { '#e2e8f0' } 'network' { '#fde68a' } 'management' { '#ede9fe' } 'identity' { '#fee2e2' } 'heat' { if ($node.severity -eq 'warning') { '#fed7aa' } else { '#dcfce7' } } default { '#f8fafc' } } $boxes.Add(('<rect x="{0}" y="{1}" width="220" height="56" rx="10" fill="{2}" stroke="#334155" />' -f $x, $y, $fill)) $labels.Add(('<text x="{0}" y="{1}" font-family="Segoe UI, Arial" font-size="14" fill="#0f172a">{2}</text>' -f ($x + 12), ($y + 25), [System.Security.SecurityElement]::Escape([string]$node.label))) if ($node.detail) { $labels.Add(('<text x="{0}" y="{1}" font-family="Segoe UI, Arial" font-size="11" fill="#475569">{2}</text>' -f ($x + 12), ($y + 42), [System.Security.SecurityElement]::Escape([string]$node.detail))) } $groupOffsets[$group] = $y + 86 } foreach ($edge in @($Model.edges)) { if ([string]::IsNullOrWhiteSpace($edge.source) -or [string]::IsNullOrWhiteSpace($edge.target)) { continue } if (-not $positions.Contains($edge.source) -or -not $positions.Contains($edge.target)) { continue } $source = $positions[$edge.source] $target = $positions[$edge.target] $x1 = $source.x + 220 $y1 = $source.y + 28 $x2 = $target.x $y2 = $target.y + 28 $lines.Add(('<line x1="{0}" y1="{1}" x2="{2}" y2="{3}" stroke="#64748b" stroke-width="2" />' -f $x1, $y1, $x2, $y2)) if ($edge.label) { $labels.Add(('<text x="{0}" y="{1}" font-family="Segoe UI, Arial" font-size="10" fill="#334155">{2}</text>' -f ([math]::Round((($x1 + $x2) / 2), 0)), ([math]::Round((($y1 + $y2) / 2) - 4, 0)), [System.Security.SecurityElement]::Escape([string]$edge.label))) } } @" <svg xmlns="http://www.w3.org/2000/svg" width="2200" height="900" viewBox="0 0 2200 900"> <rect width="2200" height="900" fill="#ffffff" /> <text x="20" y="36" font-family="Segoe UI, Arial" font-size="24" fill="#0f172a">$([System.Security.SecurityElement]::Escape($Definition.Title))</text> <text x="20" y="56" font-family="Segoe UI, Arial" font-size="12" fill="#475569">$([System.Security.SecurityElement]::Escape($Manifest.target.environmentLabel)) | Generated $([System.Security.SecurityElement]::Escape($Manifest.run.endTimeUtc)) | Ranger $([System.Security.SecurityElement]::Escape($Manifest.run.toolVersion))</text> $($lines -join [Environment]::NewLine) $($boxes -join [Environment]::NewLine) $($labels -join [Environment]::NewLine) </svg> "@ } |