├── .github └── workflows │ ├── build.yaml │ └── docker.yaml ├── .gitignore ├── .idea └── .idea.AzureDiagrams │ └── .idea │ ├── .name │ ├── indexLayout.xml │ ├── projectSettingsUpdater.xml │ └── vcs.xml ├── AzureDiagramGenerator ├── AzureDiagramGenerator.csproj ├── DrawIo │ ├── AzureResourceDrawer.cs │ ├── AzureResourceNodeBuilder.cs │ ├── CustomUserData.cs │ ├── DiagramAdjustors │ │ ├── CondensedDiagramAdjustor.cs │ │ ├── IDiagramAdjustor.cs │ │ └── VisiblePlanesDiagramAdjustor.cs │ ├── IDiagramResourceBuilder.cs │ ├── IgnoreNodeBuilder.cs │ ├── Pattern.cs │ ├── TextAlignment.cs │ └── VNetDiagramResourceBuilder.cs ├── DrawIoDiagramGenerator.cs ├── Program.cs └── readme.md ├── AzureDiagrams.sln ├── AzureDiagrams ├── AzureDiagrams.csproj ├── AzureModelRetriever.cs └── Resources │ ├── ACR.cs │ ├── ADF.cs │ ├── AKS.cs │ ├── APIm.cs │ ├── AppGateway.cs │ ├── AppInsights.cs │ ├── AppServiceApp.cs │ ├── AppServiceEnvironment.cs │ ├── AppServicePlan.cs │ ├── AzureActiveDirectory.cs │ ├── AzureResource.cs │ ├── Bastion.cs │ ├── BigDataPool.cs │ ├── Bot.cs │ ├── CognitiveSearch.cs │ ├── CognitiveServices.cs │ ├── CommonDiagnostics.cs │ ├── CommonResources.cs │ ├── ContainerApp.cs │ ├── ContainerAppEnvironment.cs │ ├── ContainerInstance.cs │ ├── CoreServices.cs │ ├── CosmosDb.cs │ ├── Disk.cs │ ├── DnsZoneVirtualNetworkLink.cs │ ├── EnumerableEx.cs │ ├── EventGridDomain.cs │ ├── EventGridTopic.cs │ ├── EventHub.cs │ ├── Firewall.cs │ ├── FlowEmphasis.cs │ ├── FlowEx.cs │ ├── FlowEx2.cs │ ├── IAssociateWithNic.cs │ ├── ICanBeAccessedViaAHostName.cs │ ├── ICanEgressViaAVnet.cs │ ├── ICanExposePublicIPAddresses.cs │ ├── ICanInjectIntoASubnet.cs │ ├── ICanWriteToLogAnalyticsWorkspaces.cs │ ├── Identity.cs │ ├── IotHub.cs │ ├── IpConfigurations.cs │ ├── KeyVault.cs │ ├── LoadBalancer.cs │ ├── LogAnalyticsWorkspace.cs │ ├── LogicApp.cs │ ├── LogicAppConnector.cs │ ├── MachineLearningWorkspace.cs │ ├── ManagedSqlDatabase.cs │ ├── ManagedSqlServer.cs │ ├── NSG.cs │ ├── NetworkProfile.cs │ ├── Nic.cs │ ├── P2S.cs │ ├── PIP.cs │ ├── PrivateDnsZone.cs │ ├── PrivateEndpoint.cs │ ├── PublicIpAddresses.cs │ ├── Region.cs │ ├── RelationshipHelper.cs │ ├── ResourceLink.cs │ ├── Retrievers │ ├── ArmClient.cs │ ├── AzureHttpEx.cs │ ├── BasicAzureResourceInfo.cs │ ├── Custom │ │ ├── ApimServiceResourceRetriever.cs │ │ ├── AppResourceRetriever.cs │ │ ├── AzureDataFactoryRetriever.cs │ │ ├── EventGridDomainRetriever.cs │ │ ├── EventGridTopicRetriever.cs │ │ ├── SynapseRetriever.cs │ │ └── VHubRetriever.cs │ ├── DiagramException.cs │ ├── Extensions │ │ ├── DiagnosticsExtensions.cs │ │ ├── IResourceExtension.cs │ │ ├── ManagedIdentityExtension.cs │ │ └── PrivateEndpointExtensions.cs │ ├── IRetrieveResource.cs │ └── ResourceRetriever.cs │ ├── S2S.cs │ ├── ServiceBus.cs │ ├── StaticSite.cs │ ├── StorageAccount.cs │ ├── StringEx.cs │ ├── Synapse.cs │ ├── UDR.cs │ ├── UserAssignedIdentity.cs │ ├── UserAssignedManagedIdentity.cs │ ├── VHub.cs │ ├── VM.cs │ ├── VMExtension.cs │ ├── VMSS.cs │ ├── VNet.cs │ ├── VNetIntegration.cs │ ├── VWan.cs │ └── VirtualHubVirtualNetworkConnection.cs ├── AzureDiagramsTests ├── AzResourceHelper.cs ├── AzureDiagramsTests.csproj ├── Basic │ ├── BasicResources.SingleStorageAccount.approved.txt │ ├── BasicResources.SingleStorageAccountSimpleConstructor.approved.txt │ ├── BasicResources.VNetWithAttachedStoragetAccountInSubNet.approved.txt │ ├── BasicResources.VNetWithSubNet.approved.txt │ ├── BasicResources.VNetWithSubNetSimpleConstructor.approved.txt │ └── BasicResources.cs ├── TestResourcesObjectMother.cs ├── Usings.cs ├── VirtualWans │ ├── VWanWithVHub.CanDrawDiagram.approved.txt │ └── VWanWithVHub.cs └── WebApps │ ├── WebAppWithPrivateEndpoint.CanDrawCondensedDiagram.approved.txt │ ├── WebAppWithPrivateEndpoint.CanDrawDiagram.approved.txt │ ├── WebAppWithPrivateEndpoint.cs │ ├── WebAppWithSlots.CanDrawDiagram.approved.txt │ └── WebAppWithSlots.cs ├── Dockerfile ├── GitVersion.yml ├── LICENSE ├── assets ├── grfsq2-platform-test-rg.drawio.png └── more-complex.drawio.png ├── entrypoint.sh └── readme.md /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: BuildAndPackage 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build-and-package-app: 13 | runs-on: ubuntu-latest 14 | 15 | # Steps represent a sequence of tasks that will be executed as part of the job 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install GitVersion 23 | uses: gittools/actions/gitversion/setup@v0.9.7 24 | with: 25 | versionSpec: '5.x' 26 | 27 | - name: Determine Version 28 | id: gitversion 29 | uses: gittools/actions/gitversion/execute@v0.9.15 30 | 31 | - name: Test 32 | run: dotnet test AzureDiagramsTests/AzureDiagramsTests.csproj 33 | 34 | - name: Pack Nuget 35 | run: dotnet pack AzureDiagramGenerator/AzureDiagramGenerator.csproj -p:Version=${{ steps.gitversion.outputs.NuGetVersionV2 }} --output ./publish/ 36 | 37 | - name: Publish to Nuget ${{ steps.gitversion.outputs.NuGetVersionV2 }} 38 | run: dotnet nuget push ./publish/AzureDiagramGenerator.${{ steps.gitversion.outputs.NuGetVersionV2 }}.nupkg --api-key ${{ secrets.NUGET_PUBLISH_KEY }} --source https://api.nuget.org/v3/index.json 39 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: PublishDocker 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-and-publish-docker-container: 8 | runs-on: ubuntu-latest 9 | if: github.ref == 'refs/heads/main' 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Install GitVersion 18 | uses: gittools/actions/gitversion/setup@v0.9.7 19 | with: 20 | versionSpec: '5.x' 21 | 22 | - name: Determine Version 23 | id: gitversion 24 | uses: gittools/actions/gitversion/execute@v0.9.15 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v1 28 | 29 | - name: Login to GitHub Container Registry 30 | uses: docker/login-action@v1 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.repository_owner }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Build and push ${{ needs.build-and-package-app.outputs.version }} 37 | uses: docker/build-push-action@v2 38 | with: 39 | context: . 40 | push: true 41 | build-args: NUGET_VERSION=${{ steps.gitversion.outputs.NuGetVersionV2 }} 42 | tags: | 43 | ghcr.io/graemefoster/azurediagrams:latest 44 | ghcr.io/graemefoster/azurediagrams:${{ steps.gitversion.outputs.NuGetVersionV2 }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Common IntelliJ Platform excludes 2 | 3 | # User specific 4 | **/.idea/**/workspace.xml 5 | **/.idea/**/tasks.xml 6 | **/.idea/shelf/* 7 | **/.idea/dictionaries 8 | **/.idea/httpRequests/ 9 | 10 | # Sensitive or high-churn files 11 | **/.idea/**/dataSources/ 12 | **/.idea/**/dataSources.ids 13 | **/.idea/**/dataSources.xml 14 | **/.idea/**/dataSources.local.xml 15 | **/.idea/**/sqlDataSources.xml 16 | **/.idea/**/dynamic.xml 17 | 18 | # Rider 19 | # Rider auto-generates .iml files, and contentModel.xml 20 | **/.idea/**/*.iml 21 | **/.idea/**/contentModel.xml 22 | **/.idea/**/modules.xml 23 | 24 | *.suo 25 | *.user 26 | .vs/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | [Dd]ebug/ 30 | [Rr]elease/ 31 | _UpgradeReport_Files/ 32 | [Pp]ackages/ 33 | 34 | Thumbs.db 35 | Desktop.ini 36 | .DS_Store 37 | 38 | publish/ 39 | 40 | *.received.txt 41 | 42 | -------------------------------------------------------------------------------- /.idea/.idea.AzureDiagrams/.idea/.name: -------------------------------------------------------------------------------- 1 | AzureDiagrams -------------------------------------------------------------------------------- /.idea/.idea.AzureDiagrams/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.AzureDiagrams/.idea/projectSettingsUpdater.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/.idea.AzureDiagrams/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AzureDiagramGenerator/AzureDiagramGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | true 9 | true 10 | README.md 11 | default 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/CustomUserData.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.Msagl.Core.Layout; 3 | 4 | namespace AzureDiagramGenerator.DrawIo; 5 | 6 | [DebuggerDisplay("{Name}-{Id}")] 7 | public class CustomUserData 8 | { 9 | public CustomUserData(Func drawNode, string name, string id) 10 | { 11 | DrawNode = drawNode; 12 | Name = name; 13 | Id = id; 14 | } 15 | 16 | public CustomUserData(Func drawEdge, string name, string id) 17 | { 18 | DrawEdge = drawEdge; 19 | Name = name; 20 | Id = id; 21 | } 22 | 23 | public Func? DrawNode { get; init; } 24 | public Func? DrawEdge { get; init; } 25 | public string Name { get; init; } 26 | public string Id { get; init; } 27 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/DiagramAdjustors/CondensedDiagramAdjustor.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams.Resources; 2 | 3 | namespace AzureDiagramGenerator.DrawIo.DiagramAdjustors; 4 | 5 | public class CondensedDiagramAdjustor : IDiagramAdjustor 6 | { 7 | private readonly IDiagramAdjustor _inner; 8 | private readonly Dictionary _replacements = new(); 9 | private readonly List _removals = new(); 10 | private readonly List _ignoreLinks = new(); 11 | 12 | public CondensedDiagramAdjustor(IDiagramAdjustor inner, AzureResource[] allResources) 13 | { 14 | _inner = inner; 15 | CollapsePrivateEndpoints(allResources); 16 | CollapseVNetIntegrations(allResources); 17 | CollapseVirtualMachines(allResources); 18 | _removals.AddRange(_replacements.Keys); 19 | } 20 | 21 | private void CollapseVirtualMachines(AzureResource[] allResources) 22 | { 23 | foreach (var vm in allResources.OfType()) 24 | { 25 | var nics = allResources.OfType() 26 | .Where(x => vm.Nics.Contains(x.Id, StringComparer.InvariantCultureIgnoreCase)); 27 | foreach (var nic in nics) 28 | { 29 | _replacements.Add(nic, vm); 30 | } 31 | var disk = allResources.OfType().SingleOrDefault(x => vm.SystemDiskId.Equals(x.Id, StringComparison.InvariantCultureIgnoreCase)); 32 | if (disk != null) 33 | { 34 | _replacements.Add(disk, vm); 35 | } 36 | } 37 | } 38 | 39 | private void CollapsePrivateEndpoints(AzureResource[] allResources) 40 | { 41 | var replacements = allResources.OfType() 42 | .Where(x => x.ResourceAccessedByMe != null) 43 | .Select(x => (pe: x, res: x.ResourceAccessedByMe!)) 44 | .Select(x => (x.res, x.pe)) 45 | .GroupBy(x => x.res, 46 | e => (pe: e.pe, nic: allResources.OfType().Single(nic => nic.ConnectedPrivateEndpoint == e.pe))) 47 | .ToArray(); 48 | 49 | foreach (var grouping in replacements) 50 | { 51 | var distinctSubnets = grouping.SelectMany(x => x.pe.SubnetIdsIAmInjectedInto).Distinct(); 52 | if (distinctSubnets.Count() == 1) 53 | { 54 | //create mappings: 55 | var currentPe = grouping.First(); 56 | _replacements.Add(grouping.Key, currentPe.pe); 57 | foreach (var secondaryPe in grouping.Skip(1)) 58 | { 59 | _replacements.Add(currentPe.pe, secondaryPe.pe); 60 | currentPe = secondaryPe; 61 | } 62 | 63 | _replacements.Add(currentPe.pe, currentPe.nic); 64 | 65 | //and reverse through the nics 66 | foreach (var secondaryPe in grouping.Reverse().Skip(1)) 67 | { 68 | _replacements.Add(currentPe.nic, secondaryPe.nic); 69 | currentPe = secondaryPe; 70 | } 71 | 72 | //the current Nic is the one that will stay on the diagram. Name it to reflect the resource that is accessed 73 | currentPe.nic.Name = currentPe.pe.ResourceAccessedByMe?.Name ?? currentPe.nic.Name; 74 | 75 | //finally target vnet integration 76 | if (grouping.Key is AppServiceApp { VNetIntegration: { } } app) 77 | { 78 | _replacements.Add(app.VNetIntegration!, currentPe.nic); 79 | } 80 | } 81 | else 82 | { 83 | Console.ForegroundColor = ConsoleColor.Yellow; 84 | Console.WriteLine($"Resource {grouping.Key.Name} is injected into multiple subnets. Unable to correctly condense it to a single subnet. Diagram may look odd."); 85 | Console.ResetColor(); 86 | } 87 | } 88 | } 89 | 90 | private void CollapseVNetIntegrations(AzureResource[] allResources) 91 | { 92 | 93 | var publicAppWithVNetIntegration = allResources.OfType() 94 | .Where(x => x.VNetIntegration != null) 95 | .Where(app => allResources.OfType().All(pe => pe.ResourceAccessedByMe != app)); 96 | 97 | foreach (var app in publicAppWithVNetIntegration) 98 | { 99 | _replacements.Add(app, app.VNetIntegration!); 100 | } 101 | 102 | } 103 | 104 | public string ImageFor(AzureResource resource) 105 | { 106 | if (resource is VNetIntegration vNetIntegration && _replacements.ContainsKey(vNetIntegration.LinkedApp)) 107 | { 108 | return vNetIntegration.LinkedApp.Image; 109 | } 110 | 111 | return resource switch 112 | { 113 | Nic { ConnectedPrivateEndpoint: not null } res => 114 | res.ConnectedPrivateEndpoint!.ResourceAccessedByMe?.Image ?? res.Image, 115 | _ => _inner.ImageFor(resource) 116 | }; 117 | } 118 | 119 | public AzureResourceNodeBuilder? CreateNodeBuilder(AzureResource resource) 120 | { 121 | if (_removals.Contains(resource)) return new IgnoreNodeBuilder(resource); 122 | 123 | //Don't draw the ASP if all of its apps are being routed via private endpoint / vnet-integration subnets 124 | if (resource is AppServicePlan asp && asp.ContainedResources.OfType().All(app => _replacements.ContainsKey(app))) 125 | { 126 | return new IgnoreNodeBuilder(resource); 127 | } 128 | 129 | return _inner.CreateNodeBuilder(resource); 130 | } 131 | 132 | /// 133 | /// look for any new 2-way links. These clutter the diagram and we prefer double arrow-heads. 134 | /// 135 | /// 136 | public void PostProcess(Dictionary all) 137 | { 138 | var realLinks = new HashSet<(AzureResource, AzureResource, Plane)>(); 139 | var originalLinks = new Dictionary<(AzureResource, AzureResource, Plane), ResourceLink>(); 140 | foreach (var resource in all.Keys) 141 | { 142 | var from = ReplacementFor(resource); 143 | foreach (var link in resource.Links) 144 | { 145 | if (_inner.DisplayLink(link)) 146 | { 147 | var to = ReplacementFor(link.To); 148 | if (realLinks.Contains((to, from, link.Plane))) 149 | { 150 | _ignoreLinks.Add(link); 151 | originalLinks[(to, from, link.Plane)].MakeTwoWay(); 152 | } 153 | else 154 | { 155 | var key = (from, to, link.Plane); 156 | realLinks.Add(key); 157 | originalLinks[key] = link; 158 | } 159 | } 160 | } 161 | } 162 | 163 | _inner.PostProcess(all); 164 | } 165 | 166 | public AzureResource ReplacementFor(AzureResource resource) 167 | { 168 | var replacement = resource; 169 | while (_replacements.ContainsKey(replacement)) 170 | { 171 | replacement = _replacements[replacement]; 172 | } 173 | 174 | return replacement; 175 | } 176 | 177 | public bool DisplayLink(ResourceLink link) 178 | { 179 | return !_ignoreLinks.Contains(link) && _inner.DisplayLink(link); 180 | } 181 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/DiagramAdjustors/IDiagramAdjustor.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams.Resources; 2 | 3 | namespace AzureDiagramGenerator.DrawIo.DiagramAdjustors; 4 | 5 | public interface IDiagramAdjustor 6 | { 7 | string ImageFor(AzureResource resource); 8 | AzureResourceNodeBuilder? CreateNodeBuilder(AzureResource resource); 9 | bool DisplayLink(ResourceLink link); 10 | 11 | /// 12 | /// Opportunity to do any post-processing after all nodes have been processed 13 | /// 14 | /// 15 | void PostProcess(Dictionary all); 16 | 17 | /// 18 | /// Either return the original resource, or a replacement if you want to reroute this nodes links via somewhere else. 19 | /// 20 | /// 21 | /// 22 | AzureResource ReplacementFor(AzureResource resource); 23 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/DiagramAdjustors/VisiblePlanesDiagramAdjustor.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams.Resources; 2 | 3 | namespace AzureDiagramGenerator.DrawIo.DiagramAdjustors; 4 | 5 | public class VisiblePlanesDiagramAdjustor : IDiagramAdjustor 6 | { 7 | private readonly Plane _visiblePlanes; 8 | 9 | public VisiblePlanesDiagramAdjustor(Plane visiblePlanes) 10 | { 11 | _visiblePlanes = visiblePlanes; 12 | } 13 | 14 | public string ImageFor(AzureResource resource) 15 | { 16 | return resource.Image; 17 | } 18 | 19 | public AzureResourceNodeBuilder? CreateNodeBuilder(AzureResource resource) 20 | { 21 | return null; 22 | } 23 | 24 | public bool DisplayLink(ResourceLink link) 25 | { 26 | return (link.Plane & _visiblePlanes) != Plane.None; 27 | } 28 | 29 | public void PostProcess(Dictionary all) 30 | { 31 | } 32 | 33 | public AzureResource ReplacementFor(AzureResource resource) 34 | { 35 | return resource; 36 | } 37 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/IDiagramResourceBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Msagl.Core.Layout; 2 | 3 | namespace AzureDiagramGenerator.DrawIo; 4 | 5 | public interface IDiagramResourceBuilder 6 | { 7 | IEnumerable CreateNodes(); 8 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/IgnoreNodeBuilder.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagramGenerator.DrawIo.DiagramAdjustors; 2 | using AzureDiagrams.Resources; 3 | using Microsoft.Msagl.Core.Layout; 4 | 5 | namespace AzureDiagramGenerator.DrawIo; 6 | 7 | internal class IgnoreNodeBuilder : AzureResourceNodeBuilder 8 | { 9 | public IgnoreNodeBuilder(AzureResource resource) : base(resource) 10 | { 11 | } 12 | 13 | protected override IEnumerable<(AzureResource, Node)> CreateNodesInternal( 14 | IDictionary resourceNodeBuilders, IDiagramAdjustor diagramAdjustor) 15 | { 16 | yield break; 17 | } 18 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/Pattern.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagramGenerator.DrawIo; 2 | 3 | public enum Pattern 4 | { 5 | Solid, 6 | Dashed 7 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/TextAlignment.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagramGenerator.DrawIo; 2 | 3 | public enum TextAlignment 4 | { 5 | Top, 6 | Middle, 7 | Bottom 8 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIo/VNetDiagramResourceBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | using AzureDiagramGenerator.DrawIo.DiagramAdjustors; 4 | using AzureDiagrams.Resources; 5 | using Microsoft.Msagl.Core.Layout; 6 | 7 | namespace AzureDiagramGenerator.DrawIo; 8 | 9 | internal class VNetDiagramResourceBuilder : AzureResourceNodeBuilder 10 | { 11 | private readonly VNet _resource; 12 | 13 | public VNetDiagramResourceBuilder(VNet resource) : base(resource) 14 | { 15 | _resource = resource; 16 | } 17 | 18 | protected override IEnumerable<(AzureResource, Node)> CreateNodesInternal( 19 | IDictionary resourceNodeBuilders, 20 | IDiagramAdjustor diagramAdjustor) 21 | { 22 | var vnetNode = 23 | AzureResourceDrawer.CreateContainerRectangleNode("VNet", _resource.Name, _resource.InternalId, "#F8CECC", 24 | TextAlignment.Top, _resource.Image); 25 | 26 | yield return (_resource, vnetNode); 27 | 28 | if (_resource.PrivateDnsZones.Count > 0) 29 | { 30 | var privateDnsZoneCluster = 31 | AzureResourceDrawer.CreateContainerRectangleNode("", "DNS Zones", 32 | _resource.InternalId + ".dnszones", "#E1D5E7", TextAlignment.Bottom); 33 | 34 | var dnsZoneImage = AzureResourceDrawer.CreateSimpleImageNode(_resource.PrivateDnsZones[0].Image, "Private Dns", _resource.Id + "_dns"); 35 | vnetNode.AddChild(privateDnsZoneCluster); 36 | privateDnsZoneCluster.AddChild(dnsZoneImage); 37 | 38 | var displayText = string.Join(" ", _resource.PrivateDnsZones.Select(x => x.Name)); 39 | var id = new Guid(SHA256.HashData(Encoding.UTF8.GetBytes(displayText + _resource.InternalId))[..16]).ToString(); 40 | var zoneText = AzureResourceDrawer.CreateTextNode(displayText, id); 41 | privateDnsZoneCluster.AddChild(zoneText); 42 | 43 | yield return (_resource, dnsZoneImage); 44 | yield return (_resource, zoneText); 45 | } 46 | 47 | foreach (var contained in _resource.ContainedResources) 48 | { 49 | var nodeBuilder = resourceNodeBuilders[contained]; 50 | foreach (var containedNode in CreateOtherResourceNodes(nodeBuilder, resourceNodeBuilders, diagramAdjustor)) 51 | { 52 | if (containedNode.Item2.ClusterParent == null) vnetNode.AddChild(containedNode.Item2); 53 | yield return containedNode; 54 | } 55 | } 56 | 57 | if (_resource.Subnets.Length == 0) 58 | { 59 | var emptyContents = AzureResourceDrawer.CreateSimpleRectangleNode("Subnet", "Empty", 60 | _resource.InternalId + $".subnets.empty", backgroundColour:"#ffffff"); 61 | vnetNode.AddChild(emptyContents); 62 | yield return (_resource, emptyContents); 63 | } 64 | 65 | foreach (var subnet in _resource.Subnets) 66 | { 67 | var images = new List(); 68 | if (subnet.NSGs.Any()) images.Add("img/lib/azure2/networking/Network_Security_Groups.svg"); 69 | if (subnet.UdrId != null) images.Add("img/lib/azure2/networking/Route_Tables.svg"); 70 | 71 | var subnetNode = 72 | AzureResourceDrawer.CreateContainerRectangleNode(subnet.AddressPrefix, subnet.Name, 73 | _resource.InternalId + $".{subnet.Name}", "white", TextAlignment.Top, 74 | images.ToArray()); 75 | 76 | vnetNode.AddChild(subnetNode); 77 | 78 | if (subnet.ContainedResources.Count == 0) 79 | { 80 | var emptyContents = AzureResourceDrawer.CreateSimpleRectangleNode("Subnet", "Empty", 81 | _resource.InternalId + $".{subnet.Name}.empty", backgroundColour:"#ffffff"); 82 | subnetNode.AddChild(emptyContents); 83 | yield return (_resource, emptyContents); 84 | } 85 | else 86 | { 87 | var subnetContainsAnything = false; 88 | foreach (var resource in subnet.ContainedResources) 89 | { 90 | var node = resourceNodeBuilders[resource]; 91 | foreach (var contained in CreateOtherResourceNodes(node, resourceNodeBuilders, diagramAdjustor)) 92 | { 93 | if (contained.Item2.ClusterParent == null) subnetNode.AddChild(contained.Item2); 94 | yield return contained; 95 | subnetContainsAnything = true; 96 | } 97 | } 98 | 99 | if (!subnetContainsAnything) 100 | { 101 | //must have condensed them all away. There won't be anything in the subnet, so show the empty box 102 | var emptyContents = AzureResourceDrawer.CreateSimpleRectangleNode("Subnet", "", 103 | _resource.InternalId + $".{subnet.Name}.empty", backgroundColour:"#ffffff"); 104 | subnetNode.AddChild(emptyContents); 105 | yield return (_resource, emptyContents); 106 | } 107 | } 108 | 109 | yield return (_resource, subnetNode); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/DrawIoDiagramGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using AzureDiagramGenerator.DrawIo; 3 | using AzureDiagramGenerator.DrawIo.DiagramAdjustors; 4 | using AzureDiagrams.Resources; 5 | using Microsoft.Msagl.Core.Layout; 6 | using Microsoft.Msagl.Core.Routing; 7 | using Microsoft.Msagl.Layout.Layered; 8 | using Microsoft.Msagl.Miscellaneous; 9 | 10 | namespace AzureDiagramGenerator; 11 | 12 | public static class DrawIoDiagramGenerator 13 | { 14 | public static Task DrawDiagram( 15 | AzureResource[] resources, 16 | bool condensed, 17 | bool showDiagnosticsFlows, 18 | bool showInferredFlows, 19 | bool showRuntimeFlows, 20 | bool showIdentityFlows 21 | ) 22 | { 23 | var graph = new GeometryGraph(); 24 | 25 | var planes = showDiagnosticsFlows ? Plane.Diagnostics : Plane.None; 26 | planes |= showInferredFlows ? Plane.Inferred : Plane.None; 27 | planes |= showRuntimeFlows ? Plane.Runtime : Plane.None; 28 | planes |= showIdentityFlows ? Plane.Identity : Plane.None; 29 | var adjustor = (IDiagramAdjustor)new VisiblePlanesDiagramAdjustor(planes); 30 | adjustor = condensed ? new CondensedDiagramAdjustor(adjustor, resources) : adjustor; 31 | 32 | var nodeBuilders = resources.ToDictionary(x => x, x => AzureResourceNodeBuilder.CreateNodeBuilder(x, adjustor)); 33 | adjustor.PostProcess(nodeBuilders); 34 | var nodes = nodeBuilders.SelectMany(x => x.Value.CreateNodes(nodeBuilders, adjustor)).ToArray(); 35 | var nodesGroupedByResource = nodes.GroupBy(x => x.Item1, x => x.Item2); 36 | var nodesDictionary = nodesGroupedByResource.ToDictionary(x => x.Key, x => x.ToArray()); 37 | 38 | var edges = nodeBuilders.Values.SelectMany(x => x.CreateEdges(nodesDictionary, adjustor)).ToArray(); 39 | 40 | nodesDictionary.SelectMany(x => x.Value).ForEach(n => 41 | { 42 | if (n is Cluster) 43 | { 44 | if (n.ClusterParent == null) 45 | { 46 | graph.RootCluster.AddChild(n); 47 | } 48 | } 49 | else 50 | { 51 | graph.Nodes.Add(n); 52 | } 53 | }); 54 | edges.ForEach(graph.Edges.Add); 55 | 56 | var sb = new StringBuilder(); 57 | 58 | var routingSettings = new EdgeRoutingSettings 59 | { 60 | Padding = 5, 61 | BendPenalty = 10, 62 | UseObstacleRectangles = false, 63 | EdgeRoutingMode = EdgeRoutingMode.Rectilinear 64 | }; 65 | 66 | var settings = new SugiyamaLayoutSettings 67 | { 68 | PackingAspectRatio = 3, 69 | PackingMethod = PackingMethod.Compact, 70 | LayerSeparation = 25, 71 | EdgeRoutingSettings = routingSettings, 72 | LiftCrossEdges = true, 73 | NodeSeparation = 25, 74 | ClusterMargin = 50, 75 | }; 76 | 77 | LayoutHelpers.CalculateLayout(graph, settings, null); 78 | 79 | var msGraph = @$" 80 | 81 | 82 | 83 | {string.Join(Environment.NewLine, graph.GetFlattenedNodesAndClusters().Select(v => ((CustomUserData)v.UserData).DrawNode!()))} 84 | {string.Join(Environment.NewLine, graph.Edges.Select(v => ((CustomUserData)v.UserData).DrawEdge!(v)))} 85 | {sb} 86 | 87 | "; 88 | 89 | return Task.FromResult(msGraph); 90 | } 91 | } -------------------------------------------------------------------------------- /AzureDiagramGenerator/readme.md: -------------------------------------------------------------------------------- 1 | # AzureDiagrams 2 | 3 | ## Generate a Draw.IO diagram from your Azure Resources 4 | 5 | ## CLI flags 6 | 7 | | Flag | Required | Description | 8 | |:-------------------|:----------|:-----------------------------------------------------------------------------| 9 | | --tenant-id | No | Tenant Id (defaults to current Azure CLI) | 10 | | --subscription | Yes | Subscription Id to run against | 11 | | --resource-group | Yes | Wildcard enabled resource group name (supports multiple) | 12 | | --output | Yes | Folder to output diagram to | 13 | | --condensed | No | True collapses private endpoints into subnets (can simplify large diagrams) | 14 | | --show-runtime | No | True to show runtime flows defined on the control plane | 15 | | --show-inferred | No | True to infer connections between resources by introspecting appSettings | 16 | | --show-identity | No | True to show User Assigned Managed Identity connections | 17 | | --show-diagnostics | No | True to show diagnostics flows | 18 | | --token | No | Optional JWT to avoid using CLI credential | 19 | | --output-file-name | No | Name of generated file. Defaults to resource-group name | 20 | | --output-png | No | Outputs a png file as-well as the drawio file (requires draw.io to be installed) | 21 | 22 | # Github Actions 23 | 24 | We have two different actions. The first runs as a Docker action, and produces a jpeg output. The second doesn't use docker, and produces a .drawio file. 25 | 26 | - [graemefoster/azurediagramsgithubactionsdocker@v0.1.2](https://github.com/marketplace/actions/azurediagramsgithubactionsdocker) 27 | - [graemefoster/azurediagramsgithubactions@v0.1.1](https://github.com/marketplace/actions/azurediagramsgithubactions) 28 | 29 | ## How does it work? 30 | AzureDiagrams queries the Azure Resource Management APIs to introspect resource-groups. It then uses a set of strategies to enrich the raw data, building a model that can be projected into other formats. 31 | 32 | It's not 100% guaranteed to be correct but it should give a good first pass at fairly complex architectures/ 33 | 34 | To layout the components I use the amazing [AutomaticGraphLayout](https://github.com/microsoft/automatic-graph-layout) library. 35 | 36 | -------------------------------------------------------------------------------- /AzureDiagrams.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDiagrams", "AzureDiagrams\AzureDiagrams.csproj", "{14A836C1-11F2-49F4-846C-E50134CD86AA}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{0E6366E5-D9A9-40CE-A7E1-99AF5CD267CB}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{7D9F0244-632E-4566-B45F-5DE92AB6C31D}" 8 | ProjectSection(SolutionItems) = preProject 9 | .github\workflows\build.yaml = .github\workflows\build.yaml 10 | .github\workflows\docker.yaml = .github\workflows\docker.yaml 11 | EndProjectSection 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{A17DDC52-23FE-42E3-A75B-ADF38FB362BA}" 14 | ProjectSection(SolutionItems) = preProject 15 | readme.md = readme.md 16 | GitVersion.yml = GitVersion.yml 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Assets", "Assets", "{7A36B0C1-6505-4F1D-B064-207290E084DE}" 20 | ProjectSection(SolutionItems) = preProject 21 | assets\grfsq2-platform-test-rg.drawio.png = assets\grfsq2-platform-test-rg.drawio.png 22 | assets\more-complex.drawio.png = assets\more-complex.drawio.png 23 | EndProjectSection 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDiagramGenerator", "AzureDiagramGenerator\AzureDiagramGenerator.csproj", "{899DE1FB-F964-4325-B52E-44565B7AA1E0}" 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{0E844E37-F819-4A15-A568-F8E692D08A9B}" 28 | ProjectSection(SolutionItems) = preProject 29 | scripts\entrypoint.sh = scripts\entrypoint.sh 30 | EndProjectSection 31 | EndProject 32 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDiagramsTests", "AzureDiagramsTests\AzureDiagramsTests.csproj", "{4E92A880-3F88-41B9-9DF3-DB20FEEB80ED}" 33 | EndProject 34 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{ABB93C54-79E9-4642-903D-FF1D889300BB}" 35 | ProjectSection(SolutionItems) = preProject 36 | Dockerfile = Dockerfile 37 | entrypoint.sh = entrypoint.sh 38 | EndProjectSection 39 | EndProject 40 | Global 41 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 42 | Debug|Any CPU = Debug|Any CPU 43 | Release|Any CPU = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 46 | {14A836C1-11F2-49F4-846C-E50134CD86AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {14A836C1-11F2-49F4-846C-E50134CD86AA}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {14A836C1-11F2-49F4-846C-E50134CD86AA}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {14A836C1-11F2-49F4-846C-E50134CD86AA}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {899DE1FB-F964-4325-B52E-44565B7AA1E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {899DE1FB-F964-4325-B52E-44565B7AA1E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {899DE1FB-F964-4325-B52E-44565B7AA1E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {899DE1FB-F964-4325-B52E-44565B7AA1E0}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {4E92A880-3F88-41B9-9DF3-DB20FEEB80ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {4E92A880-3F88-41B9-9DF3-DB20FEEB80ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {4E92A880-3F88-41B9-9DF3-DB20FEEB80ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {4E92A880-3F88-41B9-9DF3-DB20FEEB80ED}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(NestedProjects) = preSolution 60 | {7D9F0244-632E-4566-B45F-5DE92AB6C31D} = {0E6366E5-D9A9-40CE-A7E1-99AF5CD267CB} 61 | {7A36B0C1-6505-4F1D-B064-207290E084DE} = {A17DDC52-23FE-42E3-A75B-ADF38FB362BA} 62 | {0E844E37-F819-4A15-A568-F8E692D08A9B} = {A17DDC52-23FE-42E3-A75B-ADF38FB362BA} 63 | {ABB93C54-79E9-4642-903D-FF1D889300BB} = {A17DDC52-23FE-42E3-A75B-ADF38FB362BA} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /AzureDiagrams/AzureDiagrams.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | default 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /AzureDiagrams/AzureModelRetriever.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Azure.Core; 9 | using AzureDiagrams.Resources; 10 | using AzureDiagrams.Resources.Retrievers; 11 | 12 | namespace AzureDiagrams; 13 | 14 | public class AzureModelRetriever 15 | { 16 | public async Task Retrieve(TokenCredential tokenCredential, CancellationToken cancellationToken, 17 | Guid subscriptionId, string? tenantId = null, params string[] resourceGroups) 18 | { 19 | var token = await tokenCredential.GetTokenAsync( 20 | new TokenRequestContext(new[] { "https://management.azure.com/" }), 21 | cancellationToken); 22 | 23 | var httpClient = new HttpClient(); 24 | httpClient.BaseAddress = new Uri("https://management.azure.com/"); 25 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); 26 | 27 | var armClient = new ArmClient(httpClient, tokenCredential); 28 | 29 | var expandedResourceGroups = new List(); 30 | await foreach (var rg in armClient.FindResourceGroups(subscriptionId, resourceGroups) 31 | .WithCancellation(cancellationToken)) 32 | { 33 | expandedResourceGroups.Add(rg); 34 | } 35 | 36 | var resources = new List(); 37 | await foreach (var resource in armClient.Retrieve(subscriptionId, expandedResourceGroups) 38 | .WithCancellation(cancellationToken)) resources.Add(resource); 39 | 40 | var allNodes = ProcessResourcesAndAddStaticNodes(resources); 41 | 42 | return allNodes; 43 | } 44 | 45 | public static AzureResource[] ProcessResourcesAndAddStaticNodes(List resources) 46 | { 47 | var additionalNodes = resources.SelectMany(x => x.DiscoverNewNodes(resources)); 48 | 49 | //create some common nodes to represent common platform groupings (AAD, Diagnostics) 50 | var aad = new AzureActiveDirectory { Id = CommonResources.AAD, Name = "Azure Active Directory" }; 51 | var distinctRegions = resources.Select(x => x.Location).Distinct(StringComparer.InvariantCultureIgnoreCase) 52 | .ToArray(); 53 | var regions = distinctRegions.Select(x => new Region(x)).ToArray(); 54 | var core = distinctRegions.Select(x => new CoreServices 55 | { Id = $"{CommonResources.CoreServices}-{x}", Name = x, Location = x }).ToArray(); 56 | var pips = distinctRegions.Select(x => new PublicIpAddresses 57 | { Id = $"{CommonResources.PublicIpAddresses}-{x}", Name = x, Location = x }).ToArray(); 58 | var diagnostics = distinctRegions.Select(x => new CommonDiagnostics 59 | { Id = $"{CommonResources.Diagnostics}-{x}", Name = x, Location = x }).ToArray(); 60 | 61 | var allNodes = resources.Concat(additionalNodes).Concat( 62 | new AzureResource[] { aad }) 63 | .Concat(core) 64 | .Concat(diagnostics) 65 | .Concat(pips) 66 | .Concat(regions) //needs to come last to make sure we don't assign ownership of things multiple times. 67 | .ToArray(); 68 | 69 | //Discover hidden links that aren't obvious through the resource manager 70 | //For example, a NIC / private endpoint linked to a subnet 71 | foreach (var resource in allNodes) resource.BuildRelationships(allNodes); 72 | return allNodes; 73 | } 74 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ACR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureDiagrams.Resources; 4 | 5 | internal class ACR : AzureResource, ICanBeAccessedViaAHostName 6 | { 7 | public override string Image => "img/lib/azure2/containers/Container_Registries.svg"; 8 | 9 | public bool CanIAccessYouOnThisHostName(string hostname) 10 | { 11 | return hostname.Equals($"{Name}.azurecr.io", StringComparison.InvariantCultureIgnoreCase); 12 | } 13 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ADF.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AzureDiagrams.Resources.Retrievers.Custom; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources; 9 | 10 | public class ADF : AzureResource 11 | { 12 | private JObject _linkedServices = default!; 13 | 14 | public override string Image => "img/lib/azure2/databases/Data_Factory.svg"; 15 | 16 | public override Task Enrich(JObject full, Dictionary additionalResources) 17 | { 18 | _linkedServices = additionalResources[AzureDataFactoryRetriever.LinkedServices]!; 19 | return base.Enrich(full, additionalResources); 20 | } 21 | 22 | public override void BuildRelationships(IEnumerable allResources) 23 | { 24 | var possibleConnections = new RelationshipHelper( 25 | _linkedServices["value"]! 26 | .SelectMany(x => 27 | x["properties"]!["typeProperties"]?.ToObject>() 28 | ?.Select(kvp => kvp.Value.ToString() ?? "") ?? Array.Empty()) 29 | .ToArray()); 30 | 31 | possibleConnections.Discover(); 32 | possibleConnections.BuildRelationships(this, allResources); 33 | 34 | base.BuildRelationships(allResources); 35 | } 36 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/AKS.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | internal class AKS : AzureResource 4 | { 5 | public override string Image => "img/lib/azure2/containers/Kubernetes_Services.svg"; 6 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/APIm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AzureDiagrams.Resources.Retrievers.Custom; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources; 9 | 10 | public class APIm : AzureResource, ICanBeAccessedViaAHostName, ICanInjectIntoASubnet 11 | { 12 | public override string Image => "img/lib/azure2/app_services/API_Management_Services.svg"; 13 | 14 | public string[] Backends { get; set; } = default!; 15 | 16 | public string[] HostNames { get; set; } = default!; 17 | 18 | public bool CanIAccessYouOnThisHostName(string hostname) 19 | { 20 | return HostNames.Any(hn => string.Compare(hostname, hn, StringComparison.InvariantCultureIgnoreCase) == 0); 21 | } 22 | 23 | public string[] PublicIpAddresses { get; private set; } = default!; 24 | public string[] SubnetIdsIAmInjectedInto { get; private set; } = default!; 25 | 26 | public override Task Enrich(JObject full, Dictionary additionalResources) 27 | { 28 | HostNames = full["properties"]!["hostnameConfigurations"]!.Select(x => x.Value("hostName")!).ToArray(); 29 | 30 | Backends = additionalResources[ApimServiceResourceRetriever.BackendList]!["value"] 31 | ?.Select(x => x["properties"]!.Value("url")!) 32 | .Select(x => new Uri(x).Host) 33 | .ToArray() ?? Array.Empty(); 34 | 35 | PublicIpAddresses = full["properties"]!["publicIPAddresses"]? 36 | .Select(x => x.Value()!).ToArray() ?? 37 | Array.Empty(); 38 | 39 | var vnetConfig = full["properties"]!["virtualNetworkConfiguration"]!; 40 | var subnet = vnetConfig.Type == JTokenType.Null ? null : vnetConfig!.Value("subnetResourceId"); 41 | SubnetIdsIAmInjectedInto = subnet != null ? new[] { subnet! } : Array.Empty(); 42 | 43 | return base.Enrich(full, additionalResources); 44 | } 45 | 46 | public override void BuildRelationships(IEnumerable allResources) 47 | { 48 | Backends.ForEach(x => this.CreateFlowToHostName(allResources, x, "calls", Plane.Runtime)); 49 | base.BuildRelationships(allResources); 50 | } 51 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/AppGateway.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class AppGateway : AzureResource, ICanBeAccessedViaAHostName, ICanInjectIntoASubnet, ICanExposePublicIPAddresses 10 | { 11 | public override string Image => "img/lib/azure2/networking/Application_Gateways.svg"; 12 | 13 | public bool CanIAccessYouOnThisHostName(string hostname) 14 | { 15 | return Hostnames.Contains(hostname, StringComparer.InvariantCultureIgnoreCase); 16 | } 17 | 18 | public string[] Hostnames { get; private set; } = default!; 19 | 20 | public string[] HostnamesITryToContact { get; private set; } = default!; 21 | 22 | public string[] PublicIpAddresses { get; private set; } = default!; 23 | 24 | public string[] SubnetIdsIAmInjectedInto { get; private set; } = default!; 25 | 26 | public bool IsWaf { get; set; } 27 | 28 | public override Task Enrich(JObject full, Dictionary additionalResources) 29 | { 30 | IsWaf = full["properties"]!["sku"]!.Value("tier")!.Contains("waf", 31 | StringComparison.CurrentCultureIgnoreCase); 32 | 33 | SubnetIdsIAmInjectedInto = full["properties"]!["gatewayIPConfigurations"]? 34 | .Select(x => x["properties"]!["subnet"]!.Value("id")!.ToLowerInvariant()) 35 | .ToArray() ?? Array.Empty(); 36 | 37 | Hostnames = full["properties"]!["httpListeners"]? 38 | .SelectMany(x => 39 | x["properties"]!["hostNames"]?.Values().Select(hn => hn!.ToLowerInvariant()) ?? 40 | Array.Empty()) 41 | .ToArray() ?? Array.Empty(); 42 | 43 | HostnamesITryToContact = full["properties"]!["backendAddressPools"]? 44 | .SelectMany(x => 45 | x["properties"]!["backendAddresses"]?.Select(ba => ba["fqdn"]?.Value()?.ToLowerInvariant()) 46 | .Where(ba => ba != null).Select(ba => ba!) ?? Array.Empty()) 47 | .ToArray() ?? Array.Empty(); 48 | 49 | PublicIpAddresses = full["properties"]!["frontendIPConfigurations"]? 50 | .Select(x => x["properties"]!["publicIPAddress"]?.Value("id")) 51 | .Where(x => x != null) 52 | .Select(x => x!) 53 | .ToArray() ?? Array.Empty(); 54 | 55 | return base.Enrich(full, additionalResources); 56 | } 57 | 58 | 59 | public override void BuildRelationships(IEnumerable allResources) 60 | { 61 | HostnamesITryToContact.ForEach(x => this.CreateFlowToHostName(allResources, x, "Backend", Plane.Runtime)); 62 | base.BuildRelationships(allResources); 63 | } 64 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/AppInsights.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | internal class AppInsights : AzureResource 10 | { 11 | public override string Image => "img/lib/azure2/devops/Application_Insights.svg"; 12 | public string InstrumentationKey { get; private set; } = default!; 13 | public string? WorkspaceResourceId { get; private set; } 14 | 15 | public override Task Enrich(JObject full, Dictionary additionalResources) 16 | { 17 | InstrumentationKey = full["properties"]!.Value("InstrumentationKey")!; 18 | WorkspaceResourceId = full["properties"]!.Value("WorkspaceResourceId"); 19 | return base.Enrich(full, additionalResources); 20 | } 21 | 22 | public override void BuildRelationships(IEnumerable allResources) 23 | { 24 | if (WorkspaceResourceId != null) 25 | { 26 | var workspace = allResources.OfType().SingleOrDefault(x => 27 | string.Compare(WorkspaceResourceId, x.Id, StringComparison.InvariantCultureIgnoreCase) == 0); 28 | if (workspace != null) CreateFlowTo(workspace, "logs", Plane.Diagnostics); 29 | } 30 | base.BuildRelationships(allResources); 31 | } 32 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/AppServiceApp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | using AzureDiagrams.Resources.Retrievers.Custom; 7 | using AzureDiagrams.Resources.Retrievers.Extensions; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | 11 | namespace AzureDiagrams.Resources; 12 | 13 | public class AppServiceApp : AzureResource, ICanBeAccessedViaAHostName, ICanEgressViaAVnet 14 | { 15 | private string? _dockerRepo; 16 | private string? _searchService; 17 | private RelationshipHelper _hostNameDiscoverer = default!; 18 | public string ServerFarmId { get; set; } = default!; 19 | public string? VirtualNetworkSubnetId { get; set; } 20 | public string Kind { get; set; } = default!; 21 | 22 | public override string Image => Kind switch 23 | { 24 | { } str when str.Contains("workflowapp") => "img/lib/azure2/integration/Logic_Apps.svg", 25 | { } str when str.Contains("functionapp") => "img/lib/azure2/compute/Function_Apps.svg", 26 | _ => "img/lib/azure2/app_services/App_Services.svg" 27 | }; 28 | 29 | public string? AppInsightsKeyOrConnectionString { get; set; } 30 | 31 | public string[] EnabledHostNames { get; set; } = default!; 32 | 33 | public AppServiceApp(string id, string serverFarmId, string name, bool isSlot, string[] connectionStrings, 34 | string[] hostNames, string? virtualNetworkSubnetId = null, PrivateEndpointExtensions? privateEndpointExtension = null) 35 | { 36 | Id = id; 37 | ServerFarmId = serverFarmId; 38 | Name = name; 39 | EnabledHostNames = hostNames; 40 | Type = isSlot ? "microsoft.web/sites/slots" : "microsoft.web/sites"; 41 | _hostNameDiscoverer = new RelationshipHelper(connectionStrings); 42 | _hostNameDiscoverer.Discover(); 43 | VirtualNetworkSubnetId = virtualNetworkSubnetId; 44 | if (privateEndpointExtension != null) Extensions = new [] {privateEndpointExtension}; 45 | } 46 | 47 | /// 48 | /// Used for json deserialization 49 | /// 50 | [JsonConstructor] 51 | public AppServiceApp() 52 | { 53 | } 54 | 55 | public bool CanIAccessYouOnThisHostName(string hostname) 56 | { 57 | return EnabledHostNames.Any( 58 | hn => string.Compare(hn, hostname, StringComparison.InvariantCultureIgnoreCase) == 0); 59 | } 60 | 61 | 62 | public override async Task Enrich(JObject full, Dictionary additionalResources) 63 | { 64 | await base.Enrich(full, additionalResources); 65 | VirtualNetworkSubnetId = full["properties"]!["virtualNetworkSubnetId"]?.Value(); 66 | ServerFarmId = full["properties"]!.Value("serverFarmId")!; 67 | 68 | var config = additionalResources[AppResourceRetriever.ConfigAppSettingsList]; 69 | 70 | var siteProperties = full["properties"]!["siteProperties"]?["properties"]? 71 | .Select( 72 | x => new KeyValuePair( 73 | x.Value("name")!, 74 | x.Value("value"))) 75 | .ToDictionary(x => x.Key, x => x.Value)!; 76 | 77 | LookForContainerLink(siteProperties); 78 | 79 | var connectionStrings = additionalResources[AppResourceRetriever.ConnectionStringSettingsList]? 80 | ["properties"]!.ToObject>()?.Values 81 | .Select(x => x.Value("value")).Where(x => x != null).Select(x => x!) ?? Array.Empty(); 82 | 83 | var appSettings = config?["properties"]!.ToObject>() ?? 84 | new Dictionary(); 85 | var potentialConnectionStrings = appSettings.Values.Union(connectionStrings).OfType().ToArray(); 86 | _hostNameDiscoverer = new RelationshipHelper(potentialConnectionStrings); 87 | _hostNameDiscoverer.Discover(); 88 | 89 | var potentialAppInsightsKey = appSettings.Keys.FirstOrDefault(x => 90 | x.Contains("appinsights", StringComparison.InvariantCultureIgnoreCase) || 91 | x.Contains("applicationinsights", StringComparison.InvariantCultureIgnoreCase)); 92 | 93 | 94 | if (potentialAppInsightsKey != null) 95 | AppInsightsKeyOrConnectionString = (string)appSettings[potentialAppInsightsKey]; 96 | 97 | EnabledHostNames = full["properties"]!["enabledHostNames"]!.Values().Select(x => x!).ToArray(); 98 | 99 | if (appSettings.ContainsKey("AzureSearchName")) 100 | { 101 | _searchService = $"{(string)appSettings["AzureSearchName"]}.search.windows.net"; 102 | } 103 | } 104 | 105 | /// 106 | /// Look in site properties for anything starting with DOCKER| 107 | /// 108 | /// 109 | /// 110 | private void LookForContainerLink(Dictionary siteProperties) 111 | { 112 | var regex = new Regex(@"^DOCKER[|](.*?)\/"); 113 | _dockerRepo = siteProperties.Values.Where(x => x != null).Select(x => regex.Match(x!)) 114 | .FirstOrDefault(x => x.Success)?.Groups[1].Captures[0] 115 | .Value; 116 | } 117 | 118 | public override IEnumerable DiscoverNewNodes(List azureResources) 119 | { 120 | if (VirtualNetworkSubnetId != null) 121 | { 122 | VNetIntegration = new VNetIntegration($"{Id}.vnetintegration", VirtualNetworkSubnetId, this) 123 | { 124 | Name = Name 125 | }; 126 | yield return VNetIntegration; 127 | } 128 | } 129 | 130 | public override void BuildRelationships(IEnumerable allResources) 131 | { 132 | if (AppInsightsKeyOrConnectionString != null) 133 | { 134 | var appInsights = allResources.OfType() 135 | .SingleOrDefault(x => AppInsightsKeyOrConnectionString.Contains(x.InstrumentationKey)); 136 | if (appInsights != null) CreateFlowTo(appInsights, "apm", Plane.Diagnostics); 137 | } 138 | 139 | GroupSlot(allResources); 140 | 141 | if (_dockerRepo != null) 142 | { 143 | this.CreateFlowToHostName(allResources, _dockerRepo, "container pull", Plane.Runtime); 144 | } 145 | 146 | if (_searchService != null) 147 | { 148 | this.CreateFlowToHostName(allResources, _searchService, "search api", Plane.Runtime); 149 | } 150 | 151 | _hostNameDiscoverer.BuildRelationships(this, allResources); 152 | 153 | if (VNetIntegration != null) 154 | { 155 | CreateFlowTo(VNetIntegration, Plane.All); 156 | } 157 | 158 | base.BuildRelationships(allResources); 159 | } 160 | 161 | private void GroupSlot(IEnumerable allResources) 162 | { 163 | if (IsSlotContainerByAnotherApp(allResources, out var parent)) 164 | { 165 | parent!.AddSlot(this); 166 | } 167 | } 168 | 169 | internal bool IsSlotContainerByAnotherApp(IEnumerable allResources, out AzureResource? parent) 170 | { 171 | if (Type.Contains("/slots")) 172 | { 173 | var parentWebAppId = string.Join('/', Id.Split('/')[..^2]); 174 | var parentWebApp = allResources.SingleOrDefault(x => 175 | x.Id.Equals(parentWebAppId, StringComparison.InvariantCultureIgnoreCase)); 176 | parent = parentWebApp; 177 | return parentWebApp != null; 178 | } 179 | 180 | parent = null; 181 | return false; 182 | } 183 | 184 | public VNetIntegration? VNetIntegration { get; private set; } 185 | 186 | public AzureResource EgressResource() 187 | { 188 | if (VNetIntegration != null) return VNetIntegration; 189 | return this; 190 | } 191 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/AppServiceEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class AppServiceEnvironment : AzureResource, ICanInjectIntoASubnet 10 | { 11 | public override string Image => "img/lib/azure2/app_services/App_Service_Environments.svg"; 12 | 13 | public string[] SubnetIdsIAmInjectedInto { get; private set; } = default!; 14 | 15 | public override Task Enrich(JObject full, Dictionary additionalResources) 16 | { 17 | SubnetIdsIAmInjectedInto = new[] { full["properties"]!["virtualNetwork"]!.Value("id")! }; 18 | return base.Enrich(full, additionalResources); 19 | } 20 | 21 | public override void BuildRelationships(IEnumerable allResources) 22 | { 23 | var asps = allResources.OfType().Where(x => 24 | string.Equals(Id, x.ASE, StringComparison.InvariantCultureIgnoreCase)).ToArray(); 25 | asps.ForEach(OwnsResource); 26 | base.BuildRelationships(allResources); 27 | } 28 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/AppServicePlan.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources; 9 | 10 | public class AppServicePlan : AzureResource 11 | { 12 | public override string Image => "img/lib/azure2/app_services/App_Service_Plans.svg"; 13 | public override string? Fill => "#DAE8FC"; 14 | public string? ASE { get; private set; } 15 | 16 | public AppServicePlan(string id, string name) 17 | { 18 | Id = id; 19 | Name = name; 20 | Type = "microsoft.web/serverfarms"; 21 | } 22 | 23 | /// 24 | /// Used for json deserialization 25 | /// 26 | [JsonConstructor] 27 | public AppServicePlan() 28 | { 29 | } 30 | 31 | 32 | public override Task Enrich(JObject full, Dictionary additionalResources) 33 | { 34 | var hostingEnvironmentProfile = full["properties"]!["hostingEnvironmentProfile"]!; 35 | if (hostingEnvironmentProfile.Type != JTokenType.Null) 36 | { 37 | ASE = hostingEnvironmentProfile.Value("id"); 38 | } 39 | return base.Enrich(full, additionalResources); 40 | } 41 | 42 | public override void BuildRelationships(IEnumerable allResources) 43 | { 44 | var apps = allResources.OfType() 45 | .Where(x => string.Equals(Id, x.ServerFarmId, StringComparison.InvariantCultureIgnoreCase)) 46 | .Where(x => IsNotSlotContainerByAnotherApp(x, allResources)) 47 | .ToArray(); 48 | apps.ForEach(OwnsResource); 49 | base.BuildRelationships(allResources); 50 | } 51 | 52 | private bool IsNotSlotContainerByAnotherApp(AppServiceApp appServiceApp, IEnumerable allResources) 53 | { 54 | return !appServiceApp.IsSlotContainerByAnotherApp(allResources, out _); 55 | } 56 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/AzureActiveDirectory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace AzureDiagrams.Resources; 5 | 6 | public class AzureActiveDirectory : AzureResource 7 | { 8 | public override bool IsPureContainer => true; 9 | 10 | public override void BuildRelationships(IEnumerable allResources) 11 | { 12 | allResources.OfType().ForEach(OwnsResource); 13 | base.BuildRelationships(allResources); 14 | } 15 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/AzureResource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using AzureDiagrams.Resources.Retrievers.Extensions; 9 | using Newtonsoft.Json.Linq; 10 | 11 | namespace AzureDiagrams.Resources; 12 | 13 | [DebuggerDisplay("{Type}/{Name}")] 14 | public class AzureResource 15 | { 16 | private readonly string _id = default!; 17 | private readonly string? _location = default!; 18 | 19 | /// 20 | /// TODO make this a diagram construct. It is used to mark a resource that is rendered as a container of other resource, with no icon for itself. 21 | /// 22 | public virtual bool IsPureContainer => false; 23 | 24 | public List Links { get; } = new(); 25 | public List ContainedResources { get; } = new(); 26 | public IEnumerable Extensions { get; set; } = Array.Empty(); 27 | 28 | public string Id 29 | { 30 | get => _id; 31 | init 32 | { 33 | _id = value; 34 | InternalId = new Guid(SHA256.HashData(Encoding.UTF8.GetBytes(value))[..16]).ToString(); 35 | } 36 | } 37 | 38 | /// 39 | /// This is a deterministic guid based on the Resource Id. You cannot set it. It's worked out when you set the ID. 40 | /// 41 | public string InternalId { get; private init; } = default!; 42 | 43 | public string Name { get; set; } = default!; 44 | public virtual string Image { get; } = default!; 45 | public virtual string? Fill { get; } 46 | 47 | public string? Location 48 | { 49 | get => _location; 50 | init => _location = value?.Replace(" ", "").ToLowerInvariant(); 51 | } 52 | 53 | public string ManagedBy { get; set; } = default!; 54 | 55 | /// 56 | /// Used to indicate if another resource 'owns' this one. Example would be injecting a NIC into a Subnet. 57 | /// Initial use of this flag is to push the responsibility of drawing an object to the containing resource. Not the top 58 | /// level. 59 | /// 60 | public bool ContainedByAnotherResource { get; protected internal set; } 61 | 62 | public string? Type { get; set; } = default!; 63 | 64 | public virtual Task Enrich(JObject full, Dictionary additionalResources) 65 | { 66 | return Task.CompletedTask; 67 | } 68 | 69 | /// 70 | /// Opportunity to explode any 'new' nodes that aren't represented by ARM resources, but important to the diagram. 71 | /// Example is App-Service VNet Integration. We want to show flows going through the vnet-integrated subnet. 72 | /// 73 | /// 74 | public virtual IEnumerable DiscoverNewNodes(List azureResources) 75 | { 76 | yield break; 77 | } 78 | 79 | /// 80 | /// Override this to build derived relationships between nodes. 81 | /// An example would be using metadata to add private endpoints / NICs into subnets. 82 | /// 83 | /// 84 | public virtual void BuildRelationships(IEnumerable allResources) 85 | { 86 | Extensions.ForEach(x => x.BuildRelationships(this, allResources)); 87 | } 88 | 89 | /// 90 | /// Creates a flow between two resources. Commonly visualised as a line on a graph between boxes 91 | /// 92 | /// 93 | /// 94 | protected internal void CreateFlowTo(AzureResource to, Plane plane) 95 | { 96 | CreateFlowTo(to, string.Empty, plane); 97 | } 98 | 99 | /// 100 | /// Creates a flow between two resources. Commonly visualised as a line on a graph between boxes 101 | /// 102 | /// 103 | /// 104 | /// 105 | protected internal void CreateFlowTo(AzureResource to, string details, Plane plane) 106 | { 107 | if (IsPureContainer) throw new InvalidOperationException("You cannot create a flow to a pure container"); 108 | 109 | if (Links.Any(x => x.To == to)) 110 | { 111 | return; 112 | } 113 | 114 | var opposite = to.Links.SingleOrDefault(x => x.To == this && x.Plane == plane); 115 | if (opposite != null) 116 | { 117 | opposite.MakeTwoWay(); 118 | } 119 | else 120 | { 121 | var link = new ResourceLink(this, to, details, plane); 122 | Links.Add(link); 123 | } 124 | } 125 | 126 | /// 127 | /// Containing a resource will cause anything without a 'specific' drawer to be rendered as a container, with all 128 | /// contained resources inside. 129 | /// Also sets the ContainedByAnotherResource flag to tell the drawer that something else will draw it 130 | /// 131 | /// 132 | protected void OwnsResource(AzureResource contained) 133 | { 134 | ContainedResources.Add(contained); 135 | contained.ContainedByAnotherResource = true; 136 | } 137 | 138 | public void AddSlot(AppServiceApp appServiceApp) 139 | { 140 | OwnsResource(appServiceApp); 141 | } 142 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Bastion.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | public class Bastion : AzureResource, ICanInjectIntoASubnet, ICanExposePublicIPAddresses 8 | { 9 | private IpConfigurations _ipConfigurations = default!; 10 | public override string Image => "img/lib/azure2/networking/Connections.svg"; 11 | public string[] PublicIpAddresses => _ipConfigurations.PublicIpAddresses; 12 | public string[] SubnetIdsIAmInjectedInto => _ipConfigurations.SubnetAttachments; 13 | 14 | public override Task Enrich(JObject full, Dictionary additionalResources) 15 | { 16 | _ipConfigurations = new IpConfigurations(full); 17 | return base.Enrich(full, additionalResources); 18 | } 19 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/BigDataPool.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | public class BigDataPool : AzureResource 8 | { 9 | public override string Image => "img/lib/azure2/preview/RTOS.svg"; 10 | 11 | public override Task Enrich(JObject full, Dictionary additionalResources) 12 | { 13 | return base.Enrich(full, additionalResources); 14 | } 15 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Bot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources; 7 | 8 | public class Bot : AzureResource 9 | { 10 | public override string Image => "img/lib/mscae/Bot_Services.svg"; 11 | 12 | public string? BotEndpoint { get; set; } 13 | 14 | public override Task Enrich(JObject full, Dictionary additionalResources) 15 | { 16 | BotEndpoint = full["properties"]!.Value("endpoint"); 17 | return base.Enrich(full, additionalResources); 18 | } 19 | 20 | public override void BuildRelationships(IEnumerable allResources) 21 | { 22 | if (BotEndpoint != null) 23 | { 24 | if (Uri.TryCreate(BotEndpoint, UriKind.Absolute, out var uri)) 25 | { 26 | this.CreateFlowToHostName(allResources, uri.Host, "communicates", Plane.Runtime); 27 | } 28 | } 29 | base.BuildRelationships(allResources); 30 | } 31 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/CognitiveSearch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class CognitiveSearch : AzureResource, ICanBeAccessedViaAHostName 10 | { 11 | private IEnumerable _resourcesAccessOverPrivateLink; 12 | public override string Image => "img/lib/azure2/app_services/Search_Services.svg"; 13 | 14 | public string HostName { get; set; } = default!; 15 | 16 | public bool CanIAccessYouOnThisHostName(string hostname) 17 | { 18 | return string.Compare(HostName, hostname, StringComparison.InvariantCultureIgnoreCase) == 0 19 | || Name.Equals(hostname, StringComparison.InvariantCultureIgnoreCase); 20 | ; 21 | } 22 | 23 | public override Task Enrich(JObject full, Dictionary additionalResources) 24 | { 25 | HostName = $"{Name.ToLowerInvariant()}.search.windows.net"; 26 | 27 | _resourcesAccessOverPrivateLink = full["properties"]!["sharedPrivateLinkResources"]? 28 | .Select(x => x["properties"]!.Value("privateLinkResourceId")!) ?? []; 29 | 30 | return base.Enrich(full, additionalResources); 31 | } 32 | 33 | public override void BuildRelationships(IEnumerable allResources) 34 | { 35 | _resourcesAccessOverPrivateLink.ForEach(x => 36 | { 37 | var resource = allResources.SingleOrDefault(r => r.Id.ToLowerInvariant() == x.ToLowerInvariant()); 38 | if (resource != null) 39 | { 40 | CreateFlowTo(resource, "Private Link", Plane.Runtime); 41 | } 42 | }); 43 | base.BuildRelationships(allResources); 44 | } 45 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/CognitiveServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class CognitiveServices : AzureResource, ICanBeAccessedViaAHostName 10 | { 11 | const string AzureOpenAISvg = "data:image/svg+xml,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgdmlld0JveD0iMCAwIDI0IDI0IiBoZWlnaHQ9IjgwMHB4IiB3aWR0aD0iODAwcHgiIGZpbGw9IiMwMDAwMDAiPjx0aXRsZT5PcGVuQUkgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTIyLjI4MTkgOS44MjExYTUuOTg0NyA1Ljk4NDcgMCAwIDAtLjUxNTctNC45MTA4IDYuMDQ2MiA2LjA0NjIgMCAwIDAtNi41MDk4LTIuOUE2LjA2NTEgNi4wNjUxIDAgMCAwIDQuOTgwNyA0LjE4MThhNS45ODQ3IDUuOTg0NyAwIDAgMC0zLjk5NzcgMi45IDYuMDQ2MiA2LjA0NjIgMCAwIDAgLjc0MjcgNy4wOTY2IDUuOTggNS45OCAwIDAgMCAuNTExIDQuOTEwNyA2LjA1MSA2LjA1MSAwIDAgMCA2LjUxNDYgMi45MDAxQTUuOTg0NyA1Ljk4NDcgMCAwIDAgMTMuMjU5OSAyNGE2LjA1NTcgNi4wNTU3IDAgMCAwIDUuNzcxOC00LjIwNTggNS45ODk0IDUuOTg5NCAwIDAgMCAzLjk5NzctMi45MDAxIDYuMDU1NyA2LjA1NTcgMCAwIDAtLjc0NzUtNy4wNzI5em0tOS4wMjIgMTIuNjA4MWE0LjQ3NTUgNC40NzU1IDAgMCAxLTIuODc2NC0xLjA0MDhsLjE0MTktLjA4MDQgNC43NzgzLTIuNzU4MmEuNzk0OC43OTQ4IDAgMCAwIC4zOTI3LS42ODEzdi02LjczNjlsMi4wMiAxLjE2ODZhLjA3MS4wNzEgMCAwIDEgLjAzOC4wNTJ2NS41ODI2YTQuNTA0IDQuNTA0IDAgMCAxLTQuNDk0NSA0LjQ5NDR6bS05LjY2MDctNC4xMjU0YTQuNDcwOCA0LjQ3MDggMCAwIDEtLjUzNDYtMy4wMTM3bC4xNDIuMDg1MiA0Ljc4MyAyLjc1ODJhLjc3MTIuNzcxMiAwIDAgMCAuNzgwNiAwbDUuODQyOC0zLjM2ODV2Mi4zMzI0YS4wODA0LjA4MDQgMCAwIDEtLjAzMzIuMDYxNUw5Ljc0IDE5Ljk1MDJhNC40OTkyIDQuNDk5MiAwIDAgMS02LjE0MDgtMS42NDY0ek0yLjM0MDggNy44OTU2YTQuNDg1IDQuNDg1IDAgMCAxIDIuMzY1NS0xLjk3MjhWMTEuNmEuNzY2NC43NjY0IDAgMCAwIC4zODc5LjY3NjVsNS44MTQ0IDMuMzU0My0yLjAyMDEgMS4xNjg1YS4wNzU3LjA3NTcgMCAwIDEtLjA3MSAwbC00LjgzMDMtMi43ODY1QTQuNTA0IDQuNTA0IDAgMCAxIDIuMzQwOCA3Ljg3MnptMTYuNTk2MyAzLjg1NThMMTMuMTAzOCA4LjM2NCAxNS4xMTkyIDcuMmEuMDc1Ny4wNzU3IDAgMCAxIC4wNzEgMGw0LjgzMDMgMi43OTEzYTQuNDk0NCA0LjQ5NDQgMCAwIDEtLjY3NjUgOC4xMDQydi01LjY3NzJhLjc5Ljc5IDAgMCAwLS40MDctLjY2N3ptMi4wMTA3LTMuMDIzMWwtLjE0Mi0uMDg1Mi00Ljc3MzUtMi43ODE4YS43NzU5Ljc3NTkgMCAwIDAtLjc4NTQgMEw5LjQwOSA5LjIyOTdWNi44OTc0YS4wNjYyLjA2NjIgMCAwIDEgLjAyODQtLjA2MTVsNC44MzAzLTIuNzg2NmE0LjQ5OTIgNC40OTkyIDAgMCAxIDYuNjgwMiA0LjY2ek04LjMwNjUgMTIuODYzbC0yLjAyLTEuMTYzOGEuMDgwNC4wODA0IDAgMCAxLS4wMzgtLjA1NjdWNi4wNzQyYTQuNDk5MiA0LjQ5OTIgMCAwIDEgNy4zNzU3LTMuNDUzN2wtLjE0Mi4wODA1TDguNzA0IDUuNDU5YS43OTQ4Ljc5NDggMCAwIDAtLjM5MjcuNjgxM3ptMS4wOTc2LTIuMzY1NGwyLjYwMi0xLjQ5OTggMi42MDY5IDEuNDk5OHYyLjk5OTRsLTIuNTk3NCAxLjQ5OTctMi42MDY3LTEuNDk5N1oiLz48L3N2Zz4="; 12 | 13 | public override string Image => Kind.ToLowerInvariant() switch 14 | { 15 | "textanalytics" => "img/lib/azure2/ai_machine_learning/Language_Services.svg", 16 | "openai" => AzureOpenAISvg, 17 | _ => "img/lib/azure2/ai_machine_learning/Cognitive_Services.svg" 18 | } ; 19 | 20 | public string Kind { get; set; } = default!; 21 | 22 | public string[] HostNames { get; set; } = default!; 23 | 24 | public bool CanIAccessYouOnThisHostName(string hostname) 25 | { 26 | return HostNames.Contains(hostname.ToLowerInvariant()) 27 | || Name.Equals(hostname, StringComparison.InvariantCultureIgnoreCase); 28 | } 29 | 30 | public override Task Enrich(JObject full, Dictionary additionalResources) 31 | { 32 | HostNames = full["properties"]!["endpoints"]!.ToObject>()!.Values 33 | .Select(x => x.GetHostNameFromUrlString()).ToArray(); 34 | return base.Enrich(full, additionalResources); 35 | } 36 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/CommonDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace AzureDiagrams.Resources; 5 | 6 | public class CommonDiagnostics : AzureResource 7 | { 8 | public override bool IsPureContainer => true; 9 | 10 | public override void BuildRelationships(IEnumerable allResources) 11 | { 12 | allResources.OfType().Where(x => x.Location == Location).ForEach(OwnsResource); 13 | allResources.OfType().Where(x => x.Location == Location).ForEach(OwnsResource); 14 | base.BuildRelationships(allResources); 15 | } 16 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/CommonResources.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public static class CommonResources 4 | { 5 | public const string AAD = "c8be31af-ab7c-4759-862d-9b9344a4a54b"; 6 | public const string CoreServices = "516f3ff1-d065-4ce2-806b-160eb431bae5"; 7 | public const string PublicIpAddresses = "4a34de86-a2fe-4e8f-9dd3-7f7bc3435811"; 8 | public const string Diagnostics = "813867a2-b7e6-4bef-9d78-d36e02e8b533"; 9 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ContainerApp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | internal class ContainerApp : AzureResource, ICanBeAccessedViaAHostName 10 | { 11 | public string ContainerAppEnvironmentId { get; set; } = default!; 12 | public override string Image => "img/lib/azure2/other/Worker_Container_App.svg"; 13 | 14 | public string IngressFqdn { get; private set; } = default!; 15 | 16 | 17 | public bool CanIAccessYouOnThisHostName(string hostname) 18 | { 19 | return string.Compare(IngressFqdn, hostname, StringComparison.InvariantCultureIgnoreCase) == 0; 20 | } 21 | 22 | public override Task Enrich(JObject full, Dictionary additionalResources) 23 | { 24 | ContainerAppEnvironmentId = full["properties"]!.Value("managedEnvironmentId")!; 25 | IngressFqdn = full["properties"]!["configuration"]!["ingress"]!.Value("fqdn")!; 26 | return base.Enrich(full, additionalResources); 27 | } 28 | 29 | public override void BuildRelationships(IEnumerable allResources) 30 | { 31 | var kubeEnvironment = allResources.OfType().SingleOrDefault(x => 32 | string.Compare(x.Id, ContainerAppEnvironmentId, StringComparison.InvariantCultureIgnoreCase) == 0); 33 | 34 | kubeEnvironment?.DiscoveredContainerApp(this); 35 | base.BuildRelationships(allResources); 36 | } 37 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ContainerAppEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | internal class ContainerAppEnvironment : AzureResource, ICanWriteToLogAnalyticsWorkspaces, ICanInjectIntoASubnet 8 | { 9 | private string[] _subnets; 10 | public override string Image => "img/lib/azure2/other/Container_App_Environments.svg"; 11 | public string? LogAnalyticsCustomerId { get; private set; } 12 | 13 | public bool DoYouWriteTo(string customerId) 14 | { 15 | return LogAnalyticsCustomerId == customerId; 16 | } 17 | 18 | public void CreateFlowBackToMe(LogAnalyticsWorkspace workspace) 19 | { 20 | CreateFlowTo(workspace, "logs", Plane.Diagnostics); 21 | } 22 | 23 | public override Task Enrich(JObject full, Dictionary additionalResources) 24 | { 25 | var jToken = full["properties"]! 26 | ["appLogsConfiguration"]? 27 | ["logAnalyticsConfiguration"]; 28 | 29 | jToken = jToken?.Type == JTokenType.Null ? null : jToken; 30 | 31 | LogAnalyticsCustomerId = jToken?.Value("customerId"); 32 | 33 | var subnet = full["properties"]!["vnetConfiguration"]?.Value("infrastructureSubnetId"); 34 | _subnets = subnet != null ? [subnet] : []; 35 | 36 | return base.Enrich(full, additionalResources); 37 | } 38 | 39 | public void DiscoveredContainerApp(ContainerApp containerApp) 40 | { 41 | OwnsResource(containerApp); 42 | } 43 | 44 | public string[] SubnetIdsIAmInjectedInto => _subnets; 45 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ContainerInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class ContainerInstance : AzureResource, ICanInjectIntoASubnet 10 | { 11 | private RelationshipHelper _hostNameDiscoverer = default!; 12 | private string? _networkProfile = default!; 13 | public override string Image => "img/lib/azure2/containers/Container_Instances.svg"; 14 | 15 | public string[] SubnetIdsIAmInjectedInto { get; private set; } = default!; 16 | 17 | 18 | public override async Task Enrich(JObject full, Dictionary additionalResources) 19 | { 20 | await base.Enrich(full, additionalResources); 21 | 22 | _networkProfile = full["properties"]!["networkProfile"]?.Value("id"); 23 | 24 | var imageSource = full["properties"]!["containers"] 25 | ?.Select(x => x["properties"]!.Value("image")?.Split('/')[0]); 26 | 27 | var properties = full["properties"]!["containers"]?.SelectMany(x => x["properties"]!["environmentVariables"]! 28 | .Select(x => x.Value("value"))) ?? Enumerable.Empty(); 29 | 30 | if (imageSource != null) 31 | //technically not a URL but the relationship helper will sort that out for us: 32 | properties = properties.Concat(imageSource.Select(x => $"https://{x}")).Distinct(); 33 | 34 | _hostNameDiscoverer = 35 | new RelationshipHelper(properties.Where(x => x != null).Select(x => x!).ToArray()); 36 | 37 | _hostNameDiscoverer.Discover(); 38 | } 39 | 40 | public override void BuildRelationships(IEnumerable allResources) 41 | { 42 | base.BuildRelationships(allResources); 43 | _hostNameDiscoverer.BuildRelationships(this, allResources); 44 | } 45 | 46 | /// 47 | /// Using this as a hook to find the network profile that tells me which subnet I'm injected into 48 | /// 49 | /// 50 | /// 51 | public override IEnumerable DiscoverNewNodes(List azureResources) 52 | { 53 | if (_networkProfile != null) 54 | { 55 | SubnetIdsIAmInjectedInto = azureResources.OfType() 56 | .SingleOrDefault(x => x.Id.Equals(_networkProfile!, StringComparison.InvariantCultureIgnoreCase))?.SubnetIds ?? Array.Empty(); 57 | } 58 | 59 | return base.DiscoverNewNodes(azureResources); 60 | } 61 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/CoreServices.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace AzureDiagrams.Resources; 5 | 6 | public class CoreServices : AzureResource 7 | { 8 | public override bool IsPureContainer => true; 9 | 10 | public override void BuildRelationships(IEnumerable allResources) 11 | { 12 | allResources.OfType().Where(x => x.Location == Location).ForEach(OwnsResource); 13 | allResources.OfType().Where(x => x.Location == Location).ForEach(OwnsResource); 14 | allResources.OfType().Where(x => x.Location == Location).ForEach(OwnsResource); 15 | base.BuildRelationships(allResources); 16 | } 17 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/CosmosDb.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | internal class CosmosDb : AzureResource, ICanBeAccessedViaAHostName 8 | { 9 | public override string Image => "img/lib/azure2/databases/Azure_Cosmos_DB.svg"; 10 | 11 | public string? DocumentEndpointHost { get; set; } 12 | 13 | public bool CanIAccessYouOnThisHostName(string hostname) 14 | { 15 | return DocumentEndpointHost?.CompareTo(hostname.ToLowerInvariant()) == 0; 16 | } 17 | 18 | public override Task Enrich(JObject full, Dictionary additionalResources) 19 | { 20 | DocumentEndpointHost = 21 | full["properties"]!.Value("documentEndpoint")?.GetHostNameFromUrlString() ?? null; 22 | return base.Enrich(full, additionalResources); 23 | } 24 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Disk.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public class Disk : AzureResource 4 | { 5 | public override string Image => "img/lib/azure2/compute/Disks.svg"; 6 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/DnsZoneVirtualNetworkLink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class DnsZoneVirtualNetworkLink : AzureResource 10 | { 11 | private string _dnsZone = default!; 12 | private string _virtualNetwork = default!; 13 | 14 | public override Task Enrich(JObject full, Dictionary additionalResources) 15 | { 16 | _virtualNetwork = full["properties"]!["virtualNetwork"]!.Value("id")!; 17 | _dnsZone = string.Join('/', Id.Split("/").ToArray()[..^2]); 18 | return Task.CompletedTask; 19 | } 20 | 21 | public override void BuildRelationships(IEnumerable allResources) 22 | { 23 | var dnsZone = allResources.OfType().Single(x => x.Id.Equals(_dnsZone, StringComparison.InvariantCultureIgnoreCase)); 24 | var vnet = allResources.OfType().SingleOrDefault(x => x.Id.Equals(_virtualNetwork, StringComparison.InvariantCultureIgnoreCase)); 25 | if (vnet != null) 26 | { 27 | vnet.AssignPrivateDnsZone(dnsZone); 28 | dnsZone.ContainedByAnotherResource = true; 29 | } 30 | else 31 | { 32 | Console.WriteLine($"Failed to find VNET {_virtualNetwork} to link to DNS Zone {_dnsZone}"); 33 | } 34 | 35 | base.BuildRelationships(allResources); 36 | } 37 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/EnumerableEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace AzureDiagrams.Resources; 5 | 6 | public static class EnumerableEx 7 | { 8 | /// 9 | /// Not lazy. 10 | /// 11 | /// 12 | /// 13 | /// 14 | public static void ForEach(this IEnumerable items, Action action) 15 | { 16 | foreach (var item in items) action(item); 17 | } 18 | public static void ForEach(this IEnumerable items, Action action) 19 | { 20 | var idx = 0; 21 | foreach (var item in items) action(idx++, item); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/EventGridDomain.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AzureDiagrams.Resources.Retrievers.Custom; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources; 9 | 10 | public class EventGridDomain : AzureResource, ICanBeAccessedViaAHostName 11 | { 12 | public override string Image => "img/lib/azure2/integration/Event_Grid_Domains.svg"; 13 | 14 | public Dictionary Subscriptions { get; set; } = default!; 15 | 16 | public string HostName { get; private set; } = default!; 17 | 18 | public string[] Topics { get; private set; } = default!; 19 | 20 | public bool CanIAccessYouOnThisHostName(string hostname) 21 | { 22 | return HostName.Equals(hostname, StringComparison.InvariantCultureIgnoreCase); 23 | } 24 | 25 | public override Task Enrich(JObject full, Dictionary additionalResources) 26 | { 27 | Topics = additionalResources[EventGridDomainRetriever.Topics]! 28 | ["value"]!.Select(x => x!.Value("name")!).ToArray(); 29 | 30 | Subscriptions = Topics.ToDictionary(t => t, t => additionalResources[$"{t}-subscriptions"]!); 31 | 32 | HostName = full["properties"]!.Value("endpoint")!.GetHostNameFromUrlString(); 33 | 34 | return base.Enrich(full, additionalResources); 35 | } 36 | 37 | public override IEnumerable DiscoverNewNodes(List azureResources) 38 | { 39 | return Topics.Select(x => 40 | { 41 | var eventGridTopic = new EventGridTopic 42 | { 43 | Id = $"{Id}/topics/{x}", 44 | Name = x, 45 | Subscriptions = Subscriptions[x] 46 | }; 47 | OwnsResource(eventGridTopic); 48 | return eventGridTopic; 49 | }).ToArray(); 50 | } 51 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/EventGridTopic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AzureDiagrams.Resources.Retrievers.Custom; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources; 9 | 10 | public class EventGridTopic : AzureResource 11 | { 12 | public JObject? Subscriptions { get; internal set; } 13 | public override string Image => "img/lib/azure2/integration/Event_Grid_Topics.svg"; 14 | 15 | public override Task Enrich(JObject full, Dictionary additionalResources) 16 | { 17 | Subscriptions = additionalResources[EventGridTopicRetriever.Subscriptions]; 18 | return base.Enrich(full, additionalResources); 19 | } 20 | 21 | public override void BuildRelationships(IEnumerable allResources) 22 | { 23 | Subscriptions?["value"]!.ForEach(s => HandleSubscription(s["properties"]!, allResources)); 24 | base.BuildRelationships(allResources); 25 | } 26 | 27 | private void HandleSubscription(JToken jt, IEnumerable allResources) 28 | { 29 | switch (jt["destination"]!.Value("endpointType")) 30 | { 31 | case "EventHub": 32 | case "AzureFunction": 33 | case "ServiceBus": 34 | case "ServiceTopic": 35 | case "StorageQueue": 36 | case "HybridConnection": 37 | HandleResourceSubscription(jt, allResources); 38 | break; 39 | case "WebHook": 40 | HandleUrlSubscription(jt, allResources); 41 | break; 42 | } 43 | } 44 | 45 | private void HandleUrlSubscription(JToken jt, IEnumerable allResources) 46 | { 47 | var hostName = jt["destination"]!["properties"]!.Value("endpointBaseUrl")!.GetHostNameFromUrlString(); 48 | var resource = allResources.OfType() 49 | .SingleOrDefault(x => x.CanIAccessYouOnThisHostName(hostName)); 50 | if (resource != null) 51 | { 52 | CreateFlowTo((AzureResource)resource, "subscription", Plane.Runtime); 53 | } 54 | } 55 | 56 | private void HandleResourceSubscription(JToken jt, IEnumerable allResources) 57 | { 58 | var resourceId = jt["destination"]!["properties"]!.Value("resourceId")!; 59 | var resource = 60 | allResources.SingleOrDefault(x => resourceId.StartsWith(x.Id, StringComparison.InvariantCultureIgnoreCase)); 61 | if (resource != null) 62 | { 63 | CreateFlowTo(resource, "subscription", Plane.Runtime); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/EventHub.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public class EventHub : AzureResource 4 | { 5 | public override string Image => "img/lib/azure2/analytics/Event_Hubs.svg"; 6 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Firewall.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources; 7 | 8 | public class Firewall : AzureResource, ICanInjectIntoASubnet, ICanExposePublicIPAddresses 9 | { 10 | private IpConfigurations _ipConfigurations = default!; 11 | private IpConfigurations _mgmtIpConfigurations = default!; 12 | public override string Image => "img/lib/azure2/networking/Firewalls.svg"; 13 | 14 | public override Task Enrich(JObject full, Dictionary additionalResources) 15 | { 16 | _ipConfigurations = new IpConfigurations(full); 17 | _mgmtIpConfigurations = new IpConfigurations(full, "managementIpConfiguration"); 18 | return base.Enrich(full, additionalResources); 19 | } 20 | public string[] PublicIpAddresses => _ipConfigurations.PublicIpAddresses.Concat(_mgmtIpConfigurations.PublicIpAddresses).ToArray(); 21 | public string[] SubnetIdsIAmInjectedInto => _ipConfigurations.SubnetAttachments; //technically there may be another subnet associated - the mgmt one. But I can only really display one on the diagram without overcomplicating things. 22 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/FlowEmphasis.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureDiagrams.Resources; 4 | 5 | [Flags] 6 | public enum Plane 7 | { 8 | Diagnostics = 1, 9 | Runtime = 2, 10 | Identity = 4, 11 | Inferred = 8, 12 | All = Diagnostics | Runtime | Identity | Inferred, 13 | None = 0 14 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/FlowEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | public static class FlowEx 8 | { 9 | public static void CreateFlowToHostName( 10 | this AzureResource fromResource, 11 | IEnumerable allResources, 12 | string hostName, 13 | string flowName, 14 | Plane plane) 15 | { 16 | var possibleHosts = allResources.OfType() 17 | .Where(x => x.CanIAccessYouOnThisHostName(hostName)); 18 | var host = possibleHosts.SingleOrDefault(x => !(x is Nic)); 19 | if (host != null) 20 | { 21 | fromResource.CreateLayer7Flow( 22 | allResources, 23 | (AzureResource)host, 24 | flowName, 25 | hn => hn.Contains(hostName, StringComparer.InvariantCultureIgnoreCase), 26 | plane); 27 | } 28 | } 29 | 30 | public static void CreateLayer7Flow( 31 | this AzureResource fromResource, 32 | IEnumerable allResources, 33 | AzureResource connectTo, 34 | string flowName, 35 | Func nicHostNameCheck, 36 | Plane plane) 37 | { 38 | var nics = allResources.OfType().Where(nic => nicHostNameCheck(nic.HostNames)).ToArray(); 39 | 40 | if (fromResource is ICanEgressViaAVnet vnetEgress) 41 | { 42 | var egress = vnetEgress.EgressResource(); 43 | if (egress != fromResource) 44 | { 45 | fromResource.CreateFlowTo(egress, plane); 46 | } 47 | 48 | if (nics.Any()) 49 | { 50 | nics.ForEach(nic => egress.CreateFlowTo(nic, flowName, plane)); 51 | } 52 | else 53 | { 54 | //Assume all traffic going via vnet for simplicity. We can get clever if we want later around public / private IP addresses / introspecting routes, etc. 55 | egress.CreateFlowTo(connectTo, flowName, plane); 56 | } 57 | } 58 | else 59 | { 60 | //direct flow to the resource (no vnet integration). 61 | //If we found a nic that listened on the hostname then flow to that. 62 | if (nics.Any()) 63 | { 64 | nics.ForEach(nic => fromResource.CreateFlowTo(nic, flowName, plane)); 65 | } 66 | else 67 | { 68 | fromResource.CreateFlowTo(connectTo, flowName, plane); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/FlowEx2.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public static class FlowEx2 4 | { 5 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/IAssociateWithNic.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | internal interface IAssociateWithNic 4 | { 5 | string[] Nics { get; } 6 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ICanBeAccessedViaAHostName.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | internal interface ICanBeAccessedViaAHostName 4 | { 5 | bool CanIAccessYouOnThisHostName(string hostname); 6 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ICanEgressViaAVnet.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | /// 4 | /// If your traffic may flow via a different resource (example might be VNet integration) then implement this. 5 | /// Classes like HostNameDiscoverer will use it when building relationships. 6 | /// 7 | interface ICanEgressViaAVnet 8 | { 9 | AzureResource EgressResource(); 10 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ICanExposePublicIPAddresses.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | internal interface ICanExposePublicIPAddresses 4 | { 5 | string[] PublicIpAddresses { get; } 6 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ICanInjectIntoASubnet.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public interface ICanInjectIntoASubnet 4 | { 5 | string[] SubnetIdsIAmInjectedInto { get; } 6 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ICanWriteToLogAnalyticsWorkspaces.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | internal interface ICanWriteToLogAnalyticsWorkspaces 4 | { 5 | bool DoYouWriteTo(string customerId); 6 | void CreateFlowBackToMe(LogAnalyticsWorkspace workspace); 7 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Identity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AzureDiagrams.Resources; 4 | 5 | public class Identity 6 | { 7 | public string? PrincipalId { get; set; } 8 | public string? TenantId { get; set; } 9 | public string Type { get; set; } = default!; 10 | public Dictionary? UserAssignedIdentities { get; set; } 11 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/IotHub.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public class IotHub : AzureResource 4 | { 5 | public override string Image => "img/lib/azure2/iot/IoT_Hub.svg"; 6 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/IpConfigurations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | public class IpConfigurations 8 | { 9 | public IpConfigurations() 10 | { 11 | } 12 | 13 | public static IpConfigurations ForPrivateEndpoint(string ipAddress, string subnetId, string hostName) 14 | { 15 | return new IpConfigurations() 16 | { 17 | PrivateIpAddresses = new[] { ipAddress }, 18 | SubnetAttachments = new[] { subnetId }, 19 | HostNames = new[] { hostName } 20 | }; 21 | } 22 | 23 | public IpConfigurations(JObject jObject, string propertyName = "ipConfigurations") 24 | { 25 | var ipConfigurations = jObject["properties"]![propertyName]; 26 | if (ipConfigurations?.Type == JTokenType.Object) 27 | { 28 | ipConfigurations = new JArray(ipConfigurations); 29 | } 30 | 31 | PublicIpAddresses = ipConfigurations? 32 | .Select(x => 33 | x["properties"]!["publicIPAddress"] != null 34 | ? x["properties"]!["publicIPAddress"]!.Value("id")!.ToLowerInvariant() 35 | : null) 36 | .Where(x => x != null) 37 | .Select(x => x!.ToLowerInvariant()) 38 | .ToArray() ?? []; 39 | 40 | PrivateIpAddresses = ipConfigurations? 41 | .Select(x => x["properties"]!.Value("privateIPAddress")) 42 | .Where(x => x != null) 43 | .Select(x => x!) 44 | .ToArray() ?? []; 45 | 46 | SubnetAttachments = ipConfigurations? 47 | .Select(x => x["properties"]!["subnet"]?.Value("id")!.ToLowerInvariant()) 48 | .Where(x => x != null) 49 | .Select(x => x!) 50 | .ToArray() ?? []; 51 | 52 | HostNames = ipConfigurations? 53 | .SelectMany(x => 54 | x["properties"]!["privateLinkConnectionProperties"]?["fqdns"]?.Values() ?? 55 | Array.Empty()) 56 | .Select(x => x!.ToLowerInvariant()) 57 | .ToArray() ?? []; 58 | } 59 | 60 | public string[] PrivateIpAddresses { get; set; } 61 | 62 | public string[] SubnetAttachments { get; set; } 63 | 64 | public string[] HostNames { get; set; } 65 | 66 | public string[] PublicIpAddresses { get; set; } 67 | 68 | public bool CanIAccessYouOnThisHostName(string hostname) 69 | { 70 | return HostNames.Contains(hostname.ToLowerInvariant()); 71 | } 72 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/KeyVault.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources; 7 | 8 | internal class KeyVault : AzureResource, ICanBeAccessedViaAHostName 9 | { 10 | public override string Image => "img/lib/azure2/security/Key_Vaults.svg"; 11 | 12 | public bool CanIAccessYouOnThisHostName(string hostname) 13 | { 14 | return VaultUri.Equals(hostname, StringComparison.InvariantCultureIgnoreCase); 15 | } 16 | 17 | public override Task Enrich(JObject jObject, Dictionary additionalResources) 18 | { 19 | VaultUri = jObject["properties"]!.Value("vaultUri")!.GetHostNameFromUrlString(); 20 | return base.Enrich(jObject, additionalResources); 21 | } 22 | 23 | private string VaultUri { get; set; } = default!; 24 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/LoadBalancer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class LoadBalancer : AzureResource, ICanInjectIntoASubnet, ICanExposePublicIPAddresses 10 | { 11 | private IpConfigurations _frontendIpConfigurations = default!; 12 | private string[] _backendNics = default!; 13 | public override string Image => "img/lib/azure2/networking/Load_Balancers.svg"; 14 | 15 | public string[] PublicIpAddresses => _frontendIpConfigurations.PublicIpAddresses; 16 | 17 | public string[] SubnetIdsIAmInjectedInto => _frontendIpConfigurations.SubnetAttachments; 18 | 19 | public override Task Enrich(JObject full, Dictionary additionalResources) 20 | { 21 | _frontendIpConfigurations = new IpConfigurations(full, "frontendIPConfigurations"); 22 | _backendNics = 23 | full["properties"]! 24 | ["backendAddressPools"]! 25 | .SelectMany(x => 26 | x["properties"]!["loadBalancerBackendAddresses"]? 27 | .Select(lbba => 28 | lbba["properties"]! 29 | ["networkInterfaceIPConfiguration"]? 30 | .Value("id") ?? null) ?? Array.Empty() 31 | ) 32 | .Where(x => x != null) 33 | .Select(x => string.Join('/', x.Split("/")[0..^2]) 34 | ) 35 | .ToArray(); 36 | 37 | return base.Enrich(full, additionalResources); 38 | } 39 | 40 | 41 | public override void BuildRelationships(IEnumerable allResources) 42 | { 43 | _backendNics.ForEach(x => 44 | CreateFlowTo( 45 | allResources.OfType().Single(nic => nic.Id.Equals(x, StringComparison.InvariantCultureIgnoreCase)), 46 | "lb", Plane.All)); 47 | base.BuildRelationships(allResources); 48 | } 49 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/LogAnalyticsWorkspace.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources; 7 | 8 | internal class LogAnalyticsWorkspace : AzureResource 9 | { 10 | public override string Image => "img/lib/azure2/analytics/Log_Analytics_Workspaces.svg"; 11 | public string CustomerId { get; private set; } = default!; 12 | 13 | public override Task Enrich(JObject full, Dictionary additionalResources) 14 | { 15 | CustomerId = full["properties"]!.Value("customerId")!; 16 | return base.Enrich(full, additionalResources); 17 | } 18 | 19 | public override void BuildRelationships(IEnumerable allResources) 20 | { 21 | allResources.OfType().Where(x => x.DoYouWriteTo(CustomerId)) 22 | .ForEach(x => x.CreateFlowBackToMe(this)); 23 | base.BuildRelationships(allResources); 24 | } 25 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/LogicApp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class LogicApp : AzureResource, ICanBeAccessedViaAHostName 10 | { 11 | public override string Image => "img/lib/azure2/integration/Logic_Apps.svg"; 12 | 13 | public string? AccessEndpoint { get; set; } = default!; 14 | public string[] Connections { get; set; } = default!; 15 | 16 | public override Task Enrich(JObject full, Dictionary additionalResources) 17 | { 18 | Connections = full["properties"]!["parameters"]?["$connections"]?["value"]? 19 | .ToObject>()? 20 | .Values.Select(x => x.Value("connectionId")!).ToArray() ?? Array.Empty(); 21 | 22 | AccessEndpoint = full["properties"]!.Value("accessEndpoint"); 23 | 24 | return base.Enrich(full, additionalResources); 25 | } 26 | 27 | public bool CanIAccessYouOnThisHostName(string hostname) 28 | { 29 | return AccessEndpoint?.Contains(hostname.ToLowerInvariant(), StringComparison.InvariantCultureIgnoreCase) ?? 30 | false; 31 | } 32 | 33 | public override void BuildRelationships(IEnumerable allResources) 34 | { 35 | Connections.Select(c => 36 | allResources.OfType() 37 | .Single(x => x.Id.Equals(c, StringComparison.InvariantCultureIgnoreCase))) 38 | .ForEach(c => CreateFlowTo(c, "uses", Plane.Runtime)); 39 | base.BuildRelationships(allResources); 40 | } 41 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/LogicAppConnector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | public class LogicAppConnector : AzureResource 8 | { 9 | public override string Image => "img/lib/azure2/general/Input_Output.svg"; 10 | 11 | public override Task Enrich(JObject full, Dictionary additionalResources) 12 | { 13 | //TODO - is there a way to get info about the connection (api host name, or something like that?) 14 | return base.Enrich(full, additionalResources); 15 | } 16 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ManagedSqlDatabase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | internal class ManagedSqlDatabase : AzureResource 10 | { 11 | public override string Image => "img/lib/azure2/databases/SQL_Database.svg"; 12 | 13 | public string ServerId { get; private set; } = default!; 14 | 15 | public override Task Enrich(JObject full, Dictionary additionalResources) 16 | { 17 | ServerId = string.Join('/', Id.Split('/')[..^2]); 18 | return base.Enrich(full, additionalResources); 19 | } 20 | 21 | public override void BuildRelationships(IEnumerable allResources) 22 | { 23 | var server = allResources.OfType().SingleOrDefault(x => 24 | string.Compare(ServerId, x.Id, StringComparison.InvariantCultureIgnoreCase) == 0); 25 | if (server != null) server.DiscoveredDatabase(this); 26 | base.BuildRelationships(allResources); 27 | } 28 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ManagedSqlServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources; 7 | 8 | internal class ManagedSqlServer : AzureResource, ICanBeAccessedViaAHostName 9 | { 10 | public override bool IsPureContainer => false; 11 | public override string Image => "img/lib/azure2/databases/SQL_Server.svg"; 12 | 13 | public string Hostname { get; set; } 14 | 15 | public override Task Enrich(JObject full, Dictionary additionalResources) 16 | { 17 | Hostname = full["properties"]!.Value("fullyQualifiedDomainName")!; 18 | return base.Enrich(full, additionalResources); 19 | } 20 | 21 | public void DiscoveredDatabase(ManagedSqlDatabase managedSqlDatabase) 22 | { 23 | OwnsResource(managedSqlDatabase); 24 | } 25 | 26 | public bool CanIAccessYouOnThisHostName(string hostname) 27 | { 28 | //contains to enable more specific connections like 'tcp:,1433' 29 | return hostname.Contains(Hostname, StringComparison.InvariantCultureIgnoreCase); 30 | } 31 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/NSG.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class NSG : AzureResource 10 | { 11 | private string[] _networkInterfacesBoundTo = default!; 12 | private string[] _subnetsBoundTo = default!; 13 | 14 | public override string Image => "img/lib/azure2/networking/Network_Security_Groups.svg"; 15 | 16 | public override Task Enrich(JObject full, Dictionary additionalResources) 17 | { 18 | if (full["properties"]!["networkInterfaces"] != null) 19 | _networkInterfacesBoundTo = 20 | full["properties"]!["networkInterfaces"]!.Select(x => x.Value("id")!).ToArray(); 21 | else 22 | _networkInterfacesBoundTo = Array.Empty(); 23 | 24 | if (full["properties"]!["subnets"] != null) 25 | _subnetsBoundTo = 26 | full["properties"]!["subnets"]!.Select(x => x.Value("id")!).ToArray(); 27 | else 28 | _subnetsBoundTo = Array.Empty(); 29 | 30 | return Task.CompletedTask; 31 | } 32 | 33 | public override void BuildRelationships(IEnumerable allResources) 34 | { 35 | _subnetsBoundTo.ForEach(x => 36 | allResources.OfType().Single(vNet => vNet.Id == string.Join('/', x.Split('/')[..^2])) 37 | .AssignNsg(this, x.Split('/')[^1])); 38 | _networkInterfacesBoundTo.ForEach(x => allResources.OfType().Single(nic => nic.Id == x).AssignNsg(this)); 39 | base.BuildRelationships(allResources); 40 | } 41 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/NetworkProfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class NetworkProfile : AzureResource 10 | { 11 | private string[] _linkedContainers = default!; 12 | public string[] SubnetIds { get; private set; } = default!; 13 | 14 | public override Task Enrich(JObject full, Dictionary additionalResources) 15 | { 16 | _linkedContainers = full["properties"]!["containerNetworkInterfaces"] 17 | ?.Select(x => x["properties"]!["container"]?.Value("id")) 18 | .Where(x => x != null).Select(x => x!).Distinct().ToArray() ?? Array.Empty(); 19 | 20 | SubnetIds = full["properties"]!["containerNetworkInterfaceConfigurations"] 21 | ?.SelectMany(x => 22 | x["properties"]!["ipConfigurations"]!.Select(y => 23 | y["properties"]!["subnet"]!.Value("id")!)).Distinct().ToArray() ?? 24 | Array.Empty(); 25 | 26 | return base.Enrich(full, additionalResources); 27 | } 28 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Nic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace AzureDiagrams.Resources; 10 | 11 | [DebuggerDisplay("{Type}/{Name}")] 12 | public class Nic : AzureResource, ICanInjectIntoASubnet, ICanExposePublicIPAddresses, ICanBeAccessedViaAHostName 13 | { 14 | public override string Image => "img/lib/azure2/networking/Network_Interfaces.svg"; 15 | 16 | private IpConfigurations _ipConfigurations = default!; 17 | public PrivateEndpoint? ConnectedPrivateEndpoint { get; private set; } 18 | 19 | public string[] HostNames => _ipConfigurations.HostNames; 20 | 21 | [JsonConstructor] 22 | public Nic() { } 23 | 24 | public Nic(string id, IpConfigurations ipConfigurations) 25 | { 26 | Id = id; 27 | _ipConfigurations = ipConfigurations; 28 | } 29 | 30 | public bool CanIAccessYouOnThisHostName(string hostname) 31 | { 32 | return _ipConfigurations.CanIAccessYouOnThisHostName(hostname); 33 | } 34 | 35 | public string[] PublicIpAddresses => _ipConfigurations.PublicIpAddresses; 36 | 37 | public string[] SubnetIdsIAmInjectedInto => _ipConfigurations.SubnetAttachments.Distinct().ToArray(); 38 | 39 | public override void BuildRelationships(IEnumerable allResources) 40 | { 41 | ConnectedPrivateEndpoint = allResources.OfType().SingleOrDefault(x => x.Nics.Contains(Id)); 42 | if (ConnectedPrivateEndpoint != null) CreateFlowTo(ConnectedPrivateEndpoint, Plane.All); 43 | allResources.OfType().Where(x => x.Nics.Contains(Id, StringComparer.InvariantCultureIgnoreCase)).ForEach(vm => CreateFlowTo(vm, Plane.All)); 44 | base.BuildRelationships(allResources); 45 | } 46 | 47 | public override Task Enrich(JObject jObject, Dictionary additionalResources) 48 | { 49 | _ipConfigurations = new IpConfigurations(jObject); 50 | return Task.CompletedTask; 51 | } 52 | 53 | public void AssignNsg(NSG nsg) 54 | { 55 | OwnsResource(nsg); 56 | } 57 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/P2S.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | public class P2S : AzureResource 8 | { 9 | public override string Image => "img/lib/mscae/VPN_Gateway.svg"; 10 | 11 | public override Task Enrich(JObject full, Dictionary additionalResources) 12 | { 13 | VHubId = full["properties"]!["virtualHub"]?.Value("id"); 14 | return base.Enrich(full, additionalResources); 15 | } 16 | 17 | public string? VHubId { get; set; } 18 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/PIP.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | internal class PIP : AzureResource 8 | { 9 | public override string Image => "img/lib/azure2/networking/Public_IP_Addresses.svg"; 10 | 11 | public override void BuildRelationships(IEnumerable allResources) 12 | { 13 | allResources.OfType() 14 | .Where(x => x.PublicIpAddresses.Any(x => 15 | string.Compare(Id, x, StringComparison.InvariantCultureIgnoreCase) == 0)) 16 | .ForEach(x => CreateFlowTo((AzureResource)x, "From Public Internet", Plane.Runtime)); 17 | base.BuildRelationships(allResources); 18 | } 19 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/PrivateDnsZone.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public class PrivateDnsZone : AzureResource 4 | { 5 | public override string Image => "img/lib/azure2/networking/DNS_Zones.svg"; 6 | } 7 | -------------------------------------------------------------------------------- /AzureDiagrams/Resources/PrivateEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using AzureDiagrams.Resources.Retrievers.Extensions; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace AzureDiagrams.Resources; 11 | 12 | [DebuggerDisplay("{Type}/{Name}")] 13 | public class PrivateEndpoint : AzureResource, IAssociateWithNic, ICanInjectIntoASubnet 14 | { 15 | public override string Image => "img/lib/azure2/networking/Private_Link.svg"; 16 | 17 | public string[] CustomHostNames { get; private set; } = default!; 18 | 19 | public AzureResource? ResourceAccessedByMe { get; set; } 20 | 21 | public string[] Nics { get; private set; } = default!; 22 | 23 | public string[] SubnetIdsIAmInjectedInto { get; private set; } = default!; 24 | 25 | public PrivateEndpoint(string id, string[] subnets, string[] nics, string[] customHostNames) 26 | { 27 | Id = id; 28 | SubnetIdsIAmInjectedInto = subnets; 29 | Nics = nics; 30 | CustomHostNames = customHostNames; 31 | } 32 | 33 | [JsonConstructor] 34 | public PrivateEndpoint() { } 35 | 36 | 37 | public override void BuildRelationships(IEnumerable allResources) 38 | { 39 | var accessedByThisPrivateEndpoint = allResources 40 | .Select(x => new 41 | { 42 | Resource = x, 43 | PrivateEndpointInformation = x.Extensions.OfType().SingleOrDefault() 44 | }) 45 | .Where(x => x.PrivateEndpointInformation != null) 46 | .Where(x => x.PrivateEndpointInformation!.AccessedViaPrivateEndpoint(this)) 47 | .ToArray(); 48 | 49 | accessedByThisPrivateEndpoint.ForEach(x => CreateFlowTo(x.Resource, Plane.All)); 50 | 51 | //Grab hold of the resource accessed by this. Should never be more than 1. Write a warning out if we see more though 52 | ResourceAccessedByMe = accessedByThisPrivateEndpoint.First().Resource; 53 | if (!accessedByThisPrivateEndpoint.Any()) Console.WriteLine($"WARNING: Private endpoint {Id} has no backing resource. Be sure to include its resource group."); 54 | if (accessedByThisPrivateEndpoint.Length > 1) Console.WriteLine($"WARNING: Private endpoint {Id} has no backing resource."); 55 | 56 | base.BuildRelationships(allResources); 57 | } 58 | 59 | public override Task Enrich(JObject jObject, Dictionary additionalResources) 60 | { 61 | Nics = jObject["properties"]!["networkInterfaces"]!.Select(x => x.Value("id")!).ToArray(); 62 | CustomHostNames = jObject["properties"]!["customDnsConfigs"]!.Select(x => x.Value("fqdn")!).ToArray(); 63 | SubnetIdsIAmInjectedInto = new[] { jObject["properties"]!["subnet"]!.Value("id")! }; 64 | 65 | return Task.CompletedTask; 66 | } 67 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/PublicIpAddresses.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace AzureDiagrams.Resources; 5 | 6 | public class PublicIpAddresses : AzureResource 7 | { 8 | public override bool IsPureContainer => true; 9 | 10 | public override void BuildRelationships(IEnumerable allResources) 11 | { 12 | allResources.OfType().Where(x => x.Location == Location).ForEach(OwnsResource); 13 | base.BuildRelationships(allResources); 14 | } 15 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Region.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | public class Region : AzureResource 8 | { 9 | public override bool IsPureContainer => true; 10 | public override string Fill => "#F5F5F5"; 11 | 12 | public Region(string? location) 13 | { 14 | Location = location ?? "global"; 15 | Name = Location; 16 | Id = $"azdatacentre-{location}"; 17 | } 18 | 19 | public override void BuildRelationships(IEnumerable allResources) 20 | { 21 | var azureResources = allResources.Except(new [] {this}).Where(x => !x.ContainedByAnotherResource).Where(x => (x.Location ?? "global").Equals(Location, StringComparison.InvariantCultureIgnoreCase)); 22 | azureResources.ForEach(OwnsResource); 23 | base.BuildRelationships(allResources); 24 | } 25 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/RelationshipHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Common; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class RelationshipHelper 10 | { 11 | private readonly string[] _potentialConnectionStrings; 12 | private static readonly Regex KvRegex = new Regex(@"^\@Microsoft\.KeyVault\(VaultName\=(.*?);"); 13 | private static readonly Regex HostNameLikeRegex = new Regex(@"\/\/(([A-Za-z0-9-]{2,100}\.?)+)\b"); 14 | private static readonly Regex SimpleHostNameLikeRegex = new Regex(@"^[A-Za-z0-9-_]{2,100}$"); 15 | 16 | private (string storageName, string storageSuffix)[] _connectedStorageAccounts = default!; 17 | private (string serverName, string database)[] _databaseConnections = default!; 18 | private string[] _keyVaultReferences = default!; 19 | private string[] _hostNamesAccessedInAppSettings = default!; 20 | 21 | public RelationshipHelper(string[] potentialConnectionStrings) 22 | { 23 | _potentialConnectionStrings = potentialConnectionStrings; 24 | } 25 | 26 | public void Discover() 27 | { 28 | _connectedStorageAccounts = _potentialConnectionStrings 29 | .OfType() 30 | .Where(appSetting => appSetting.Contains("DefaultEndpointsProtocol") && 31 | appSetting.Contains("AccountName")) 32 | .Select(x => 33 | { 34 | var parts = x!.Split(';') 35 | .Where(part => !string.IsNullOrEmpty(part)) 36 | .Select(part => new KeyValuePair(part.Split('=')[0].ToLowerInvariant(), 37 | part.Split('=')[1].ToLowerInvariant())) 38 | .ToDictionary(part => part.Key, part => part.Value); 39 | 40 | return (parts["accountname"], 41 | "." + (parts.ContainsKey("endpointsuffix") ? parts["endpointsuffix"] : "core.windows.net")); 42 | }) 43 | .Distinct() 44 | .ToArray(); 45 | 46 | _databaseConnections = _potentialConnectionStrings 47 | .Where(appSetting => (appSetting.Contains("Data Source=") || appSetting.Contains("Server")) && 48 | (appSetting.Contains("Initial Catalog=") || appSetting.Contains("Database="))) 49 | .Select(x => 50 | { 51 | var csb = new DbConnectionStringBuilder 52 | { 53 | ConnectionString = x 54 | }; 55 | return 56 | ((string)(csb.ContainsKey("Data Source") ? csb["Data Source"] : csb["Server"]), 57 | (string)(csb.ContainsKey("Initial Catalog") ? csb["Initial Catalog"] : csb["Database"])); 58 | }).ToArray(); 59 | 60 | _keyVaultReferences = _potentialConnectionStrings 61 | .Select(x => KvRegex.Match(x)) 62 | .Where(x => x.Success) 63 | .Select(x => x.Groups[1].Captures[0].Value) 64 | .ToArray(); 65 | 66 | _hostNamesAccessedInAppSettings = _potentialConnectionStrings 67 | .Select(x => 68 | { 69 | if (Uri.TryCreate(x, UriKind.Absolute, out var uri)) return uri.Host; 70 | 71 | //try look for a URL like pattern in the string 72 | var match = HostNameLikeRegex.Match(x); 73 | if (match.Success) 74 | { 75 | return match.Groups[1].Value; 76 | } 77 | 78 | return string.Empty; 79 | } 80 | ) 81 | .Where(x => !string.IsNullOrEmpty(x)) 82 | .Union(_potentialConnectionStrings.Where(x => SimpleHostNameLikeRegex.IsMatch(x))) 83 | .Distinct() 84 | .ToArray(); 85 | } 86 | 87 | /// 88 | /// Build flows 89 | /// 90 | /// 91 | /// If you have a public service that has vnet integration (e.g. App in app-service-plan) then you may optionally access 92 | /// some resources via a 93 | /// 94 | /// 95 | /// 96 | /// 97 | public void BuildRelationships(AzureResource from, IEnumerable allResources) 98 | { 99 | foreach (var storageAccount in _connectedStorageAccounts) 100 | { 101 | var storage = allResources.OfType() 102 | .SingleOrDefault(x => x.Name.ToLowerInvariant() == storageAccount.storageName); 103 | if (storage != null) 104 | { 105 | from.CreateLayer7Flow(allResources, storage, "uses", 106 | hns => hns.Any(hn => 107 | hn.StartsWith(storageAccount.storageName) && hn.EndsWith(storageAccount.storageSuffix)), 108 | Plane.Runtime); 109 | } 110 | } 111 | 112 | foreach (var databaseConnection in _databaseConnections) 113 | { 114 | //TODO check server name as-well 115 | var server = allResources.OfType().SingleOrDefault(x => 116 | x.CanIAccessYouOnThisHostName(databaseConnection.serverName)); 117 | 118 | // string.Compare(x.Name, databaseConnection.serverName, StringComparison.InvariantCultureIgnoreCase) == 0); 119 | 120 | if (server != null) 121 | { 122 | from.CreateLayer7Flow(allResources, server, "sql", 123 | hns => hns.Any(hn => hn.StartsWith(server.Name)), Plane.Runtime); 124 | } 125 | } 126 | 127 | foreach (var keyVaultReference in _keyVaultReferences) 128 | { 129 | //TODO KeyVault via private endpoint. Needs a generic way to look for a host that is accessed via private endpoints. 130 | 131 | //TODO check server name as-well 132 | var keyVault = allResources.OfType().SingleOrDefault(x => 133 | string.Compare(x.Name, keyVaultReference, StringComparison.InvariantCultureIgnoreCase) == 0); 134 | if (keyVault != null) 135 | { 136 | from.CreateLayer7Flow(allResources, keyVault, "secrets", 137 | hns => hns.Any(hn => keyVault.CanIAccessYouOnThisHostName(hn)), Plane.Runtime); 138 | } 139 | } 140 | 141 | allResources.OfType() 142 | .Where(x => _hostNamesAccessedInAppSettings.Any(x.CanIAccessYouOnThisHostName)) 143 | .ForEach(x => 144 | { 145 | from.CreateLayer7Flow(allResources, (AzureResource)x, "calls", 146 | hns => hns.Any(x.CanIAccessYouOnThisHostName), Plane.Runtime); 147 | }); 148 | } 149 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ResourceLink.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace AzureDiagrams.Resources; 3 | 4 | public class ResourceLink 5 | { 6 | public ResourceLink(AzureResource @from, AzureResource to, string? details, Plane plane) 7 | { 8 | From = from; 9 | To = to; 10 | Details = details; 11 | Plane = plane; 12 | } 13 | 14 | public AzureResource From { get; } 15 | public AzureResource To { get; } 16 | public string? Details { get; } 17 | 18 | public Plane Plane { get; } 19 | 20 | public void MakeTwoWay() 21 | { 22 | IsTwoWay = true; 23 | } 24 | 25 | public bool IsTwoWay { get; private set; } 26 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/AzureHttpEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | 7 | namespace AzureDiagrams.Resources.Retrievers; 8 | 9 | public static class AzureHttpEx 10 | { 11 | public static async IAsyncEnumerable GetAzResourcesAsync(this HttpClient httpClient, string uri, 12 | string apiVersion) 13 | { 14 | ArmClient.AzureList? results = null; 15 | do 16 | { 17 | results = results == null 18 | ? await GetAzResourceAsync>(httpClient, uri, apiVersion, HttpMethod.Get) 19 | : await GetAzResourceAsync>(httpClient, 20 | httpClient.BaseAddress!.MakeRelativeUri(new Uri(results.NextLink!)).ToString(), null, 21 | HttpMethod.Get); 22 | 23 | foreach (var item in results.Value) 24 | { 25 | yield return item; 26 | } 27 | } while (results.NextLink != null); 28 | } 29 | 30 | public static async Task GetAzResourceAsync(this HttpClient httpClient, string uri, string? apiVersion, 31 | HttpMethod? method = null) 32 | { 33 | var apiVersionQueryString = 34 | apiVersion == null ? "" : (uri.Contains("?") ? "&" : "?") + $"api-version={apiVersion}"; 35 | var resourceUri = $"{uri}{apiVersionQueryString}"; 36 | 37 | var request = new HttpRequestMessage(method ?? HttpMethod.Get, resourceUri); 38 | var httpResponseMessage = await httpClient.SendAsync(request); 39 | var responseContent = await httpResponseMessage.Content.ReadAsStringAsync(); 40 | 41 | httpResponseMessage.EnsureSuccessStatusCode(); 42 | var response = JsonConvert.DeserializeObject(responseContent)!; 43 | return response; 44 | } 45 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/BasicAzureResourceInfo.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources.Retrievers; 2 | 3 | public class BasicAzureResourceInfo 4 | { 5 | public string Id { get; init; } = default!; 6 | public string Type { get; init; } = default!; 7 | public string Name { get; init; } = default!; 8 | public string Location { get; set; } = default!; 9 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Custom/ApimServiceResourceRetriever.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using AzureDiagrams.Resources.Retrievers.Extensions; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources.Retrievers.Custom; 7 | 8 | /// 9 | /// Difficult to dive into all operations. So for the moment it only looks at Backends to build relationships. 10 | /// 11 | public class ApimServiceResourceRetriever : ResourceRetriever 12 | { 13 | public const string BackendList = "backends"; 14 | 15 | public ApimServiceResourceRetriever(JObject basicAzureResourceJObject) : base(basicAzureResourceJObject, 16 | "2021-08-01", true, 17 | extensions: new IResourceExtension[] 18 | { new DiagnosticsExtensions(), new PrivateEndpointExtensions(), new ManagedIdentityExtension() }) 19 | { 20 | } 21 | 22 | protected override IEnumerable<(string key, HttpMethod method, string suffix, string? version)> 23 | AdditionalResources() 24 | { 25 | yield return (BackendList, HttpMethod.Get, BackendList, null); 26 | } 27 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Custom/AppResourceRetriever.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using AzureDiagrams.Resources.Retrievers.Extensions; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources.Retrievers.Custom; 7 | 8 | public class AppResourceRetriever : ResourceRetriever 9 | { 10 | public const string ConfigAppSettingsList = "config/appSettings/list"; 11 | public const string ConnectionStringSettingsList = "config/connectionStrings/list"; 12 | 13 | public AppResourceRetriever(JObject basicAzureResourceJObject) : base(basicAzureResourceJObject, "2021-01-15", true, 14 | new IResourceExtension[] { new PrivateEndpointExtensions(), new ManagedIdentityExtension() }) 15 | { 16 | } 17 | 18 | protected override IEnumerable<(string key, HttpMethod method, string suffix, string? version)> 19 | AdditionalResources() 20 | { 21 | yield return (ConfigAppSettingsList, HttpMethod.Post, ConfigAppSettingsList, null); 22 | yield return (ConnectionStringSettingsList, HttpMethod.Post, ConnectionStringSettingsList, null); 23 | } 24 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Custom/AzureDataFactoryRetriever.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using AzureDiagrams.Resources.Retrievers.Extensions; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources.Retrievers.Custom; 7 | 8 | public class AzureDataFactoryRetriever : ResourceRetriever 9 | { 10 | public const string LinkedServices = "linkedservices"; 11 | 12 | public AzureDataFactoryRetriever(JObject basicAzureResourceJObject) : base(basicAzureResourceJObject, 13 | fetchFullResource: true, apiVersion: "2018-06-01", 14 | extensions: new IResourceExtension[] 15 | { new DiagnosticsExtensions(), new PrivateEndpointExtensions(), new ManagedIdentityExtension() }) 16 | { 17 | } 18 | 19 | protected override IEnumerable<(string key, HttpMethod method, string suffix, string? version)> 20 | AdditionalResources() 21 | { 22 | yield return (LinkedServices, HttpMethod.Get, LinkedServices, null); 23 | } 24 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Custom/EventGridDomainRetriever.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using AzureDiagrams.Resources.Retrievers.Extensions; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources.Retrievers.Custom; 8 | 9 | public class EventGridDomainRetriever : ResourceRetriever 10 | { 11 | public const string Topics = "topics"; 12 | 13 | public EventGridDomainRetriever(JObject basicAzureResourceJObject) : base(basicAzureResourceJObject, 14 | fetchFullResource: true, apiVersion: "2021-06-01-preview", 15 | extensions: new IResourceExtension[] { new DiagnosticsExtensions(), new PrivateEndpointExtensions(), new ManagedIdentityExtension() }) 16 | { 17 | } 18 | 19 | protected override IEnumerable<(string key, HttpMethod method, string suffix, string? version)> 20 | AdditionalResources() 21 | { 22 | yield return (Topics, HttpMethod.Get, Topics, null); 23 | } 24 | 25 | protected override IEnumerable<(string key, HttpMethod method, string api, string? version)> AdditionalResourcesEnhanced(BasicAzureResourceInfo basicInfo, Dictionary additionalResources, JObject? fullResource) 26 | { 27 | foreach (var topic in additionalResources[Topics]! 28 | ["value"]!.Select(x => x!.Value("name")!)) 29 | { 30 | yield return ($"{topic}-subscriptions", HttpMethod.Get, $"topics/{topic}/providers/Microsoft.EventGrid/eventSubscriptions", null); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Custom/EventGridTopicRetriever.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using AzureDiagrams.Resources.Retrievers.Extensions; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources.Retrievers.Custom; 7 | 8 | public class EventGridTopicRetriever : ResourceRetriever 9 | { 10 | public const string Subscriptions = "subscriptions"; 11 | 12 | public EventGridTopicRetriever(JObject basicAzureResourceJObject) : base(basicAzureResourceJObject, 13 | fetchFullResource: true, apiVersion: "2021-06-01-preview", 14 | extensions: new IResourceExtension[] 15 | { new DiagnosticsExtensions(), new PrivateEndpointExtensions(), new ManagedIdentityExtension() }) 16 | { 17 | } 18 | 19 | protected override IEnumerable<(string key, HttpMethod method, string suffix, string? version)> 20 | AdditionalResources() 21 | { 22 | yield return (Subscriptions, HttpMethod.Get, "providers/Microsoft.EventGrid/eventSubscriptions", null); 23 | } 24 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Custom/SynapseRetriever.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Azure.Core; 8 | using AzureDiagrams.Resources.Retrievers.Extensions; 9 | using Newtonsoft.Json; 10 | using Newtonsoft.Json.Linq; 11 | 12 | namespace AzureDiagrams.Resources.Retrievers.Custom; 13 | 14 | /// 15 | /// Had hoped to fetch linked-services but you query them on a different api, need a different token, and that api might be on a private endpoint... So leaving for now :( 16 | /// 17 | public class SynapseRetriever : ResourceRetriever 18 | { 19 | public const string LinkedServices = "linkedservices"; 20 | private readonly TokenCredential _tokenCredential; 21 | 22 | public SynapseRetriever(JObject basicAzureResourceJObject, TokenCredential tokenCredential) : base( 23 | basicAzureResourceJObject, 24 | fetchFullResource: true, apiVersion: "2021-06-01", 25 | extensions: new IResourceExtension[] 26 | { new DiagnosticsExtensions(), new PrivateEndpointExtensions(), new ManagedIdentityExtension() }) 27 | { 28 | _tokenCredential = tokenCredential; 29 | } 30 | 31 | protected override async Task> AdditionalResourcesCustom( 32 | BasicAzureResourceInfo basicInfo, Dictionary initialResources, JObject? fullResource) 33 | { 34 | var token = await _tokenCredential.GetTokenAsync( 35 | new TokenRequestContext(new[] { "https://dev.azuresynapse.net" }), CancellationToken.None); 36 | var devEndpoint = fullResource!["properties"]!["connectivityEndpoints"]!.Value("dev")!; 37 | var client = new HttpClient(); //TODO - might be nice to pull this out so I'm not newing this up. Minor though. 38 | client.Timeout = TimeSpan.FromSeconds(5); 39 | var msg = new HttpRequestMessage(HttpMethod.Get, 40 | $"{devEndpoint}/linkedServices?api-version=2019-06-01-preview"); 41 | msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); 42 | try 43 | { 44 | var response = await client.SendAsync(msg); 45 | if (response.IsSuccessStatusCode) 46 | { 47 | var responseContent = await response.Content.ReadAsStringAsync(); 48 | return new Dictionary() 49 | { 50 | [LinkedServices] = JsonConvert.DeserializeObject(responseContent)! 51 | }; 52 | } 53 | 54 | Console.ForegroundColor = ConsoleColor.Red; 55 | Console.WriteLine( 56 | $"\tFailed to fetch linked services from {devEndpoint}. Response {response.StatusCode}|{await response.Content.ReadAsStringAsync()}. If Synapse uses Private Endpoints you will need to run this from a location with access."); 57 | Console.ResetColor(); 58 | return new Dictionary(); 59 | } 60 | catch (TimeoutException) 61 | { 62 | Console.ForegroundColor = ConsoleColor.Red; 63 | Console.WriteLine( 64 | $"\tFailed to fetch linked services from {devEndpoint}. Timed out. If Synapse uses Private Endpoints you will need to run this from a location with access."); 65 | Console.ResetColor(); 66 | return new Dictionary(); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Custom/VHubRetriever.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources.Retrievers.Custom; 6 | 7 | public class VHubRetriever : ResourceRetriever 8 | { 9 | public const string VirtualNetworkConnections = "hubVirtualNetworkConnections"; 10 | 11 | public VHubRetriever(JObject basicAzureResourceJObject) : base(basicAzureResourceJObject, "2021-03-01", true) 12 | { 13 | } 14 | 15 | protected override IEnumerable<(string key, HttpMethod method, string suffix, string? version)> AdditionalResources() 16 | { 17 | yield return (VirtualNetworkConnections, HttpMethod.Get, "hubVirtualNetworkConnections", "2021-03-01"); 18 | } 19 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/DiagramException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureDiagrams.Resources.Retrievers; 4 | 5 | public class DiagramException : Exception 6 | { 7 | public DiagramException(string message, Exception inner) : base(message, inner) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Extensions/DiagnosticsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources.Retrievers.Extensions; 9 | 10 | public class DiagnosticsExtensions : IResourceExtension 11 | { 12 | private string? _diagnosticsWorkspaceId; 13 | 14 | public (string key, HttpMethod method, string suffix, string? version)? ApiCall => ("diagnostics", HttpMethod.Get, 15 | "providers/microsoft.insights/diagnosticSettings", "2021-05-01-preview"); 16 | 17 | public Task Enrich(AzureResource resource, JObject raw, Dictionary additionalResources) 18 | { 19 | var workspaces = additionalResources[ApiCall!.Value.key]!["value"]!; 20 | if (workspaces.Any()) _diagnosticsWorkspaceId = workspaces[0]?["properties"]?.Value("workspaceId"); 21 | return Task.CompletedTask; 22 | } 23 | 24 | public void BuildRelationships(AzureResource resource, IEnumerable allResources) 25 | { 26 | if (_diagnosticsWorkspaceId != null) 27 | { 28 | var workspace = allResources.OfType().SingleOrDefault(x => 29 | x.Id.Equals(_diagnosticsWorkspaceId, StringComparison.InvariantCultureIgnoreCase)); 30 | if (workspace != null) resource.CreateFlowTo(workspace, "diagnostics", Plane.Diagnostics); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Extensions/IResourceExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources.Retrievers.Extensions; 7 | 8 | public interface IResourceExtension 9 | { 10 | /// 11 | /// If you need extra data from an API then declare it here 12 | /// 13 | public (string key, HttpMethod method, string suffix, string? version)? ApiCall { get; } 14 | 15 | /// 16 | /// Use the data to build the extension 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | Task Enrich(AzureResource resource, JObject raw, Dictionary additionalResources); 23 | 24 | /// 25 | /// Create any relationships between the resource holding the extension, and other resources 26 | /// 27 | /// 28 | /// 29 | void BuildRelationships(AzureResource resource, IEnumerable allResources); 30 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Extensions/ManagedIdentityExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources.Retrievers.Extensions; 9 | 10 | public class ManagedIdentityExtension : IResourceExtension 11 | { 12 | private Identity? Identity { get; set; } 13 | 14 | public (string key, HttpMethod method, string suffix, string? version)? ApiCall { get; } 15 | 16 | public Task Enrich(AzureResource resource, JObject raw, Dictionary additionalResources) 17 | { 18 | Identity = raw["identity"]?.ToObject(); 19 | return Task.CompletedTask; 20 | } 21 | 22 | public void BuildRelationships(AzureResource resource, IEnumerable allResources) 23 | { 24 | Identity?.UserAssignedIdentities?.Keys.ForEach(i => 25 | allResources.OfType().Where(uami => uami.Id.Equals(i, StringComparison.InvariantCultureIgnoreCase)) 26 | .ForEach(uami => resource.CreateFlowTo(uami, "AAD Identity", Plane.Identity))); 27 | } 28 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/Extensions/PrivateEndpointExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources.Retrievers.Extensions; 9 | 10 | public class PrivateEndpointExtensions : IResourceExtension 11 | { 12 | //Used internally 13 | public PrivateEndpointExtensions() 14 | { 15 | } 16 | 17 | //For testing 18 | public PrivateEndpointExtensions(string[] privateEndpointIds) 19 | { 20 | PrivateEndpointConnections = privateEndpointIds; 21 | } 22 | 23 | public string[] PrivateEndpointConnections { get; private set; } = default!; 24 | 25 | public (string key, HttpMethod method, string suffix, string? version)? ApiCall => null; 26 | 27 | public Task Enrich(AzureResource resource, JObject raw, Dictionary additionalResources) 28 | { 29 | //Private endpoints are expressed in a common way across the platform. 30 | PrivateEndpointConnections = 31 | raw["properties"]?["privateEndpointConnections"] 32 | ?.Select(x => x["properties"]!["privateEndpoint"]!.Value("id")!).ToArray() ?? 33 | Array.Empty(); 34 | 35 | return Task.CompletedTask; 36 | } 37 | 38 | public void BuildRelationships(AzureResource resource, IEnumerable allResources) 39 | { 40 | } 41 | 42 | public bool AccessedViaPrivateEndpoint(PrivateEndpoint privateEndpoint) 43 | { 44 | return PrivateEndpointConnections?.Any(x => 45 | x.Equals(privateEndpoint.Id, StringComparison.InvariantCultureIgnoreCase)) ?? false; 46 | } 47 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Retrievers/IRetrieveResource.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | 4 | namespace AzureDiagrams.Resources.Retrievers; 5 | 6 | public interface IRetrieveResource 7 | { 8 | Task FetchResource(HttpClient client); 9 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/S2S.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagrams.Resources; 6 | 7 | public class S2S : AzureResource 8 | { 9 | public override string Image => "img/lib/mscae/VPN_Gateway.svg"; 10 | public override Task Enrich(JObject full, Dictionary additionalResources) 11 | { 12 | VHubId = full["properties"]!["virtualHub"]?.Value("id"); 13 | return base.Enrich(full, additionalResources); 14 | } 15 | 16 | public string? VHubId { get; set; } 17 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/ServiceBus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class ServiceBus : AzureResource, ICanBeAccessedViaAHostName 10 | { 11 | public override string Image => "img/lib/mscae/Service_Bus.svg"; 12 | 13 | public string[] HostNames { get; private set; } = default!; 14 | 15 | public bool CanIAccessYouOnThisHostName(string hostname) 16 | { 17 | return HostNames.Contains(hostname, StringComparer.InvariantCultureIgnoreCase); 18 | } 19 | 20 | public override Task Enrich(JObject full, Dictionary additionalResources) 21 | { 22 | HostNames = new[] { full["properties"]!.Value("serviceBusEndpoint")!.GetHostNameFromUrlString() }; 23 | return base.Enrich(full, additionalResources); 24 | } 25 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/StaticSite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class StaticSite : AzureResource, ICanBeAccessedViaAHostName 10 | { 11 | public override string Image => "img/lib/azure2/app_services/App_Service_Domains.svg"; 12 | 13 | public string[] EnabledHostNames { get; set; } = default!; 14 | 15 | public bool CanIAccessYouOnThisHostName(string hostname) 16 | { 17 | return EnabledHostNames.Any( 18 | hn => string.Compare(hn, hostname, StringComparison.InvariantCultureIgnoreCase) == 0); 19 | } 20 | 21 | 22 | public override async Task Enrich(JObject full, Dictionary additionalResources) 23 | { 24 | await base.Enrich(full, additionalResources); 25 | EnabledHostNames = 26 | new[] { full["properties"]!.Value("defaultHostname")! } 27 | .Concat(full["properties"]!["customDomains"]!.Values().Select(x => x!)).ToArray(); 28 | } 29 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/StorageAccount.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace AzureDiagrams.Resources; 10 | 11 | [DebuggerDisplay("{Type}/{Name}")] 12 | public class StorageAccount : AzureResource, ICanBeAccessedViaAHostName 13 | { 14 | public StorageAccount(string id, string name, string[] hostNames) 15 | { 16 | Id = id; 17 | Name = name; 18 | HostNames = hostNames; 19 | } 20 | 21 | /// 22 | /// Used for json deserialization 23 | /// 24 | [JsonConstructor] 25 | public StorageAccount() 26 | { 27 | } 28 | 29 | public string[] HostNames { get; private set; } = default!; 30 | public override string Image => "img/lib/azure2/storage/Storage_Accounts.svg"; 31 | 32 | public override Task Enrich(JObject jObject, Dictionary additionalResources) 33 | { 34 | HostNames = jObject["properties"]!["primaryEndpoints"]?.ToObject>()?.Values.Select(x => x.GetHostNameFromUrlString()).ToArray() ?? 35 | Array.Empty(); 36 | return base.Enrich(jObject, additionalResources); 37 | } 38 | 39 | public bool CanIAccessYouOnThisHostName(string hostname) 40 | { 41 | return HostNames.Contains(hostname, StringComparer.InvariantCultureIgnoreCase); 42 | } 43 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/StringEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureDiagrams.Resources; 4 | 5 | public static class StringEx 6 | { 7 | public static string GetHostNameFromUrlString(this string urlString) 8 | { 9 | return new Uri(urlString, UriKind.RelativeOrAbsolute).Host.ToLowerInvariant(); 10 | } 11 | 12 | public static string? GetHostNameFromUrlStringOrNull(this string urlString) 13 | { 14 | if (Uri.TryCreate(urlString, UriKind.Absolute, out var url)) return url.Host.ToLowerInvariant(); 15 | 16 | return null; 17 | } 18 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/Synapse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AzureDiagrams.Resources.Retrievers.Custom; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources; 9 | 10 | public class Synapse : AzureResource, ICanBeAccessedViaAHostName 11 | { 12 | private JObject _linkedServices = default!; 13 | public override string Image => "img/lib/azure2/databases/Data_Factory.svg"; 14 | 15 | public override Task Enrich(JObject full, Dictionary additionalResources) 16 | { 17 | HostNames = full["properties"]!["connectivityEndpoints"]!.ToObject>()!.Values 18 | .Select(x => x.GetHostNameFromUrlStringOrNull() ?? x).ToArray(); 19 | _linkedServices = additionalResources[SynapseRetriever.LinkedServices]!; 20 | return base.Enrich(full, additionalResources); 21 | } 22 | 23 | public override void BuildRelationships(IEnumerable allResources) 24 | { 25 | var possibleConnections = new RelationshipHelper( 26 | _linkedServices["value"]! 27 | .SelectMany(x => 28 | x["properties"]!["typeProperties"]?.ToObject>() 29 | ?.Select(kvp => kvp.Value.ToString() ?? "") ?? Array.Empty()) 30 | .ToArray()); 31 | 32 | possibleConnections.Discover(); 33 | possibleConnections.BuildRelationships(this, allResources); 34 | 35 | base.BuildRelationships(allResources); 36 | } 37 | 38 | private string[] HostNames { get; set; } = default!; 39 | 40 | public bool CanIAccessYouOnThisHostName(string hostname) 41 | { 42 | return HostNames.Contains(hostname, StringComparer.InvariantCultureIgnoreCase); 43 | } 44 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/UDR.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public class UDR : AzureResource 4 | { 5 | public override string Image => "img/lib/azure2/networking/Route_Tables.svg"; 6 | } 7 | -------------------------------------------------------------------------------- /AzureDiagrams/Resources/UserAssignedIdentity.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public class UserAssignedIdentity 4 | { 5 | public string PrincipalId { get; set; } = default!; 6 | public string ClientId { get; set; } = default!; 7 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/UserAssignedManagedIdentity.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public class UserAssignedManagedIdentity : AzureResource 4 | { 5 | public override string Image => "img/lib/azure2/identity/Managed_Identities.svg"; 6 | 7 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/VHub.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Metadata; 5 | using System.Threading.Tasks; 6 | using AzureDiagrams.Resources.Retrievers.Custom; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace AzureDiagrams.Resources; 11 | 12 | public class VHub : AzureResource 13 | { 14 | public override string Fill => "#d5e8d4"; 15 | 16 | public VHub(string vWanId, string id, string name, string[] connectedVirtualNetworkIds) 17 | { 18 | Id = id; 19 | Name = name; 20 | VWanId = vWanId; 21 | ConnectedVirtualNetworkIds = connectedVirtualNetworkIds; 22 | } 23 | 24 | [JsonConstructor] 25 | public VHub() 26 | { 27 | } 28 | 29 | public override Task Enrich(JObject full, Dictionary additionalResources) 30 | { 31 | VWanId = full["properties"]!["virtualWan"]!.Value("id")!; 32 | ConnectedVirtualNetworkIds = additionalResources[VHubRetriever.VirtualNetworkConnections]!["value"]? 33 | .Select(x => x["properties"]!["remoteVirtualNetwork"]!.Value("id")) 34 | .Select(x => x!).ToArray() ?? 35 | Array.Empty(); 36 | FirewallId = full["properties"]!["azureFirewall"]?.Value("id"); 37 | 38 | return base.Enrich(full, additionalResources); 39 | } 40 | 41 | public string? FirewallId { get; set; } 42 | 43 | public string[] ConnectedVirtualNetworkIds { get; set; } = default!; 44 | 45 | public string VWanId { get; set; } = default!; 46 | 47 | public override IEnumerable DiscoverNewNodes(List azureResources) 48 | { 49 | foreach (var vnet in ConnectedVirtualNetworkIds) 50 | { 51 | var vnetResource = azureResources.OfType() 52 | .SingleOrDefault(x => x.Id.Equals(vnet, StringComparison.InvariantCultureIgnoreCase)); 53 | if (vnetResource != null) 54 | { 55 | var vnetConnection = new VirtualHubVirtualNetworkConnection() 56 | { 57 | VHub = this, 58 | Id = $"{Id}.{vnet}", 59 | LinkedVNet = vnetResource, 60 | Name = "Virtual Hub Link" 61 | }; 62 | vnetResource.LinksToVHub(vnetConnection); 63 | yield return vnetConnection; 64 | } 65 | } 66 | } 67 | 68 | public override void BuildRelationships(IEnumerable allResources) 69 | { 70 | if (FirewallId != null) 71 | { 72 | var firewall = allResources.OfType() 73 | .SingleOrDefault(x => x.Id.Equals(FirewallId, StringComparison.InvariantCultureIgnoreCase)); 74 | if (firewall != null) 75 | { 76 | OwnsResource(firewall); 77 | } 78 | } 79 | 80 | allResources.OfType() 81 | .Where(x => x.VHubId?.Equals(Id, StringComparison.InvariantCultureIgnoreCase) ?? false) 82 | .ForEach(OwnsResource); 83 | 84 | allResources.OfType() 85 | .Where(x => x.VHubId?.Equals(Id, StringComparison.InvariantCultureIgnoreCase) ?? false) 86 | .ForEach(OwnsResource); 87 | 88 | //Issue with the SugiyamaLayoutSettings drawing an edge from a cluster (which is how the VNet appears) to a node... Instead we will need to gen a new node to represent the VHub -> VNet connection, and draw the link between them 89 | 90 | 91 | base.BuildRelationships(allResources); 92 | } 93 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/VM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class VM : AzureResource, IAssociateWithNic 10 | { 11 | public override string Image => "img/lib/azure2/compute/Virtual_Machine.svg"; 12 | public override string? Fill => "#D5E8D4"; 13 | public string SystemDiskId { get; protected set; } = default!; 14 | 15 | public string[] DataDiskIds { get; set; } = default!; 16 | public string BootDiagnosticsStorageHost { get; protected set; } = default!; 17 | public string[] Nics { get; protected set; } = default!; 18 | 19 | public override Task Enrich(JObject jObject, Dictionary additionalResources) 20 | { 21 | SystemDiskId = jObject["properties"]!["storageProfile"]!["osDisk"]!["managedDisk"]!.Value("id")!; 22 | DataDiskIds = 23 | jObject["properties"]!["storageProfile"]!["dataDisks"] 24 | ?.Select(x => x["managedDisk"]?.Value("id")) 25 | .Where(x => x != null) 26 | .Select(x => x!) 27 | .ToArray() ?? Array.Empty(); 28 | 29 | BootDiagnosticsStorageHost = 30 | jObject["properties"]!["diagnosticsProfile"]!["bootDiagnostics"]!.Value("storageUri")!; 31 | 32 | Nics = jObject["properties"]!["networkProfile"]!["networkInterfaces"]!.Select(x => x.Value("id")!) 33 | .ToArray(); 34 | 35 | return Task.CompletedTask; 36 | } 37 | 38 | 39 | public override void BuildRelationships(IEnumerable allResources) 40 | { 41 | var disk = allResources.OfType().Single(x => string.Equals(x.Id, SystemDiskId, StringComparison.InvariantCultureIgnoreCase)); 42 | CreateFlowTo(disk, Plane.Runtime); 43 | OwnsResource(disk); 44 | 45 | DataDiskIds.Select(x => 46 | allResources.OfType().Single(x => string.Equals(x.Id, SystemDiskId, StringComparison.InvariantCultureIgnoreCase)) 47 | ) 48 | .ForEach(dataDisk => 49 | { 50 | CreateFlowTo(dataDisk, Plane.Runtime); 51 | OwnsResource(dataDisk); 52 | }); 53 | 54 | var allNics = Nics.Select(nic => 55 | allResources.OfType().Single(x => x.Id.Equals(nic, StringComparison.InvariantCultureIgnoreCase))); 56 | 57 | var injectedSubnets = allNics.SelectMany(nic => nic.SubnetIdsIAmInjectedInto).ToArray(); 58 | if (injectedSubnets.Length == 1) 59 | { 60 | var vnetId = string.Join('/', injectedSubnets[0].Split('/')[..^2]); 61 | var vnet = allResources.OfType() 62 | .SingleOrDefault(x => x.Id.Equals(vnetId, StringComparison.InvariantCultureIgnoreCase)); 63 | vnet?.GiveHomeToVirtualMachine(this, injectedSubnets[0].Split('/')[^1]); 64 | } 65 | else 66 | { 67 | //inject the VM into the VNet... It can be in multiple subnets so it feels weird to try put it into each 68 | var vnets = injectedSubnets.Select(sn => allResources.OfType().Single(x => 69 | x.Id.Equals(string.Join('/', sn.Split('/')[..^2]), StringComparison.InvariantCultureIgnoreCase))) 70 | .Distinct(); 71 | vnets.ForEach(vnet => vnet.GiveHomeToVirtualMachine(this)); 72 | } 73 | 74 | if (!string.IsNullOrEmpty(BootDiagnosticsStorageHost)) 75 | { 76 | var hostname = BootDiagnosticsStorageHost.GetHostNameFromUrlString(); 77 | var allPossibleStorageAccounts = allResources.OfType().Where(x => 78 | x.CanIAccessYouOnThisHostName(BootDiagnosticsStorageHost.GetHostNameFromUrlString())); 79 | var storage = allPossibleStorageAccounts.OfType().SingleOrDefault() ?? allPossibleStorageAccounts.SingleOrDefault(); 80 | 81 | if (storage != null) 82 | { 83 | this.CreateLayer7Flow(allResources, (AzureResource)storage, "boot-diagnostics", 84 | hns => hns.Any(hn => hn.Contains(hostname)), Plane.Diagnostics); 85 | } 86 | } 87 | 88 | base.BuildRelationships(allResources); 89 | } 90 | 91 | public void AddExtension(VMExtension vmExtension) 92 | { 93 | OwnsResource(vmExtension); 94 | } 95 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/VMExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace AzureDiagrams.Resources; 7 | 8 | public class VMExtension : AzureResource 9 | { 10 | private string _vm = default!; 11 | 12 | public override Task Enrich(JObject full, Dictionary additionalResources) 13 | { 14 | _vm = string.Join('/', Id.Split('/')[..^2]).ToLowerInvariant(); 15 | return base.Enrich(full, additionalResources); 16 | } 17 | 18 | public override void BuildRelationships(IEnumerable allResources) 19 | { 20 | allResources.OfType().Single(x => x.Id.ToLowerInvariant() == _vm).AddExtension(this); 21 | base.BuildRelationships(allResources); 22 | } 23 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/VMSS.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace AzureDiagrams.Resources; 8 | 9 | public class VMSS : AzureResource, ICanInjectIntoASubnet 10 | { 11 | public override string Image => "img/lib/azure2/compute/VM_Scale_Sets.svg"; 12 | 13 | public override Task Enrich(JObject jObject, Dictionary additionalResources) 14 | { 15 | var nicConfigurations = 16 | jObject["properties"]!["virtualMachineProfile"]!["networkProfile"]!["networkInterfaceConfigurations"] 17 | ?.SelectMany(x => x["properties"]!["ipConfigurations"]!)?.ToArray(); 18 | 19 | LoadBalancerRelationships = nicConfigurations?.SelectMany(GetLoadBalancersFromIpConfiguration).Distinct() ?? 20 | Array.Empty(); 21 | SubnetIdsIAmInjectedInto = nicConfigurations?.Select(GetSubnetFromIpConfiguration).Where(x => x != null) 22 | .Distinct().Select(x => x!) 23 | .ToArray() ?? Array.Empty(); 24 | 25 | return Task.CompletedTask; 26 | } 27 | 28 | private string? GetSubnetFromIpConfiguration(JToken ipConfiguration) 29 | { 30 | return ipConfiguration["properties"]!["subnet"]?.Value("id"); 31 | } 32 | 33 | public IEnumerable LoadBalancerRelationships { get; private set; } = default!; 34 | 35 | private IEnumerable GetLoadBalancersFromIpConfiguration(JToken ipConfiguration) 36 | { 37 | var lbBackEndPools = ipConfiguration["properties"]!["loadBalancerBackendAddressPools"] 38 | ?.Select(lbbap => string.Join('/', 39 | lbbap.Value("id")?.Split('/')[..^2] ?? Array.Empty())) ?? 40 | Array.Empty(); 41 | 42 | var lbNatPools = ipConfiguration["properties"]!["loadBalancerInboundNatPools"] 43 | ?.Select(lbbap => string.Join('/', 44 | lbbap.Value("id")?.Split('/')[..^2] ?? Array.Empty())) ?? 45 | Array.Empty(); 46 | 47 | return lbBackEndPools.Union(lbNatPools); 48 | } 49 | 50 | public override void BuildRelationships(IEnumerable allResources) 51 | { 52 | allResources.OfType() 53 | .Where(x => LoadBalancerRelationships.Contains(x.Id, StringComparer.InvariantCultureIgnoreCase)) 54 | .ForEach(lb => lb.CreateFlowTo(this, "Connects", Plane.Runtime)); 55 | base.BuildRelationships(allResources); 56 | } 57 | 58 | public string[] SubnetIdsIAmInjectedInto { get; private set; } = default!; 59 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/VNet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace AzureDiagrams.Resources; 9 | 10 | public class VNet : AzureResource 11 | { 12 | private string[] _peerings = []; 13 | 14 | public VNet(string id, string name, Subnet[] subnets) 15 | { 16 | Id = id; 17 | Name = name; 18 | Subnets = subnets; 19 | } 20 | 21 | /// 22 | /// Used for json deserialization 23 | /// 24 | [JsonConstructor] 25 | public VNet() 26 | { 27 | } 28 | 29 | public Subnet[] Subnets { get; private set; } = default!; 30 | public List PrivateDnsZones { get; } = new(); 31 | 32 | public override string Image => "img/lib/azure2/networking/Virtual_Networks.svg"; 33 | 34 | public override Task Enrich(JObject full, Dictionary additionalResources) 35 | { 36 | Subnets = full["properties"]!["subnets"]!.Select(x => new Subnet 37 | ( 38 | x.Value("name")!, 39 | x["properties"]!.Value("addressPrefix")!, 40 | x["properties"]!["routeTable"]?.Value("id") 41 | )).ToArray(); 42 | 43 | _peerings = 44 | full["properties"]! 45 | ["virtualNetworkPeerings"]? 46 | .Select(x => x["properties"]!["remoteVirtualNetwork"]!.Value("id")!) 47 | .ToArray() ?? []; 48 | 49 | return Task.CompletedTask; 50 | } 51 | 52 | public void AssignPrivateDnsZone(PrivateDnsZone resource) 53 | { 54 | PrivateDnsZones.Add(resource); 55 | } 56 | 57 | private void InjectResourceInto(AzureResource resource, string subnet) 58 | { 59 | Subnets.Single(x => string.Compare(x.Name, subnet, StringComparison.InvariantCultureIgnoreCase) == 0) 60 | .ContainedResources.Add(resource); 61 | resource.ContainedByAnotherResource = true; 62 | } 63 | 64 | public void AssignNsg(NSG nsg, string subnet) 65 | { 66 | Subnets.Single(x => x.Name == subnet).NSGs.Add(nsg); 67 | nsg.ContainedByAnotherResource = true; 68 | } 69 | 70 | public override void BuildRelationships(IEnumerable allResources) 71 | { 72 | allResources 73 | .OfType() 74 | .Select(x => (resource: (AzureResource)x, subnets: SubnetsInsideThisVNet(x.SubnetIdsIAmInjectedInto))) 75 | .ForEach(r => 76 | r.subnets.ForEach(s => InjectResourceInto(r.resource, s))); 77 | 78 | Subnets.Where(x => x.UdrId != null).ForEach(x => 79 | { 80 | var udr = allResources.OfType().SingleOrDefault(udr => udr.Id.Equals(x.UdrId)); 81 | if (udr != null) //Azure Firewall Management for example has a weird UDR! 82 | { 83 | InjectResourceInto(udr, x.Name); 84 | } 85 | }); 86 | 87 | foreach (var peeredVnet in _peerings) 88 | { 89 | var peeredVnetResource = allResources.SingleOrDefault(x => 90 | string.Equals(x.Id, peeredVnet, StringComparison.InvariantCultureIgnoreCase)); 91 | if (peeredVnetResource != null) 92 | { 93 | CreateFlowTo(peeredVnetResource, "peering", Plane.Runtime); 94 | } 95 | } 96 | 97 | base.BuildRelationships(allResources); 98 | } 99 | 100 | private IEnumerable SubnetsInsideThisVNet(string[] subnetIdsIAmInjectedInto) 101 | { 102 | return subnetIdsIAmInjectedInto.Where(x => 103 | string.Compare(Id, string.Join('/', x.Split('/')[..^2]), StringComparison.InvariantCultureIgnoreCase) == 104 | 0) 105 | .Select(x => x.Split('/')[^1]); 106 | } 107 | 108 | /// 109 | /// VMs can be associated to multiple nics, in different subnets. So you can choose to put it in either. 110 | /// 111 | /// 112 | /// 113 | public void GiveHomeToVirtualMachine(VM vm, string? optionalSubnetId = null) 114 | { 115 | if (string.IsNullOrEmpty(optionalSubnetId)) 116 | OwnsResource(vm); 117 | else 118 | InjectResourceInto(vm, optionalSubnetId); 119 | } 120 | 121 | public class Subnet 122 | { 123 | public Subnet(string name, string addressPrefix, string? udrId = null) 124 | { 125 | Name = name; 126 | UdrId = udrId; 127 | AddressPrefix = addressPrefix; 128 | } 129 | 130 | public string Name { get; init; } 131 | public string? UdrId { get; init; } 132 | public string AddressPrefix { get; init; } 133 | 134 | public List ContainedResources { get; } = new(); 135 | 136 | public List NSGs { get; } = new(); 137 | } 138 | 139 | /// 140 | /// Setup a link from a vnet to a hub 141 | /// 142 | public void LinksToVHub(VirtualHubVirtualNetworkConnection virtualHubVirtualNetworkConnection) 143 | { 144 | OwnsResource(virtualHubVirtualNetworkConnection); 145 | } 146 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/VNetIntegration.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDiagrams.Resources; 2 | 3 | public class VNetIntegration : AzureResource, ICanInjectIntoASubnet 4 | { 5 | public AppServiceApp LinkedApp { get; } 6 | private readonly string _vnetIntegratedInto; 7 | 8 | public override string Image => "img/lib/azure2/networking/Virtual_Networks.svg"; 9 | 10 | public VNetIntegration(string id, string vnetIntegratedInto, AppServiceApp linkedApp) 11 | { 12 | LinkedApp = linkedApp; 13 | Id = id; 14 | _vnetIntegratedInto = vnetIntegratedInto; 15 | } 16 | 17 | public string[] SubnetIdsIAmInjectedInto => new[] { _vnetIntegratedInto }; 18 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/VWan.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Newtonsoft.Json; 5 | 6 | namespace AzureDiagrams.Resources; 7 | 8 | public class VWan : AzureResource 9 | { 10 | public override string Image => "img/lib/azure2/networking/Virtual_WANs.svg"; 11 | public override string? Fill => "#dae8fc"; 12 | 13 | public VWan(string id, string name) 14 | { 15 | Id = id; 16 | Name = name; 17 | } 18 | 19 | [JsonConstructor] 20 | public VWan() { } 21 | 22 | public override void BuildRelationships(IEnumerable allResources) 23 | { 24 | allResources.OfType().Where(x => x.VWanId.Equals(Id, StringComparison.InvariantCultureIgnoreCase)).ForEach(OwnsResource); 25 | base.BuildRelationships(allResources); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /AzureDiagrams/Resources/VirtualHubVirtualNetworkConnection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AzureDiagrams.Resources; 4 | 5 | public class VirtualHubVirtualNetworkConnection : AzureResource 6 | { 7 | public override string Image => "img/lib/azure2/networking/Virtual_WANs.svg"; 8 | public VHub VHub { get; init; } 9 | public VNet LinkedVNet { get; init; } 10 | 11 | public override void BuildRelationships(IEnumerable allResources) 12 | { 13 | CreateFlowTo(VHub, Plane.All); 14 | } 15 | } -------------------------------------------------------------------------------- /AzureDiagramsTests/AzResourceHelper.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams; 2 | using AzureDiagrams.Resources; 3 | 4 | namespace AzureDiagramsTests; 5 | 6 | public static class AzResourceHelper 7 | { 8 | public static Guid TestSubscriptionId = Guid.Parse("DE84C705-69EB-4EC1-8C35-CA648AC05E88"); 9 | 10 | public static string GetResourceId(string resourceGroupName, string resourceName) 11 | { 12 | return $"/subscriptions/{TestSubscriptionId}/resourceGroups/{resourceGroupName}/{resourceName}"; 13 | } 14 | 15 | public static AzureResource[] Process(this AzureResource[] resources) 16 | { 17 | var allResources = AzureModelRetriever.ProcessResourcesAndAddStaticNodes(resources.ToList()); 18 | return allResources; 19 | } 20 | } -------------------------------------------------------------------------------- /AzureDiagramsTests/AzureDiagramsTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /AzureDiagramsTests/Basic/BasicResources.SingleStorageAccount.approved.txt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /AzureDiagramsTests/Basic/BasicResources.SingleStorageAccountSimpleConstructor.approved.txt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /AzureDiagramsTests/Basic/BasicResources.VNetWithAttachedStoragetAccountInSubNet.approved.txt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | 33 | 34 | 36 | 37 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /AzureDiagramsTests/Basic/BasicResources.VNetWithSubNet.approved.txt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /AzureDiagramsTests/Basic/BasicResources.VNetWithSubNetSimpleConstructor.approved.txt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /AzureDiagramsTests/Basic/BasicResources.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams.Resources; 2 | using Shouldly; 3 | 4 | namespace AzureDiagramsTests.Basic; 5 | 6 | public class BasicResources 7 | { 8 | [Fact] 9 | public async Task SingleStorageAccount() 10 | { 11 | var resources = TestResourcesObjectMother 12 | .StorageAccount(); 13 | 14 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 15 | resources.ToArray(), 16 | false, 17 | false, 18 | false, 19 | false, 20 | false); 21 | 22 | diagram.ShouldMatchApproved(); 23 | } 24 | 25 | [Fact] 26 | public async Task SingleStorageAccountSimpleConstructor() 27 | { 28 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 29 | new [] 30 | { 31 | new StorageAccount("6e89f6aa-1b83-42f1-ad92-786b47d9fdf7", "test-storage", Array.Empty()) 32 | }, 33 | false, 34 | false, 35 | false, 36 | false, 37 | false); 38 | 39 | diagram.ShouldMatchApproved(); 40 | } 41 | 42 | [Fact] 43 | public async Task VNetWithSubNet() 44 | { 45 | var resources = await TestResourcesObjectMother 46 | .VirtualNetwork("subnet1"); 47 | 48 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 49 | resources.ToArray(), 50 | false, 51 | false, 52 | false, 53 | false, 54 | false); 55 | 56 | diagram.ShouldMatchApproved(); 57 | } 58 | 59 | [Fact] 60 | public async Task VNetWithSubNetSimpleConstructor() 61 | { 62 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 63 | new AzureResource [] 64 | { 65 | new VNet("6e89f6aa-1b83-42f1-ad92-786b47d9fdf7", "vnet", new [] 66 | { 67 | new VNet.Subnet("subnet1", "10.1.0.0/26"), 68 | new VNet.Subnet("subnet2", "10.1.1.0/26"), 69 | }) 70 | }, 71 | false, 72 | false, 73 | false, 74 | false, 75 | false); 76 | 77 | diagram.ShouldMatchApproved(); 78 | } 79 | 80 | [Fact] 81 | public async Task VNetWithAttachedStoragetAccountInSubNet() 82 | { 83 | var resources = (await TestResourcesObjectMother.StorageAccountWithPrivateEndpoint()) 84 | .ToArray(); 85 | 86 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 87 | resources.ToArray(), 88 | false, 89 | false, 90 | false, 91 | true, 92 | false); 93 | 94 | diagram.ShouldMatchApproved(); 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /AzureDiagramsTests/TestResourcesObjectMother.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams.Resources; 2 | using AzureDiagrams.Resources.Retrievers.Extensions; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace AzureDiagramsTests; 6 | 7 | public class TestResourcesObjectMother 8 | { 9 | public static IEnumerable StorageAccount() 10 | { 11 | yield return new StorageAccount() 12 | { 13 | Id = AzResourceHelper.GetResourceId("test-rg", 14 | "storage123"), 15 | Name = "storage123", 16 | Type = "microsoft.storage/storageaccounts" 17 | }; 18 | } 19 | 20 | public static async Task> StorageAccountWithPrivateEndpoint() 21 | { 22 | var vnet = (VNet)(await VirtualNetwork("test-subnet")).Single(); 23 | 24 | var storage = new StorageAccount() 25 | { 26 | Id = AzResourceHelper.GetResourceId("test-rg", 27 | "storage123"), 28 | Name = "storage123", 29 | Type = "microsoft.storage/storageaccounts", 30 | Extensions = new[] { new PrivateEndpointExtensions() } 31 | }; 32 | var peId = new Guid("A1621A82-22D1-495D-8B9F-AF87F31D21C2"); 33 | 34 | var privateEndpointResourceId = AzResourceHelper.GetResourceId("test-rg", 35 | $"pe-{peId}"); 36 | 37 | var rawStorageJson = JObject.FromObject(new 38 | { 39 | properties = new 40 | { 41 | privateEndpointConnections = new[] 42 | { 43 | new 44 | { 45 | properties = new 46 | { 47 | privateEndpoint = new 48 | { 49 | id = privateEndpointResourceId 50 | } 51 | } 52 | } 53 | } 54 | } 55 | }); 56 | await storage.Enrich(rawStorageJson, new Dictionary()); 57 | storage.Extensions.ForEach(x => x.Enrich(storage, rawStorageJson, new Dictionary())); 58 | 59 | var nicId = new Guid("BD4F785D-6E10-40C5-9BF1-D04B20ECA9BF"); 60 | 61 | var nic = new Nic() 62 | { 63 | Id = AzResourceHelper.GetResourceId("test-rg", 64 | $"pe-nic-{nicId}"), 65 | }; 66 | 67 | await nic.Enrich(JObject.FromObject(new 68 | { 69 | properties = new 70 | { 71 | ipConfigurations = new[] 72 | { 73 | new 74 | { 75 | properties = new 76 | { 77 | subnet = new 78 | { 79 | id = $"{vnet.Id}/subnets/{vnet.Subnets[0].Name}" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | }), new Dictionary()); 86 | 87 | var pe = new PrivateEndpoint() 88 | { 89 | Id = privateEndpointResourceId, 90 | }; 91 | 92 | await pe.Enrich(JObject.FromObject(new 93 | { 94 | properties = new 95 | { 96 | networkInterfaces = new[] 97 | { 98 | new 99 | { 100 | id = nic.Id 101 | } 102 | }, 103 | customDnsConfigs = new[] 104 | { 105 | new 106 | { 107 | fqdn = "test-pe.localtest.me" 108 | } 109 | }, 110 | subnet = new 111 | { 112 | id = $"{vnet.Id}/subnets/{vnet.Subnets[0].Name}" 113 | } 114 | } 115 | }), new Dictionary()); 116 | 117 | var allResources = new AzureResource[] { vnet, storage, pe, nic }; 118 | return allResources.Process(); 119 | } 120 | 121 | public static async Task> VirtualNetwork(string subnet) 122 | { 123 | var vnet = new VNet() 124 | { 125 | Id = AzResourceHelper.GetResourceId("test-rg", 126 | "vnet123"), 127 | }; 128 | await vnet.Enrich(JObject.FromObject(new 129 | { 130 | properties = new 131 | { 132 | subnets = new[] 133 | { 134 | new 135 | { 136 | name = subnet, 137 | properties = new 138 | { 139 | addressPrefix = "10.0.0.0/24" 140 | } 141 | } 142 | } 143 | } 144 | }), new Dictionary()); 145 | return new[] { vnet }; 146 | } 147 | } -------------------------------------------------------------------------------- /AzureDiagramsTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /AzureDiagramsTests/VirtualWans/VWanWithVHub.CanDrawDiagram.approved.txt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | 33 | 34 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /AzureDiagramsTests/VirtualWans/VWanWithVHub.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams.Resources; 2 | using Shouldly; 3 | 4 | namespace AzureDiagramsTests.VirtualWans; 5 | 6 | public class VWanWithVHub 7 | { 8 | [Fact] 9 | public async Task CanDrawDiagram() 10 | { 11 | var resources = BuildScenario(); 12 | 13 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 14 | resources, 15 | false, 16 | false, 17 | false, 18 | true, 19 | false); 20 | 21 | diagram.ShouldMatchApproved(); 22 | } 23 | 24 | private static AzureResource[] BuildScenario() 25 | { 26 | var vwanId = new Guid("6d4ced93-d694-4e0c-b3a0-6cef2c095061").ToString(); 27 | var vnetId = "/subscriptions/123/resourceGroups/rg1/providers/microsoft.network/virtualnetworks/vnet123"; 28 | 29 | var vwan = new VWan(vwanId, "vwan"); 30 | 31 | var vnet = new VNet(vnetId, "test-vnet", Array.Empty()); 32 | 33 | var vhub = new VHub( 34 | vwanId, 35 | new Guid("318e3768-dacb-4e2c-b87e-288107259a6c").ToString(), 36 | "Hub-1", 37 | new[] { vnetId } 38 | ); 39 | 40 | // var appServicePlanId = 41 | // "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/serverFarms/TestAppServicePlan"; 42 | // 43 | // var asp = new AppServicePlan(appServicePlanId, "TestAppServicePlan"); 44 | // 45 | // var peId = new Guid("36a566f9-f6b8-439c-9371-204842160e2b").ToString(); 46 | // var pe2Id = new Guid("36a566f9-f6b8-439c-9371-204842160e2d").ToString(); 47 | // 48 | // var app1HostName = "TestApp1.azurewebsites.net"; 49 | // var app2HostName = "TestApp2.azurewebsites.net"; 50 | // 51 | // var app1 = new AppServiceApp("/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/sites/TestApp1", 52 | // appServicePlanId, 53 | // "TestApp1", 54 | // false, 55 | // Array.Empty(), 56 | // new[] { app1HostName }, 57 | // virtualNetworkSubnetId: $"{vnetId}/subnets/vnet-integration", 58 | // new PrivateEndpointExtensions(new[] { peId })); 59 | // 60 | // var app2 = new AppServiceApp("/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/sites/TestApp2", 61 | // appServicePlanId, 62 | // "TestApp2", 63 | // false, 64 | // new[] { $"https://{app1HostName}" }, 65 | // new[] { app2HostName }, 66 | // virtualNetworkSubnetId: $"{vnetId}/subnets/vnet-integration", 67 | // new PrivateEndpointExtensions(new[] { pe2Id })); 68 | // 69 | // var nicId = new Guid("36a566f9-f6b8-439c-9371-204842160e2c").ToString(); 70 | // var nic = new Nic(nicId, 71 | // IpConfigurations.ForPrivateEndpoint("10.1.1.1", $"{vnetId}/subnets/private-endpoints", app1HostName)); 72 | // 73 | // var privateEndpoint = new PrivateEndpoint(peId, new[] { $"{vnetId}/subnets/private-endpoints" }, 74 | // new[] { nic.Id }, new[] { $"https://{app1HostName}" }); 75 | // 76 | // var nic2Id = new Guid("36a566f9-f6b8-439c-9371-204842160e2e").ToString(); 77 | // var nic2 = new Nic(nic2Id, 78 | // IpConfigurations.ForPrivateEndpoint("10.1.1.2", $"{vnetId}/subnets/private-endpoints", app2HostName)); 79 | // 80 | // var privateEndpoint2 = new PrivateEndpoint(pe2Id, new[] { $"{vnetId}/subnets/private-endpoints" }, 81 | // new[] { nic2.Id }, new[] { $"https://{app2HostName}" }); 82 | 83 | var azureResources = new AzureResource[] 84 | { 85 | vwan, 86 | vhub, 87 | vnet 88 | }; 89 | 90 | var resources = azureResources.Process(); 91 | return resources; 92 | } 93 | } -------------------------------------------------------------------------------- /AzureDiagramsTests/WebApps/WebAppWithPrivateEndpoint.CanDrawCondensedDiagram.approved.txt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | 33 | 34 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /AzureDiagramsTests/WebApps/WebAppWithPrivateEndpoint.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams.Resources; 2 | using AzureDiagrams.Resources.Retrievers.Extensions; 3 | using Shouldly; 4 | 5 | namespace AzureDiagramsTests.WebApps; 6 | 7 | public class WebAppWithPrivateEndpoint 8 | { 9 | [Fact] 10 | public async Task CanDrawDiagram() 11 | { 12 | var resources = BuildScenario(); 13 | 14 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 15 | resources, 16 | false, 17 | false, 18 | false, 19 | true, 20 | false); 21 | 22 | diagram.ShouldMatchApproved(); 23 | } 24 | 25 | 26 | [Fact] 27 | public async Task CanDrawCondensedDiagram() 28 | { 29 | var resources = BuildScenario(); 30 | 31 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 32 | resources, 33 | true, 34 | false, 35 | false, 36 | true, 37 | false); 38 | 39 | diagram.ShouldMatchApproved(); 40 | } 41 | 42 | private static AzureResource[] BuildScenario() 43 | { 44 | var vnetId = "/subscriptions/123/resourceGroups/rg1/providers/microsoft.network/virtualnetworks/vnet123"; 45 | 46 | var vnet = new VNet(vnetId, "test-vnet", new[] 47 | { 48 | new VNet.Subnet("private-endpoints", "10.1.1.0/24"), 49 | new VNet.Subnet("vnet-integration", "10.1.2.0/24"), 50 | }); 51 | 52 | var appServicePlanId = 53 | "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/serverFarms/TestAppServicePlan"; 54 | 55 | var asp = new AppServicePlan(appServicePlanId, "TestAppServicePlan"); 56 | 57 | var peId = new Guid("36a566f9-f6b8-439c-9371-204842160e2b").ToString(); 58 | var pe2Id = new Guid("36a566f9-f6b8-439c-9371-204842160e2d").ToString(); 59 | 60 | var app1HostName = "TestApp1.azurewebsites.net"; 61 | var app2HostName = "TestApp2.azurewebsites.net"; 62 | 63 | var app1 = new AppServiceApp("/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/sites/TestApp1", 64 | appServicePlanId, 65 | "TestApp1", 66 | false, 67 | Array.Empty(), 68 | new[] { app1HostName }, 69 | virtualNetworkSubnetId: $"{vnetId}/subnets/vnet-integration", 70 | new PrivateEndpointExtensions(new[] { peId })); 71 | 72 | var app2 = new AppServiceApp("/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/sites/TestApp2", 73 | appServicePlanId, 74 | "TestApp2", 75 | false, 76 | new[] { $"https://{app1HostName}" }, 77 | new[] { app2HostName }, 78 | virtualNetworkSubnetId: $"{vnetId}/subnets/vnet-integration", 79 | new PrivateEndpointExtensions(new[] { pe2Id })); 80 | 81 | var nicId = new Guid("36a566f9-f6b8-439c-9371-204842160e2c").ToString(); 82 | var nic = new Nic(nicId, 83 | IpConfigurations.ForPrivateEndpoint("10.1.1.1", $"{vnetId}/subnets/private-endpoints", app1HostName)); 84 | 85 | var privateEndpoint = new PrivateEndpoint(peId, new[] { $"{vnetId}/subnets/private-endpoints" }, 86 | new[] { nic.Id }, new[] { $"https://{app1HostName}" }); 87 | 88 | var nic2Id = new Guid("36a566f9-f6b8-439c-9371-204842160e2e").ToString(); 89 | var nic2 = new Nic(nic2Id, 90 | IpConfigurations.ForPrivateEndpoint("10.1.1.2", $"{vnetId}/subnets/private-endpoints", app2HostName)); 91 | 92 | var privateEndpoint2 = new PrivateEndpoint(pe2Id, new[] { $"{vnetId}/subnets/private-endpoints" }, 93 | new[] { nic2.Id }, new[] { $"https://{app2HostName}" }); 94 | 95 | var azureResources = new AzureResource[] 96 | { 97 | vnet, 98 | asp, 99 | app1, 100 | app2, 101 | nic, 102 | nic2, 103 | privateEndpoint, 104 | privateEndpoint2 105 | }; 106 | 107 | var resources = azureResources.Process(); 108 | return resources; 109 | } 110 | } -------------------------------------------------------------------------------- /AzureDiagramsTests/WebApps/WebAppWithSlots.CanDrawDiagram.approved.txt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | 33 | 34 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /AzureDiagramsTests/WebApps/WebAppWithSlots.cs: -------------------------------------------------------------------------------- 1 | using AzureDiagrams.Resources; 2 | using Shouldly; 3 | 4 | namespace AzureDiagramsTests.WebApps; 5 | 6 | public class WebAppWithSlots 7 | { 8 | [Fact] 9 | public async Task CanDrawDiagram() 10 | { 11 | var appServicePlanId = "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/serverFarms/TestAppServicePlan"; 12 | var asp = new AppServicePlan(appServicePlanId, "TestAppServicePlan"); 13 | 14 | var app1 = new AppServiceApp("/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/sites/TestApp1", appServicePlanId, 15 | "TestApp1", false, Array.Empty(), Array.Empty()); 16 | 17 | var app1Slot = new AppServiceApp("/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/sites/TestApp1/slots/green", appServicePlanId, 18 | "TestApp1-green", true, Array.Empty(), Array.Empty()); 19 | 20 | var app2 = new AppServiceApp("/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/sites/TestApp2", appServicePlanId, 21 | "TestApp2", false, Array.Empty(), Array.Empty()); 22 | 23 | var app2Slot = new AppServiceApp("/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Web/sites/TestApp2/slots/green", appServicePlanId, 24 | "TestApp2-green", true, Array.Empty(), Array.Empty()); 25 | 26 | var azureResources = new AzureResource[] 27 | { 28 | asp, 29 | app1, 30 | app1Slot, 31 | app2, 32 | app2Slot 33 | }; 34 | 35 | var diagram = await AzureDiagramGenerator.DrawIoDiagramGenerator.DrawDiagram( 36 | azureResources.Process(), 37 | false, 38 | false, 39 | false, 40 | true, 41 | false); 42 | 43 | //TODO - shouldn't have to do this in the tests.... 44 | 45 | diagram.ShouldMatchApproved(); 46 | } 47 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env 2 | 3 | # Add draw.io linux 4 | RUN apt-get update && apt-get install libglib2.0-0 libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libgtk-3-0 libgbm-dev libasound2 xvfb -y 5 | ADD https://github.com/jgraph/drawio-desktop/releases/download/v17.4.2/drawio-x86_64-17.4.2.AppImage /drawio/drawio-x86_64-17.4.2.AppImage 6 | RUN chmod 777 /drawio/drawio-x86_64-17.4.2.AppImage 7 | 8 | # Copy everything 9 | COPY ./entrypoint.sh ./ 10 | 11 | RUN chmod 777 /entrypoint.sh 12 | 13 | RUN dotnet tool install --global AzureDiagramGenerator 14 | 15 | ENV ELECTRON_DISABLE_SECURITY_WARNINGS "true" 16 | ENV DRAWIO_DISABLE_UPDATE "true" 17 | ENV XVFB_DISPLAY ":42" 18 | ENV XVFB_OPTIONS "" 19 | 20 | ENTRYPOINT ["/entrypoint.sh"] 21 | 22 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | assembly-versioning-scheme: MajorMinorPatch 2 | mode: Mainline 3 | branches: {} 4 | ignore: 5 | sha: [] 6 | merge-message-formats: {} 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 graemefoster 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/grfsq2-platform-test-rg.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graemefoster/AzureResourceMap/606dffc0a216fbf985706e78c6d6bd5691c3e1cd/assets/grfsq2-platform-test-rg.drawio.png -------------------------------------------------------------------------------- /assets/more-complex.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graemefoster/AzureResourceMap/606dffc0a216fbf985706e78c6d6bd5691c3e1cd/assets/more-complex.drawio.png -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Start Xvfb 5 | export DISPLAY="${XVFB_DISPLAY}" 6 | # shellcheck disable=SC2086 7 | Xvfb "${XVFB_DISPLAY}" ${XVFB_OPTIONS} & 8 | 9 | # shellcheck disable=SC2068 10 | /root/.dotnet/tools/AzureDiagramGenerator "$@" 11 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # AzureDiagrams 2 | 3 | [![CI](https://github.com/graemefoster/AzureResourceMap/actions/workflows/build.yaml/badge.svg?branch=main)](https://github.com/graemefoster/AzureResourceMap/actions/workflows/build.yaml) 4 | [![NuGet](https://img.shields.io/nuget/dt/AzureDiagramGenerator.svg)](https://www.nuget.org/packages/AzureDiagramGenerator) 5 | [![NuGet](https://img.shields.io/nuget/vpre/AzureDiagramGenerator.svg)](https://www.nuget.org/packages/AzureDiagramGenerator) 6 | 7 | ## Generate a Draw.IO diagram from your Azure Resources 8 | 9 | TLDR; 10 | 11 | ```bash 12 | dotnet tool install --global AzureDiagramGenerator 13 | 14 | az login 15 | 16 | AzureDiagramGenerator --tenant-id --subscription-id --resource-group --resource-group --show-runtime --output c:/temp/ 17 | ``` 18 | 19 | ## CLI flags 20 | 21 | | Flag | Required | Description | 22 | |:---------------------|:----------|:-----------------------------------------------------------------------------| 23 | | --tenant-id | No | Tenant Id (defaults to current Azure CLI) | 24 | | --subscription | Yes | Subscription Id to run against | 25 | | --resource-group | Yes | Wildcard enabled resource group name (supports multiple) | 26 | | --output | Yes | Folder to output diagram to | 27 | | --condensed | No | True collapses private endpoints into subnets (can simplify large diagrams) | 28 | | --show-runtime | No | True to show runtime flows defined on the control plane | 29 | | --show-inferred | No | True to infer connections between resources by introspecting appSettings | 30 | | --show-identity | No | True to show User Assigned Managed Identity connections | 31 | | --show-diagnostics | No | True to show diagnostics flows | 32 | | --token | No | Optional JWT to avoid using CLI credential | 33 | | --output-file-name | No | Name of generated file. Defaults to resource-group name | 34 | | --output-png | No | Outputs a png file (requires draw.io to be installed) | 35 | 36 | # Github Actions 37 | 38 | We have two different actions. The first runs as a Docker action, and produces a jpeg output. The second doesn't use docker, and produces a .drawio file. 39 | 40 | - [graemefoster/azurediagramsgithubactionsdocker@v0.1.2](https://github.com/marketplace/actions/azurediagramsgithubactionsdocker) 41 | - [graemefoster/azurediagramsgithubactions@v0.1.1](https://github.com/marketplace/actions/azurediagramsgithubactions) 42 | 43 | 44 | 45 | ## Example outputs 46 | ### Azure App Service with App Insights / database / Key Vault 47 | ![AzureSimple](./assets/grfsq2-platform-test-rg.drawio.png) 48 | 49 | ### More complex with VNets and private endpoints 50 | ![AzureSimple](./assets/more-complex.drawio.png) 51 | 52 | ## How does it work? 53 | AzureDiagrams queries the Azure Resource Management APIs to introspect resource-groups. It then uses a set of strategies to enrich the raw data, building a model that can be projected into other formats. 54 | 55 | It's not 100% guaranteed to be correct but it should give a good first pass at fairly complex architectures/ 56 | 57 | To layout the components I use the amazing [AutomaticGraphLayout](https://github.com/microsoft/automatic-graph-layout) library. 58 | 59 | ## Todo 60 | There are many, many Azure services not yet covered. I'll try and put a table here of what is covered, and how comprehensive it is covered. 61 | 62 | ## Output Formats 63 | The initial version supports Draw.IO diagrams. 64 | 65 | 66 | --------------------------------------------------------------------------------