mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-15 01:37:37 +01:00
700 lines
27 KiB
PowerShell
700 lines
27 KiB
PowerShell
#!/usr/bin/env pwsh
|
|
<#
|
|
.SYNOPSIS
|
|
Cosmos DB Connectivity Diagnostic Script
|
|
Captures local network connectivity, private network posture, and RBAC evidence.
|
|
|
|
.DESCRIPTION
|
|
This script performs comprehensive network and access diagnostics for Cosmos DB accounts.
|
|
It can run in interactive or non-interactive mode and produces a JSON report for triage.
|
|
|
|
.PARAMETER EndpointUrl
|
|
The Cosmos DB account endpoint URL.
|
|
Format: https://<account-name>.documents.azure.com or https://<account-name>.documents.azure.com:443/
|
|
WHERE TO GET: Azure Portal > Cosmos DB Account > Overview tab > URI field
|
|
OR: Use the endpoint shown in Cosmos Explorer connection string
|
|
|
|
.PARAMETER SubscriptionId
|
|
Azure subscription ID containing the Cosmos account.
|
|
WHERE TO GET: Azure Portal > Subscriptions > Copy Subscription ID
|
|
FORMAT: 12345678-1234-1234-1234-123456789012
|
|
|
|
.PARAMETER ResourceGroup
|
|
Azure resource group name containing the Cosmos account.
|
|
WHERE TO GET: Azure Portal > Cosmos DB Account > Resource group field (top-right)
|
|
|
|
.PARAMETER AccountName
|
|
Cosmos DB account name.
|
|
WHERE TO GET: Azure Portal > Cosmos DB Account > Account Name field
|
|
Or extract from endpoint URL (part before .documents.azure.com)
|
|
|
|
.PARAMETER PrivateEndpointIP
|
|
(Optional) Expected private endpoint IP if account uses private link.
|
|
WHERE TO GET: Azure Portal > Cosmos DB Account > Private Endpoint Connections tab > Private IP address column
|
|
|
|
.PARAMETER VpnSubnetRange
|
|
(Optional) Customer's VPN/client subnet CIDR for route analysis.
|
|
FORMAT: 10.0.0.0/24
|
|
WHERE TO GET: Ask your network team or check VPN client properties
|
|
|
|
.PARAMETER Interactive
|
|
If specified, script prompts for missing parameters instead of requiring them as arguments.
|
|
|
|
.PARAMETER Redact
|
|
If specified, output JSON redacts sensitive identifiers (tenant ID, subscription ID, usernames).
|
|
|
|
.EXAMPLE
|
|
# Interactive mode - script will prompt for inputs
|
|
.\Diagnose-CosmosConnectivity.ps1 -Interactive
|
|
|
|
.EXAMPLE
|
|
# Non-interactive with full parameters
|
|
.\Diagnose-CosmosConnectivity.ps1 `
|
|
-EndpointUrl "https://my-cosmos-account.documents.azure.com" `
|
|
-SubscriptionId "12345678-1234-1234-1234-123456789012" `
|
|
-ResourceGroup "my-rg" `
|
|
-AccountName "my-cosmos-account"
|
|
|
|
.EXAMPLE
|
|
# With private endpoint and output redaction
|
|
.\Diagnose-CosmosConnectivity.ps1 `
|
|
-EndpointUrl "https://my-cosmos-account.documents.azure.com" `
|
|
-SubscriptionId "12345678-1234-1234-1234-123456789012" `
|
|
-ResourceGroup "my-rg" `
|
|
-AccountName "my-cosmos-account" `
|
|
-PrivateEndpointIP "10.123.171.30" `
|
|
-Redact
|
|
#>
|
|
|
|
param(
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)]
|
|
[ValidateScript({$_ -match "^https://[a-z0-9-]+\.documents\.azure\.com" -or $_ -match "^https://[a-z0-9-]+\.documents\.azure\.com:443"})]
|
|
[string]$EndpointUrl,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)]
|
|
[guid]$SubscriptionId,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)]
|
|
[string]$ResourceGroup,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)]
|
|
[string]$AccountName,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)]
|
|
[string]$PrivateEndpointIP,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)]
|
|
[string]$VpnSubnetRange,
|
|
|
|
[switch]$Interactive,
|
|
|
|
[switch]$Redact
|
|
)
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
|
|
$ScriptVersion = "1.0.0"
|
|
$DiagnosticTimestamp = Get-Date -Format "o"
|
|
$TcpConnectTimeoutMs = 5000
|
|
$DnsTimeoutMs = 5000
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
function Show-InputInstructions {
|
|
Write-Host @"
|
|
═════════════════════════════════════════════════════════════════════════════
|
|
COSMOS DB CONNECTIVITY DIAGNOSTIC SCRIPT v$ScriptVersion
|
|
═════════════════════════════════════════════════════════════════════════════
|
|
|
|
This script will collect network and access diagnostics for your Cosmos DB account.
|
|
|
|
WHERE TO FIND YOUR INPUTS:
|
|
─────────────────────────────────────────────────────────────────────────────
|
|
|
|
1. ENDPOINT URL (Required)
|
|
Location: Azure Portal > Cosmos DB Account > Overview tab
|
|
Look for: "URI" field
|
|
Example: https://my-cosmos-account.documents.azure.com
|
|
⚠ Include https:// but do NOT include trailing slash or port suffix
|
|
|
|
2. SUBSCRIPTION ID (Required)
|
|
Location: Azure Portal > Subscriptions
|
|
Look for: "Subscription ID" column or click your subscription > Copy ID
|
|
Format: 12345678-1234-1234-1234-123456789012
|
|
|
|
3. RESOURCE GROUP (Required)
|
|
Location: Azure Portal > Cosmos DB Account > Top-right corner
|
|
Look for: "Resource group" field
|
|
Example: my-production-rg
|
|
|
|
4. ACCOUNT NAME (Required)
|
|
Location: Either extract from endpoint URL or find in portal
|
|
From URL: Take the part before ".documents.azure.com"
|
|
From Portal: Account name appears in the breadcrumb and overview
|
|
Example: my-cosmos-account
|
|
|
|
5. PRIVATE ENDPOINT IP (Optional, but recommended)
|
|
Location: Azure Portal > Cosmos DB Account > Private Endpoint Connections
|
|
Look for: "Private IP address" column (only if private endpoints exist)
|
|
Format: 10.123.171.30 (will be 10.x.x.x or 172.16-31.x.x range)
|
|
Skip this if: You are using public endpoint only
|
|
|
|
6. VPN SUBNET RANGE (Optional)
|
|
Location: Ask your network team or VPN client settings
|
|
Used to: Analyze if routing from your network to private endpoint is blocked
|
|
Format: 10.0.0.0/24 (CIDR notation)
|
|
Skip this if: You are not using a VPN
|
|
|
|
═════════════════════════════════════════════════════════════════════════════
|
|
|
|
"@
|
|
}
|
|
|
|
function Read-InputsInteractively {
|
|
Show-InputInstructions
|
|
|
|
Write-Host "Please provide the following information:" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
# Endpoint URL
|
|
do {
|
|
$endpoint = Read-Host "Endpoint URL (e.g., https://my-cosmos.documents.azure.com)"
|
|
if ($endpoint -notmatch "^https://[a-z0-9-]+\.documents\.azure\.com") {
|
|
Write-Host "Invalid format. Expected: https://<account-name>.documents.azure.com" -ForegroundColor Yellow
|
|
}
|
|
} while ($endpoint -notmatch "^https://[a-z0-9-]+\.documents\.azure\.com")
|
|
|
|
# Subscription ID
|
|
do {
|
|
$subId = Read-Host "Subscription ID (12345678-1234-1234-1234-123456789012)"
|
|
if ($subId -notmatch "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") {
|
|
Write-Host "Invalid format. Expected GUID format." -ForegroundColor Yellow
|
|
}
|
|
} while ($subId -notmatch "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
|
|
|
$rg = Read-Host "Resource Group name"
|
|
$account = Read-Host "Account Name"
|
|
$peIP = Read-Host "Private Endpoint IP (optional, press Enter to skip)"
|
|
$vpnSubnet = Read-Host "VPN Subnet Range (optional, e.g., 10.0.0.0/24, press Enter to skip)"
|
|
|
|
return @{
|
|
EndpointUrl = $endpoint
|
|
SubscriptionId = [guid]$subId
|
|
ResourceGroup = $rg
|
|
AccountName = $account
|
|
PrivateEndpointIP = if ($peIP) { $peIP } else { $null }
|
|
VpnSubnetRange = if ($vpnSubnet) { $vpnSubnet } else { $null }
|
|
}
|
|
}
|
|
|
|
function Invoke-DnsResolution {
|
|
param([string]$Hostname)
|
|
|
|
$result = @{
|
|
hostname = $Hostname
|
|
succeeded = $false
|
|
addresses = @()
|
|
error = $null
|
|
dnsServers = @()
|
|
latencyMs = 0
|
|
}
|
|
|
|
try {
|
|
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
|
$addresses = [System.Net.Dns]::GetHostAddresses($Hostname)
|
|
$stopwatch.Stop()
|
|
|
|
$result.succeeded = $true
|
|
$result.addresses = @($addresses | ForEach-Object { $_.ToString() })
|
|
$result.latencyMs = [int]$stopwatch.ElapsedMilliseconds
|
|
|
|
# Try to get DNS servers (Windows/Linux specific)
|
|
if ($PSVersionTable.Platform -ne "Unix" -or $PSVersionTable.OS -like "*Linux*") {
|
|
try {
|
|
$dnsConfig = Get-DnsClientServerAddress -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($dnsConfig) {
|
|
$result.dnsServers = @($dnsConfig.ServerAddresses)
|
|
}
|
|
} catch { }
|
|
}
|
|
} catch {
|
|
$result.error = $_.Exception.Message
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Invoke-TcpConnectivityTest {
|
|
param(
|
|
[string]$Hostname,
|
|
[int]$Port = 443,
|
|
[int]$TimeoutMs = 5000
|
|
)
|
|
|
|
$result = @{
|
|
hostname = $Hostname
|
|
port = $Port
|
|
succeeded = $false
|
|
error = $null
|
|
latencyMs = 0
|
|
sourceIp = $null
|
|
}
|
|
|
|
try {
|
|
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
|
$tcpClient = New-Object System.Net.Sockets.TcpClient
|
|
$task = $tcpClient.ConnectAsync($Hostname, $Port)
|
|
$task.Wait($TimeoutMs)
|
|
$stopwatch.Stop()
|
|
|
|
if ($task.IsCompleted) {
|
|
$result.succeeded = $true
|
|
$result.latencyMs = [int]$stopwatch.ElapsedMilliseconds
|
|
|
|
# Try to get source IP
|
|
try {
|
|
$endpoint = $tcpClient.Client.LocalEndPoint
|
|
$result.sourceIp = $endpoint.Address.ToString()
|
|
} catch { }
|
|
} else {
|
|
$result.error = "Connection timeout after ${TimeoutMs}ms"
|
|
}
|
|
|
|
$tcpClient.Close()
|
|
} catch {
|
|
$result.error = $_.Exception.Message
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Invoke-HttpsProbe {
|
|
param([string]$Url)
|
|
|
|
$result = @{
|
|
url = $Url
|
|
succeeded = $false
|
|
statusCode = $null
|
|
error = $null
|
|
latencyMs = 0
|
|
}
|
|
|
|
try {
|
|
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
|
$response = Invoke-WebRequest -Uri $Url -Method Head -TimeoutSec 5 -ErrorAction Stop
|
|
$stopwatch.Stop()
|
|
|
|
$result.succeeded = $true
|
|
$result.statusCode = [int]$response.StatusCode
|
|
$result.latencyMs = [int]$stopwatch.ElapsedMilliseconds
|
|
} catch {
|
|
$result.statusCode = [int]($_.Exception.Response.StatusCode)
|
|
$result.error = $_.Exception.Message
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Get-PrivateNetworkIndicators {
|
|
param(
|
|
[string[]]$ResolvedAddresses,
|
|
[string]$PrivateEndpointIP,
|
|
[string]$VpnSubnetRange
|
|
)
|
|
|
|
$result = @{
|
|
isPrivateRange = $false
|
|
indicators = @()
|
|
matchesExpectedPrivateEndpoint = $false
|
|
vpnRouteWarning = $null
|
|
}
|
|
|
|
# Check if resolved IPs are private range
|
|
foreach ($addr in $ResolvedAddresses) {
|
|
if (IsPrivateIpAddress $addr) {
|
|
$result.isPrivateRange = $true
|
|
$result.indicators += "Resolved to RFC 1918 private IP range ($addr)"
|
|
}
|
|
}
|
|
|
|
# Check if matches expected private endpoint
|
|
if ($PrivateEndpointIP -and $ResolvedAddresses -contains $PrivateEndpointIP) {
|
|
$result.matchesExpectedPrivateEndpoint = $true
|
|
$result.indicators += "Matches expected private endpoint IP ($PrivateEndpointIP)"
|
|
} elseif ($PrivateEndpointIP -and $ResolvedAddresses.Count -gt 0) {
|
|
$result.indicators += "WARNING: Resolved to $($ResolvedAddresses[0]) but expected private endpoint IP is $PrivateEndpointIP"
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function IsPrivateIpAddress {
|
|
param([string]$IpAddress)
|
|
|
|
try {
|
|
$ip = [System.Net.IPAddress]::Parse($IpAddress)
|
|
# RFC 1918 ranges
|
|
if ($ip.ToString() -match "^10\." -or $ip.ToString() -match "^172\.(1[6-9]|2[0-9]|3[01])\." -or $ip.ToString() -match "^192\.168\.") {
|
|
return $true
|
|
}
|
|
# Loopback
|
|
if ($ip.AddressFamily -eq "InterNetwork" -and $ip.GetAddressBytes()[0] -eq 127) {
|
|
return $true
|
|
}
|
|
} catch { }
|
|
|
|
return $false
|
|
}
|
|
|
|
function Get-AzureCliContext {
|
|
$result = @{
|
|
installed = $false
|
|
authenticated = $false
|
|
currentUser = $null
|
|
currentTenant = $null
|
|
currentSubscription = $null
|
|
error = $null
|
|
}
|
|
|
|
try {
|
|
$output = & az --version 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
$result.installed = $true
|
|
}
|
|
} catch {
|
|
$result.error = "Azure CLI not found. Skipping Azure context checks."
|
|
return $result
|
|
}
|
|
|
|
try {
|
|
$account = & az account show 2>&1 | ConvertFrom-Json
|
|
$result.authenticated = $true
|
|
$result.currentUser = $account.user.name
|
|
$result.currentTenant = $account.tenantId
|
|
$result.currentSubscription = $account.id
|
|
} catch {
|
|
$result.error = "Not authenticated with Azure CLI. Run 'az login' to proceed with Azure checks."
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Get-AzureAccountNetworkConfig {
|
|
param(
|
|
[guid]$SubscriptionId,
|
|
[string]$ResourceGroup,
|
|
[string]$AccountName
|
|
)
|
|
|
|
$result = @{
|
|
checked = $false
|
|
publicNetworkAccessRestricted = $null
|
|
privateEndpoints = @()
|
|
vnetRules = @()
|
|
error = $null
|
|
}
|
|
|
|
try {
|
|
$scope = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.DocumentDB/databaseAccounts/$AccountName"
|
|
$account = & az cosmosdb show --resource-group $ResourceGroup --name $AccountName 2>&1 | ConvertFrom-Json
|
|
|
|
if ($account) {
|
|
$result.checked = $true
|
|
$result.publicNetworkAccessRestricted = $account.properties.publicNetworkAccess -eq "Disabled"
|
|
|
|
# Get private endpoints
|
|
$peConnections = & az cosmosdb private-endpoint-connection list --resource-group $ResourceGroup --name $AccountName 2>&1 | ConvertFrom-Json
|
|
if ($peConnections) {
|
|
$result.privateEndpoints = @($peConnections | Select-Object -Property id, @{n='state';e={$_.properties.privateLinkServiceConnectionState.status}})
|
|
}
|
|
}
|
|
} catch {
|
|
$result.error = $_.Exception.Message
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Get-RbacAssessment {
|
|
param(
|
|
[guid]$SubscriptionId,
|
|
[string]$ResourceGroup,
|
|
[string]$AccountName
|
|
)
|
|
|
|
$result = @{
|
|
checked = $false
|
|
canReadAccount = $false
|
|
canManageAccount = $false
|
|
canExecuteDataPlaneOps = $false
|
|
roleAssignments = @()
|
|
classification = "unknown"
|
|
error = $null
|
|
}
|
|
|
|
try {
|
|
$scope = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.DocumentDB/databaseAccounts/$AccountName"
|
|
|
|
# Try to read account (implies Reader or higher)
|
|
$account = & az cosmosdb show --resource-group $ResourceGroup --name $AccountName 2>&1 | ConvertFrom-Json
|
|
if ($account) {
|
|
$result.checked = $true
|
|
$result.canReadAccount = $true
|
|
|
|
# Check role assignments
|
|
$roles = & az role assignment list --scope $scope 2>&1 | ConvertFrom-Json
|
|
if ($roles) {
|
|
$result.roleAssignments = @($roles | Select-Object -Property roleDefinitionName, principalName)
|
|
|
|
# Classify permissions
|
|
$roleNames = $roles | Select-Object -ExpandProperty roleDefinitionName
|
|
if ($roleNames -contains "Contributor" -or $roleNames -contains "Owner") {
|
|
$result.canManageAccount = $true
|
|
$result.canExecuteDataPlaneOps = $true
|
|
$result.classification = "sufficient"
|
|
} elseif ($roleNames -contains "Cosmos DB Operator" -or $roleNames -contains "Cosmos DB Account Reader") {
|
|
$result.canExecuteDataPlaneOps = $true
|
|
$result.classification = "partial"
|
|
} else {
|
|
$result.classification = "partial"
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
$result.error = $_.Exception.Message
|
|
$result.classification = "insufficient"
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Invoke-Classification {
|
|
param(
|
|
[hashtable]$DnsResult,
|
|
[hashtable]$TcpResult,
|
|
[hashtable]$PrivateNetworkIndicators,
|
|
[hashtable]$AzureNetworkConfig
|
|
)
|
|
|
|
$classification = @{
|
|
status = "unknown"
|
|
code = "unknown"
|
|
summary = "Unable to classify"
|
|
rootCause = $null
|
|
recommendedActions = @()
|
|
}
|
|
|
|
# DNS failure
|
|
if (-not $DnsResult.succeeded) {
|
|
$classification.status = "failure"
|
|
$classification.code = "dns_resolution_failed"
|
|
$classification.summary = "DNS resolution failed. The Cosmos DB endpoint hostname cannot be resolved."
|
|
$classification.rootCause = "DNS configuration, VPN/proxy DNS redirect, or network connectivity issue"
|
|
$classification.recommendedActions = @(
|
|
"1. Check if you are connected to corporate VPN or proxy that intercepts DNS",
|
|
"2. Manually run: nslookup $($DnsResult.hostname)",
|
|
"3. If nslookup fails, check with your network team or ISP",
|
|
"4. Try pinging the endpoint or using nslookup with alternate DNS: nslookup $($DnsResult.hostname) 8.8.8.8"
|
|
)
|
|
return $classification
|
|
}
|
|
|
|
# DNS succeeded but TCP failed
|
|
if ($DnsResult.succeeded -and -not $TcpResult.succeeded) {
|
|
$classification.status = "failure"
|
|
$classification.code = "tcp_connectivity_blocked"
|
|
$classification.summary = "DNS resolution succeeded but TCP 443 connection failed. Network path is blocked."
|
|
|
|
if ($PrivateNetworkIndicators.isPrivateRange) {
|
|
$classification.rootCause = "Private endpoint configured but network path blocked (VPN routing, firewall/NVA, NSG, UDR, or peering issue)"
|
|
$classification.recommendedActions = @(
|
|
"1. Verify VPN connectivity and that your client subnet can route to the private endpoint subnet",
|
|
"2. Ask your network team to verify routing between $([System.Net.Dns]::GetHostName()) and private endpoint $($DnsResult.addresses[0])",
|
|
"3. Check Azure network security groups (NSGs) rules for port 443 inbound",
|
|
"4. Verify Azure Virtual Network peering and User Defined Routes (UDRs)",
|
|
"5. Check if corporate firewall/NVA is blocking the connection",
|
|
"6. Manually run: Test-NetConnection -ComputerName $($DnsResult.hostname) -Port 443"
|
|
)
|
|
} else {
|
|
$classification.rootCause = "Public endpoint network path blocked (firewall, proxy, ISP, or regional restriction)"
|
|
$classification.recommendedActions = @(
|
|
"1. Check if corporate firewall is blocking outbound port 443",
|
|
"2. If behind proxy, verify proxy settings allow HTTPS to documents.azure.com",
|
|
"3. Manually run: Test-NetConnection -ComputerName $($DnsResult.hostname) -Port 443",
|
|
"4. Try connecting from a different network to isolate the issue"
|
|
)
|
|
}
|
|
return $classification
|
|
}
|
|
|
|
# Both succeeded
|
|
if ($DnsResult.succeeded -and $TcpResult.succeeded) {
|
|
$classification.status = "success"
|
|
$classification.code = "network_connectivity_healthy"
|
|
$classification.summary = "Network connectivity is healthy. DNS resolves and TCP 443 is reachable."
|
|
$classification.rootCause = $null
|
|
$classification.recommendedActions = @(
|
|
"✓ Local network connectivity is working",
|
|
"If Cosmos DB operations still fail, check:",
|
|
" - RBAC/authentication permissions",
|
|
" - Account firewall IP rules (if enabled)",
|
|
" - Data plane token expiry",
|
|
" - Application-level issues (connection strings, SDK versions)"
|
|
)
|
|
return $classification
|
|
}
|
|
|
|
return $classification
|
|
}
|
|
|
|
function Redact-Sensitive {
|
|
param([object]$Object)
|
|
|
|
if (-not $Redact) { return $Object }
|
|
|
|
$json = $Object | ConvertTo-Json -Depth 10
|
|
$json = $json -replace [regex]::Escape($SubscriptionId.ToString()), "REDACTED-SUBSCRIPTION-ID"
|
|
|
|
# Redact tenant IDs (GUIDs in certain fields)
|
|
$json = $json -replace '"currentTenant"\s*:\s*"[^"]*"', '"currentTenant": "REDACTED-TENANT-ID"'
|
|
|
|
# Redact user names
|
|
$json = $json -replace '"currentUser"\s*:\s*"[^"]*"', '"currentUser": "REDACTED-USER-NAME"'
|
|
$json = $json -replace '"principalName"\s*:\s*"[^"]*"', '"principalName": "REDACTED-PRINCIPAL-NAME"'
|
|
|
|
return $json | ConvertFrom-Json
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main Execution
|
|
# ============================================================================
|
|
|
|
try {
|
|
# Validate and collect inputs
|
|
if ($Interactive -and -not $EndpointUrl) {
|
|
$inputs = Read-InputsInteractively
|
|
$EndpointUrl = $inputs.EndpointUrl
|
|
$SubscriptionId = $inputs.SubscriptionId
|
|
$ResourceGroup = $inputs.ResourceGroup
|
|
$AccountName = $inputs.AccountName
|
|
$PrivateEndpointIP = $inputs.PrivateEndpointIP
|
|
$VpnSubnetRange = $inputs.VpnSubnetRange
|
|
} elseif (-not $EndpointUrl) {
|
|
Write-Host "No endpoint URL provided. Use -Interactive flag or provide parameters." -ForegroundColor Red
|
|
Show-InputInstructions
|
|
exit 1
|
|
}
|
|
|
|
# Extract hostname from URL
|
|
$uri = [System.Uri]$EndpointUrl
|
|
$hostname = $uri.Host
|
|
|
|
Write-Host "Collecting diagnostics for: $hostname" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
# Run diagnostics
|
|
Write-Host "[1/5] DNS Resolution..." -ForegroundColor Cyan
|
|
$dnsResult = Invoke-DnsResolution -Hostname $hostname
|
|
|
|
Write-Host "[2/5] TCP Connectivity (port 443)..." -ForegroundColor Cyan
|
|
$tcpResult = Invoke-TcpConnectivityTest -Hostname $hostname -Port 443 -TimeoutMs $TcpConnectTimeoutMs
|
|
|
|
Write-Host "[3/5] HTTPS Probe..." -ForegroundColor Cyan
|
|
$httpsResult = Invoke-HttpsProbe -Url $EndpointUrl
|
|
|
|
Write-Host "[4/5] Private Network Analysis..." -ForegroundColor Cyan
|
|
$privateNetIndicators = Get-PrivateNetworkIndicators -ResolvedAddresses $dnsResult.addresses -PrivateEndpointIP $PrivateEndpointIP -VpnSubnetRange $VpnSubnetRange
|
|
|
|
Write-Host "[5/5] Azure Configuration & RBAC..." -ForegroundColor Cyan
|
|
$cliContext = Get-AzureCliContext
|
|
$networkConfig = @{ checked = $false; error = "Skipped" }
|
|
$rbacAssessment = @{ checked = $false; classification = "unknown"; error = "Skipped" }
|
|
|
|
if ($cliContext.authenticated -and $SubscriptionId -and $ResourceGroup -and $AccountName) {
|
|
$networkConfig = Get-AzureAccountNetworkConfig -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -AccountName $AccountName
|
|
$rbacAssessment = Get-RbacAssessment -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -AccountName $AccountName
|
|
} elseif (-not $cliContext.authenticated) {
|
|
Write-Host " ⚠ Azure CLI not authenticated. Skipping Azure checks. Run 'az login' to enable." -ForegroundColor Yellow
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Generating classification..." -ForegroundColor Cyan
|
|
$classification = Invoke-Classification -DnsResult $dnsResult -TcpResult $tcpResult -PrivateNetworkIndicators $privateNetIndicators -AzureNetworkConfig $networkConfig
|
|
|
|
# Build final report
|
|
$report = @{
|
|
version = $ScriptVersion
|
|
timestamp = $DiagnosticTimestamp
|
|
target = @{
|
|
endpointUrl = if ($Redact) { "REDACTED" } else { $EndpointUrl }
|
|
hostname = $hostname
|
|
subscriptionId = if ($Redact -and $SubscriptionId) { "REDACTED" } else { $SubscriptionId.ToString() }
|
|
resourceGroup = if ($Redact -and $ResourceGroup) { "REDACTED" } else { $ResourceGroup }
|
|
accountName = if ($Redact -and $AccountName) { "REDACTED" } else { $AccountName }
|
|
}
|
|
execution = @{
|
|
hostname = [System.Net.Dns]::GetHostName()
|
|
platform = $PSVersionTable.OS
|
|
powershellVersion = $PSVersionTable.PSVersion.ToString()
|
|
}
|
|
diagnostics = @{
|
|
dns = $dnsResult
|
|
tcp = $tcpResult
|
|
https = $httpsResult
|
|
privateNetwork = $privateNetIndicators
|
|
azureNetworkConfig = $networkConfig
|
|
rbac = $rbacAssessment
|
|
azureCli = $cliContext
|
|
}
|
|
classification = $classification
|
|
}
|
|
|
|
# Redact if requested
|
|
if ($Redact) {
|
|
$report = Redact-Sensitive -Object $report
|
|
}
|
|
|
|
# Output JSON report
|
|
$jsonReport = $report | ConvertTo-Json -Depth 10
|
|
|
|
# Save to file
|
|
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
|
$outputFile = "cosmos-diagnostic-$timestamp.json"
|
|
$jsonReport | Out-File -FilePath $outputFile -Encoding UTF8
|
|
|
|
Write-Host ""
|
|
Write-Host "═════════════════════════════════════════════════════════════════════════════" -ForegroundColor Green
|
|
Write-Host "DIAGNOSTIC COMPLETE" -ForegroundColor Green
|
|
Write-Host "═════════════════════════════════════════════════════════════════════════════" -ForegroundColor Green
|
|
Write-Host ""
|
|
Write-Host "Summary:" -ForegroundColor Cyan
|
|
Write-Host " DNS Resolution: $(if ($dnsResult.succeeded) { '✓ PASS' } else { '✗ FAIL' })"
|
|
Write-Host " TCP Connectivity: $(if ($tcpResult.succeeded) { '✓ PASS' } else { '✗ FAIL' })"
|
|
Write-Host " Private Network: $(if ($privateNetIndicators.isPrivateRange) { 'Detected (Private Endpoint)' } else { 'Not Detected (Public Endpoint)' })"
|
|
Write-Host " Classification: $($classification.status.ToUpper()) - $($classification.code)"
|
|
Write-Host ""
|
|
Write-Host "Full report saved to: $outputFile" -ForegroundColor Green
|
|
Write-Host ""
|
|
Write-Host "Summary:" -ForegroundColor Yellow
|
|
Write-Host $classification.summary
|
|
Write-Host ""
|
|
if ($classification.recommendedActions.Count -gt 0) {
|
|
Write-Host "Recommended Actions:" -ForegroundColor Yellow
|
|
$classification.recommendedActions | ForEach-Object { Write-Host " $_" }
|
|
}
|
|
Write-Host ""
|
|
|
|
# Output JSON to console for easy copy/paste
|
|
Write-Host "Full JSON Report:" -ForegroundColor Cyan
|
|
Write-Host "─────────────────────────────────────────────────────────────────────────────"
|
|
Write-Host $jsonReport
|
|
|
|
} catch {
|
|
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
|
|
exit 1
|
|
}
|