Skip to content

Commit ce080db

Browse files
committed
feat: update orphaned resource detection to 11 resource types, Remove NICs and NSGs (no cost). Add Load Balancers, Private DNS Zones,Private Endpoints, Virtual Network Gateways, and DDoS Protection Plans.Update tests and documentation accordingly.
1 parent 6bab381 commit ce080db

File tree

7 files changed

+492
-49
lines changed

7 files changed

+492
-49
lines changed

docs/FEATURES.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,16 @@ Detect orphaned Azure resources across subscriptions and calculate their real wa
101101
| Resource Type | Detection Criteria |
102102
|---------------|--------------------|
103103
| Unattached Disks | Managed disks with no `managedBy` (not attached to any VM) |
104-
| Orphaned NICs | Network interfaces not attached to a VM or private endpoint |
105104
| Orphaned Public IPs | Public IPs not associated with any IP configuration or NAT gateway |
106-
| Orphaned NSGs | Network security groups not attached to any NIC or subnet |
107105
| Empty App Service Plans | App Service Plans with zero hosted apps |
106+
| Orphaned SQL Elastic Pools | SQL Elastic Pools with no databases in the pool |
107+
| Orphaned Application Gateways | Application gateways with no backend address pools or targets |
108+
| Orphaned NAT Gateways | NAT gateways not associated with any subnet |
109+
| Orphaned Load Balancers | Load balancers with no backend address pools |
110+
| Orphaned Private DNS Zones | Private DNS zones with no virtual network links |
111+
| Orphaned Private Endpoints | Private endpoints with no connections or unapproved connections |
112+
| Orphaned Virtual Network Gateways | Virtual network gateways with no IP configurations |
113+
| Orphaned DDoS Protection Plans | DDoS protection plans with no associated virtual networks |
108114

109115
### Authentication Required
110116

docs/ORPHANED_RESOURCES.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ Orphaned resources are Azure resources that were created but are no longer servi
77
- 💿 **Unattached Managed Disks** - Storage volumes that were detached from virtual machines (often after VM deletion) but remain in your subscription
88
- 🌐 **Unattached Public IPs** - IP addresses that are no longer associated with any network interface or load balancer
99
- 📋 **Empty App Service Plans** - Hosting plans that have no web apps deployed but still reserve compute capacity
10-
- ⚖️ **Orphaned Load Balancers** - Load balancers with no backend pools or targets
10+
- 🗄️ **Orphaned SQL Elastic Pools** - Elastic pools with no databases, still incurring reserved compute and storage costs
11+
- 🚪 **Orphaned Application Gateways** - Application gateways with no backend address pools or targets configured
12+
- 🔀 **Orphaned NAT Gateways** - NAT gateways not associated with any subnet
13+
- ⚖️ **Orphaned Load Balancers** - Load balancers with no backend address pools configured
14+
- 🔒 **Orphaned Private DNS Zones** - Private DNS zones with no virtual network links
15+
- 🔗 **Orphaned Private Endpoints** - Private endpoints with no connections or unapproved connection state
16+
- 🌉 **Orphaned Virtual Network Gateways** - Virtual network gateways with no IP configurations
17+
- 🛡️ **Orphaned DDoS Protection Plans** - DDoS protection plans with no associated virtual networks
1118

1219
These resources can accumulate silently over time, creating unnecessary costs. A single forgotten public IP might seem insignificant, but across multiple subscriptions and resource groups, orphaned resources can add up to hundreds or thousands of dollars per month.
1320

@@ -59,7 +66,14 @@ Once configured, you can ask Claude:
5966
-**Unattached Managed Disks** - Disks not attached to any VM
6067
-**Unattached Public IPs** - Public IPs with no configuration
6168
-**Orphaned App Service Plans** - Plans with no web apps
62-
-**Orphaned Load Balancers** - LBs with no backends
69+
-**Orphaned SQL Elastic Pools** - Elastic pools with no databases
70+
-**Orphaned Application Gateways** - Application gateways with no backend targets
71+
-**Orphaned NAT Gateways** - NAT gateways with no associated subnets
72+
-**Orphaned Load Balancers** - Load balancers with no backend address pools
73+
-**Orphaned Private DNS Zones** - Private DNS zones with no virtual network links
74+
-**Orphaned Private Endpoints** - Private endpoints with no or unapproved connections
75+
-**Orphaned Virtual Network Gateways** - Virtual network gateways with no IP configurations
76+
-**Orphaned DDoS Protection Plans** - DDoS protection plans with no associated virtual networks
6377

