├── .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 |
5 |
6 |
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