6478
## 💵 Cost Calculation:
6579

@@ -120,13 +134,6 @@ ID: `e4303b68-1de0-4a9d-ad35-5c3eb13c05e7`
120134

121135
## 🛠️ Troubleshooting:
122136

123-
### "No module named 'azure.mgmt.web'"
124-
125-
Install the missing package:
126-
```bash
127-
pip install azure-mgmt-web
128-
```
129-
130137
### "Authentication failed"
131138

132139
Verify you're logged in:

docs/TOOLS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ These tools require Azure authentication. See [FEATURES.md](FEATURES.md#orphaned
3939

4040
| Tool | Description |
4141
|------|-------------|
42-
| `find_orphaned_resources` | Detect orphaned resources (unattached disks, NICs, public IPs, NSGs, empty App Service Plans) and compute wasted cost |
42+
| `find_orphaned_resources` | Detect orphaned resources (unattached disks, public IPs, empty App Service Plans, SQL Elastic Pools, Application Gateways, NAT Gateways, Load Balancers, Private DNS Zones, Private Endpoints, Virtual Network Gateways, DDoS Protection Plans) and compute wasted cost |
4343

4444
---
4545

src/azure_pricing_mcp/formatters.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,9 @@ def format_orphaned_resources_response(result: dict[str, Any]) -> str:
634634
return (
635635
"### ✅ No Orphaned Resources Found\n\n"
636636
f"Scanned {len(subscriptions)} subscription(s) — "
637-
"no orphaned disks, NICs, public IPs, NSGs, or empty App Service Plans detected."
637+
"no orphaned disks, public IPs, App Service Plans, SQL Elastic Pools, "
638+
"Application Gateways, NAT Gateways, Load Balancers, Private DNS Zones, "
639+
"Private Endpoints, Virtual Network Gateways, or DDoS Protection Plans detected."
638640
)
639641

640642
# Collect all orphaned resources across subscriptions

src/azure_pricing_mcp/services/orphaned_resources.py

Lines changed: 97 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Orphaned Azure resource scanning and cost lookup service.
22
3-
This service detects orphaned resources (unattached disks, NICs, public IPs,
4-
NSGs, App Service Plans) across Azure subscriptions and computes their
5-
historical cost via the Azure Cost Management API.
3+
This service detects orphaned resources (unattached disks, public IPs,
4+
App Service Plans, SQL Elastic Pools, Application Gateways, NAT Gateways,
5+
Load Balancers, Private DNS Zones, Private Endpoints, Virtual Network Gateways,
6+
DDoS Protection Plans) across Azure subscriptions and computes their historical
7+
cost via the Azure Cost Management API.
68
79
All methods require Azure authentication. If not authenticated, they return
810
a friendly error message with instructions for how to authenticate.
@@ -27,9 +29,7 @@
2729
# Azure Resource Manager API versions per resource type
2830
ARM_API_VERSIONS = {
2931
"disks": "2023-10-02",
30-
"networkInterfaces": "2023-11-01",
3132
"publicIPAddresses": "2023-11-01",
32-
"networkSecurityGroups": "2023-11-01",
3333
"serverfarms": "2023-12-01",
3434
"subscriptions": "2022-12-01",
3535
}
@@ -46,16 +46,6 @@
4646
timeCreated = tostring(properties.timeCreated)
4747
"""
4848

49-
# Resource Graph query for orphaned NICs (not attached to any VM)
50-
ORPHANED_NICS_QUERY = """
51-
Resources
52-
| where type =~ 'microsoft.network/networkinterfaces'
53-
| where isnull(properties.virtualMachine.id) or properties.virtualMachine.id == ''
54-
| where isnull(properties.privateEndpoint.id) or properties.privateEndpoint.id == ''
55-
| project id, name, type, location, resourceGroup,
56-
subscriptionId
57-
"""
58-
5949
# Resource Graph query for orphaned public IPs (not associated)
6050
ORPHANED_PUBLIC_IPS_QUERY = """
6151
Resources
@@ -67,25 +57,100 @@
6757
allocationMethod = tostring(properties.publicIPAllocationMethod)
6858
"""
6959

70-
# Resource Graph query for orphaned NSGs (not associated with subnet or NIC)
71-
ORPHANED_NSGS_QUERY = """
60+
# Resource Graph query for empty App Service Plans
61+
ORPHANED_ASP_QUERY = """
62+
Resources
63+
| where type =~ 'microsoft.web/serverfarms'
64+
| where properties.numberOfSites == 0
65+
| project id, name, type, location, resourceGroup,
66+
subscriptionId,
67+
sku = tostring(sku.name),
68+
tier = tostring(sku.tier)
69+
"""
70+
71+
# Resource Graph query for orphaned SQL Elastic Pools (no databases)
72+
ORPHANED_SQL_ELASTIC_POOLS_QUERY = """
73+
Resources
74+
| where type =~ 'microsoft.sql/servers/elasticpools'
75+
| project id, name, type, location, resourceGroup,
76+
subscriptionId,
77+
sku = tostring(sku.name),
78+
tier = tostring(sku.tier),
79+
dtu = tostring(properties.dtu),
80+
serverName = tostring(split(id, '/')[8])
81+
"""
82+
83+
# Resource Graph query for orphaned Application Gateways (no backend targets)
84+
ORPHANED_APP_GATEWAYS_QUERY = """
85+
Resources
86+
| where type =~ 'microsoft.network/applicationgateways'
87+
| where isnull(properties.backendAddressPools) or array_length(properties.backendAddressPools) == 0
88+
or properties.backendAddressPools == '[]'
89+
| project id, name, type, location, resourceGroup,
90+
subscriptionId,
91+
sku = tostring(sku.name),
92+
tier = tostring(sku.tier)
93+
"""
94+
95+
# Resource Graph query for orphaned NAT Gateways (not associated with any subnet)
96+
ORPHANED_NAT_GATEWAYS_QUERY = """
7297
Resources
73-
| where type =~ 'microsoft.network/networksecuritygroups'
74-
| where isnull(properties.networkInterfaces) or array_length(properties.networkInterfaces) == 0
98+
| where type =~ 'microsoft.network/natgateways'
7599
| where isnull(properties.subnets) or array_length(properties.subnets) == 0
76100
| project id, name, type, location, resourceGroup,
77101
subscriptionId
78102
"""
79103

80-
# Resource Graph query for empty App Service Plans
81-
ORPHANED_ASP_QUERY = """
104+
# Resource Graph query for orphaned Load Balancers (no backend address pools)
105+
ORPHANED_LOAD_BALANCERS_QUERY = """
82106
Resources
83-
| where type =~ 'microsoft.web/serverfarms'
84-
| where properties.numberOfSites == 0
107+
| where type =~ 'microsoft.network/loadbalancers'
108+
| where isnull(properties.backendAddressPools) or array_length(properties.backendAddressPools) == 0
109+
| project id, name, type, location, resourceGroup,
110+
subscriptionId,
111+
sku = tostring(sku.name)
112+
"""
113+
114+
# Resource Graph query for orphaned Private DNS Zones (no virtual network links)
115+
ORPHANED_PRIVATE_DNS_ZONES_QUERY = """
116+
Resources
117+
| where type =~ 'microsoft.network/privatednszones'
118+
| where isnull(properties.numberOfVirtualNetworkLinks)
119+
or properties.numberOfVirtualNetworkLinks == 0
120+
| project id, name, type, location, resourceGroup,
121+
subscriptionId,
122+
recordSets = tostring(properties.numberOfRecordSets)
123+
"""
124+
125+
# Resource Graph query for orphaned Private Endpoints (no connections or not approved)
126+
ORPHANED_PRIVATE_ENDPOINTS_QUERY = """
127+
Resources
128+
| where type =~ 'microsoft.network/privateendpoints'
129+
| where isnull(properties.privateLinkServiceConnections)
130+
or array_length(properties.privateLinkServiceConnections) == 0
131+
or properties.privateLinkServiceConnections[0].properties.privateLinkServiceConnectionState.status != 'Approved'
132+
| project id, name, type, location, resourceGroup,
133+
subscriptionId
134+
"""
135+
136+
# Resource Graph query for orphaned Virtual Network Gateways (no IP configurations)
137+
ORPHANED_VNET_GATEWAYS_QUERY = """
138+
Resources
139+
| where type =~ 'microsoft.network/virtualnetworkgateways'
140+
| where isnull(properties.ipConfigurations) or array_length(properties.ipConfigurations) == 0
85141
| project id, name, type, location, resourceGroup,
86142
subscriptionId,
87143
sku = tostring(sku.name),
88-
tier = tostring(sku.tier)
144+
gatewayType = tostring(properties.gatewayType)
145+
"""
146+
147+
# Resource Graph query for orphaned DDoS Protection Plans (no virtual networks)
148+
ORPHANED_DDOS_PLANS_QUERY = """
149+
Resources
150+
| where type =~ 'microsoft.network/ddosprotectionplans'
151+
| where isnull(properties.virtualNetworks) or array_length(properties.virtualNetworks) == 0
152+
| project id, name, type, location, resourceGroup,
153+
subscriptionId
89154
"""
90155

91156

@@ -351,10 +416,16 @@ async def scan(
351416
# Execute all orphaned resource queries
352417
queries = {
353418
"Unattached Disk": ORPHANED_DISKS_QUERY,
354-
"Orphaned NIC": ORPHANED_NICS_QUERY,
355419
"Orphaned Public IP": ORPHANED_PUBLIC_IPS_QUERY,
356-
"Orphaned NSG": ORPHANED_NSGS_QUERY,
357420
"Empty App Service Plan": ORPHANED_ASP_QUERY,
421+
"Orphaned SQL Elastic Pool": ORPHANED_SQL_ELASTIC_POOLS_QUERY,
422+
"Orphaned Application Gateway": ORPHANED_APP_GATEWAYS_QUERY,
423+
"Orphaned NAT Gateway": ORPHANED_NAT_GATEWAYS_QUERY,
424+
"Orphaned Load Balancer": ORPHANED_LOAD_BALANCERS_QUERY,
425+
"Orphaned Private DNS Zone": ORPHANED_PRIVATE_DNS_ZONES_QUERY,
426+
"Orphaned Private Endpoint": ORPHANED_PRIVATE_ENDPOINTS_QUERY,
427+
"Orphaned Virtual Network Gateway": ORPHANED_VNET_GATEWAYS_QUERY,
428+
"Orphaned DDoS Protection Plan": ORPHANED_DDOS_PLANS_QUERY,
358429
}
359430

360431
all_orphaned: list[dict[str, Any]] = []

src/azure_pricing_mcp/tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def get_tool_definitions() -> list[Tool]:
347347
# Orphaned Resources Tool (requires Azure authentication)
348348
Tool(
349349
name="find_orphaned_resources",
350-
description="Detect orphaned Azure resources (unattached disks, NICs, public IPs, NSGs, empty App Service Plans) across subscriptions and compute their real historical cost via Azure Cost Management. Requires Azure authentication (az login or environment variables).",
350+
description="Detect orphaned Azure resources (unattached disks, public IPs, App Service Plans, SQL Elastic Pools, Application Gateways, NAT Gateways, Load Balancers, Private DNS Zones, Private Endpoints, Virtual Network Gateways, DDoS Protection Plans) across subscriptions and compute their real historical cost via Azure Cost Management. Requires Azure authentication (az login or environment variables).",
351351
inputSchema={
352352
"type": "object",
353353
"properties": {

0 commit comments

Comments
 (0)