From 2569a5e5d109c9fc317b4391561ec36dcb14c2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Can=20Bak=C4=B1r?= Date: Thu, 8 Oct 2026 16:37:02 +0300 Subject: [PATCH 1/2] azure: add service fabric clusters --- go.mod | 10 +- go.sum | 20 ++- pkg/providers/azure/azure.go | 12 +- pkg/providers/azure/servicefabric.go | 191 ++++++++++++++++++++++ pkg/providers/azure/servicefabric_test.go | 181 ++++++++++++++++++++ 5 files changed, 401 insertions(+), 13 deletions(-) create mode 100644 pkg/providers/azure/servicefabric.go create mode 100644 pkg/providers/azure/servicefabric_test.go diff --git a/go.mod b/go.mod index f8b9b6bf..e15bb905 100644 --- a/go.mod +++ b/go.mod @@ -154,8 +154,8 @@ require ( require ( cloud.google.com/go/asset v1.21.1 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement v1.1.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice v1.0.0 @@ -168,6 +168,8 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabric/armservicefabric v1.2.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabricmanagedclusters/armservicefabricmanagedclusters v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager v1.3.0 github.com/alitto/pond/v2 v2.3.2 @@ -187,7 +189,7 @@ require ( cloud.google.com/go/orgpolicy v1.15.0 // indirect cloud.google.com/go/osconfig v1.14.6 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect github.com/akrylysov/pogreb v0.10.1 // indirect @@ -201,7 +203,7 @@ require ( github.com/gaissmai/bart v0.29.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gofrs/flock v0.8.1 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/logrusorgru/aurora/v4 v4.0.0 // indirect diff --git a/go.sum b/go.sum index ce243d1b..0a26313a 100644 --- a/go.sum +++ b/go.sum @@ -23,10 +23,10 @@ cloud.google.com/go/osconfig v1.14.6 h1:4uJrA1obzMBp1I+DF15y/MvsXKIODevuANpq3Qhv cloud.google.com/go/osconfig v1.14.6/go.mod h1:LS39HDBH0IJDFgOUkhSZUHFQzmcWaCpYXLrc3A4CVzI= git.arvancloud.ir/arvancloud/cdn-go-sdk v0.12.1 h1:yYxJ5KSIcL8YJrQrKlcPZqvQQc4nK8VVrVkEK8J0NwY= git.arvancloud.ir/arvancloud/cdn-go-sdk v0.12.1/go.mod h1:ujHWzPDF3SdsJNwN2tBYZQyQJE9h0obkR7TYJRe8KO4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= @@ -63,14 +63,18 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1. github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0/go.mod h1:TpiwjwnW/khS0LKs4vW5UmmT9OWcxaveS8U7+tlknzo= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabric/armservicefabric v1.2.0 h1:3N7h+QCg+UPHkm5UjMPyD8yiDofLk4X+8idyyV27R4U= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabric/armservicefabric v1.2.0/go.mod h1:Msj1PiUuCxDqPEW23SJUtju8dLNTzuD3nJZA7VmJoKM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabricmanagedclusters/armservicefabricmanagedclusters v1.0.0 h1:xM0rIelm0xigMO/SUYqaXGWV3IJknzhemunL913bpn8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabricmanagedclusters/armservicefabricmanagedclusters v1.0.0/go.mod h1:+taqpiJmp4uF9NaGm7wWvuips6hYNXj3d690H51VAxo= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager v1.3.0 h1:e3kTG23M5ps+DjvPolK4dcgohDY8sHsXU7zrdHj1WzY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager v1.3.0/go.mod h1:Os5dq8Cvvz97rJauZhZJAfKHN+OEvF/0nVmHzF4aVys= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= @@ -217,8 +221,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/pkg/providers/azure/azure.go b/pkg/providers/azure/azure.go index 96221225..77c481b4 100644 --- a/pkg/providers/azure/azure.go +++ b/pkg/providers/azure/azure.go @@ -21,7 +21,7 @@ const ( providerName = "azure" ) -var Services = []string{"vm", "publicip", "trafficmanager", "cdn", "dns", "loadbalancer", "applicationgateway", "aks", "storage", "containerinstances", "appservice", "functions", "apimanagement", "frontdoor", "containerapps", "staticwebapps"} +var Services = []string{"vm", "publicip", "trafficmanager", "cdn", "dns", "loadbalancer", "applicationgateway", "aks", "storage", "containerinstances", "appservice", "functions", "apimanagement", "frontdoor", "containerapps", "staticwebapps", "servicefabric"} // Provider is a data provider for Azure API using Track 2 SDK type Provider struct { @@ -273,6 +273,16 @@ func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) { } resources.Merge(frontDoors) } + + if p.services.Has("servicefabric") { + sfp := &serviceFabricProvider{Credential: p.Credential, SubscriptionID: subscriptionID, id: p.id, extendedMetadata: p.extendedMetadata} + clusters, err := sfp.GetResource(ctx) + if err != nil { + gologger.Warning().Msgf("Error listing Service Fabric clusters for subscription %s: %s", subscriptionID, err) + continue + } + resources.Merge(clusters) + } } return resources, nil } diff --git a/pkg/providers/azure/servicefabric.go b/pkg/providers/azure/servicefabric.go new file mode 100644 index 00000000..52b3091b --- /dev/null +++ b/pkg/providers/azure/servicefabric.go @@ -0,0 +1,191 @@ +package azure + +import ( + "context" + "fmt" + "net/url" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabric/armservicefabric" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabricmanagedclusters/armservicefabricmanagedclusters" + "github.com/projectdiscovery/cloudlist/pkg/schema" +) + +// serviceFabricProvider is a provider for classic and managed Azure Service Fabric clusters +type serviceFabricProvider struct { + id string + SubscriptionID string + Credential azcore.TokenCredential + extendedMetadata bool + clientOptions *arm.ClientOptions +} + +// name returns the name of the provider +func (sp *serviceFabricProvider) name() string { + return "servicefabric" +} + +// GetResource returns the endpoints of classic and managed Service Fabric clusters +func (sp *serviceFabricProvider) GetResource(ctx context.Context) (*schema.Resources, error) { + list := schema.NewResources() + + // The two cluster kinds live in separate resource providers, so one being + // unregistered or forbidden must not hide the other. + classic, classicErr := sp.fetchClusters(ctx) + for _, cluster := range classic { + if cluster.Properties == nil || cluster.Properties.ManagementEndpoint == nil { + continue + } + endpoint, err := url.Parse(*cluster.Properties.ManagementEndpoint) + if err != nil || endpoint.Hostname() == "" { + continue + } + + var metadata map[string]string + if sp.extendedMetadata { + metadata = sp.getClusterMetadata(cluster) + } + + list.Append(&schema.Resource{ + Provider: providerName, + ID: sp.id, + DNSName: endpoint.Hostname(), + Service: sp.name(), + Metadata: metadata, + }) + } + + managed, managedErr := sp.fetchManagedClusters(ctx) + for _, cluster := range managed { + if cluster.Properties == nil { + continue + } + props := cluster.Properties + + var metadata map[string]string + if sp.extendedMetadata { + metadata = sp.getManagedClusterMetadata(cluster) + } + + list.Append(&schema.Resource{ + Provider: providerName, + ID: sp.id, + DNSName: stringValue(props.Fqdn), + PublicIPv4: stringValue(props.IPv4Address), + PublicIPv6: stringValue(props.IPv6Address), + Service: sp.name(), + Metadata: metadata, + }) + } + + if classicErr != nil && managedErr != nil { + return nil, fmt.Errorf("%w; %w", classicErr, managedErr) + } + return list, nil +} + +func (sp *serviceFabricProvider) fetchClusters(ctx context.Context) ([]*armservicefabric.Cluster, error) { + client, err := armservicefabric.NewClustersClient(sp.SubscriptionID, sp.Credential, sp.clientOptions) + if err != nil { + return nil, fmt.Errorf("failed to create Service Fabric clusters client: %w", err) + } + + // The classic API returns every cluster in the subscription in one response. + resp, err := client.List(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to list Service Fabric clusters: %w", err) + } + return resp.Value, nil +} + +func (sp *serviceFabricProvider) fetchManagedClusters(ctx context.Context) ([]*armservicefabricmanagedclusters.ManagedCluster, error) { + client, err := armservicefabricmanagedclusters.NewManagedClustersClient(sp.SubscriptionID, sp.Credential, sp.clientOptions) + if err != nil { + return nil, fmt.Errorf("failed to create Service Fabric managed clusters client: %w", err) + } + + var clusters []*armservicefabricmanagedclusters.ManagedCluster + pager := client.NewListBySubscriptionPager(nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list Service Fabric managed clusters: %w", err) + } + clusters = append(clusters, page.Value...) + } + return clusters, nil +} + +func (sp *serviceFabricProvider) getClusterMetadata(cluster *armservicefabric.Cluster) map[string]string { + metadata := sp.baseMetadata(cluster.Name, cluster.ID, cluster.Location, cluster.Type, cluster.Tags) + metadata["cluster_kind"] = "classic" + + props := cluster.Properties + schema.AddMetadata(metadata, "management_endpoint", props.ManagementEndpoint) + schema.AddMetadata(metadata, "cluster_code_version", props.ClusterCodeVersion) + schema.AddMetadata(metadata, "vm_image", props.VMImage) + if props.ClusterState != nil { + metadata["cluster_state"] = string(*props.ClusterState) + } + if props.ProvisioningState != nil { + metadata["provisioning_state"] = string(*props.ProvisioningState) + } + if props.ReliabilityLevel != nil { + metadata["reliability_level"] = string(*props.ReliabilityLevel) + } + return metadata +} + +func (sp *serviceFabricProvider) getManagedClusterMetadata(cluster *armservicefabricmanagedclusters.ManagedCluster) map[string]string { + metadata := sp.baseMetadata(cluster.Name, cluster.ID, cluster.Location, cluster.Type, cluster.Tags) + metadata["cluster_kind"] = "managed" + + props := cluster.Properties + schema.AddMetadata(metadata, "dns_name", props.DNSName) + schema.AddMetadata(metadata, "cluster_code_version", props.ClusterCodeVersion) + if props.ClientConnectionPort != nil { + metadata["client_connection_port"] = fmt.Sprintf("%d", *props.ClientConnectionPort) + } + if props.HTTPGatewayConnectionPort != nil { + metadata["http_gateway_connection_port"] = fmt.Sprintf("%d", *props.HTTPGatewayConnectionPort) + } + if props.ClusterState != nil { + metadata["cluster_state"] = string(*props.ClusterState) + } + if props.ProvisioningState != nil { + metadata["provisioning_state"] = string(*props.ProvisioningState) + } + if cluster.SKU != nil && cluster.SKU.Name != nil { + metadata["sku"] = string(*cluster.SKU.Name) + } + return metadata +} + +func (sp *serviceFabricProvider) baseMetadata(name, id, location, resourceType *string, tags map[string]*string) map[string]string { + metadata := make(map[string]string) + + schema.AddMetadata(metadata, "cluster_name", name) + schema.AddMetadata(metadata, "cluster_id", id) + metadata["subscription_id"] = sp.SubscriptionID + metadata["owner_id"] = sp.SubscriptionID + schema.AddMetadata(metadata, "location", location) + schema.AddMetadata(metadata, "type", resourceType) + + if id != nil { + if _, resourceGroup := parseAzureResourceID(*id); resourceGroup != "" { + metadata["resource_group"] = resourceGroup + } + } + if tagString := buildAzureTagString(tags); tagString != "" { + metadata["tags"] = tagString + } + return metadata +} + +func stringValue(s *string) string { + if s == nil { + return "" + } + return *s +} diff --git a/pkg/providers/azure/servicefabric_test.go b/pkg/providers/azure/servicefabric_test.go new file mode 100644 index 00000000..ecaeacab --- /dev/null +++ b/pkg/providers/azure/servicefabric_test.go @@ -0,0 +1,181 @@ +package azure + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/projectdiscovery/cloudlist/pkg/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testSubscriptionID = "00000000-0000-0000-0000-000000000000" + +type fakeCredential struct{} + +func (fakeCredential) GetToken(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) { + return azcore.AccessToken{Token: "fake", ExpiresOn: time.Now().Add(time.Hour)}, nil +} + +const classicClustersResponse = `{"value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ServiceFabric/clusters/classic1", + "name": "classic1", + "type": "Microsoft.ServiceFabric/clusters", + "location": "eastus", + "tags": {"env": "prod"}, + "properties": { + "managementEndpoint": "https://classic1.eastus.cloudapp.azure.com:19080", + "clusterEndpoint": "https://eastus.servicefabric.azure.com/runtime/clusters/abc", + "clusterState": "Ready", + "reliabilityLevel": "Silver", + "nodeTypes": [] + } + }, + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ServiceFabric/clusters/classic2", + "name": "classic2", + "location": "westus", + "properties": {"managementEndpoint": "https://20.42.0.10:19080", "nodeTypes": []} + }, + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ServiceFabric/clusters/no-endpoint", + "name": "no-endpoint", + "location": "westus", + "properties": {"nodeTypes": []} + } +]}` + +const managedClustersPage1 = `{"value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg2/providers/Microsoft.ServiceFabric/managedClusters/managed1", + "name": "managed1", + "location": "eastus", + "sku": {"name": "Standard"}, + "properties": { + "dnsName": "managed1", + "adminUserName": "admin", + "fqdn": "managed1.eastus.cloudapp.azure.com", + "ipv4Address": "20.42.0.20", + "ipv6Address": "2603:1030:20e:3::23c", + "clientConnectionPort": 19000, + "httpGatewayConnectionPort": 19080, + "clusterState": "Ready" + } + } +], "nextLink": "%s/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.ServiceFabric/managedClusters?page=2"}` + +const managedClustersPage2 = `{"value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg2/providers/Microsoft.ServiceFabric/managedClusters/managed2", + "name": "managed2", + "location": "westus", + "sku": {"name": "Basic"}, + "properties": {"dnsName": "managed2", "adminUserName": "admin", "fqdn": "managed2.westus.cloudapp.azure.com"} + } +]}` + +// newTestServiceFabricProvider points the real SDK clients at a fake ARM endpoint. +func newTestServiceFabricProvider(t *testing.T, classicStatus int) *serviceFabricProvider { + t.Helper() + var server *httptest.Server + server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch { + case strings.HasSuffix(r.URL.Path, "/providers/Microsoft.ServiceFabric/clusters"): + if classicStatus != http.StatusOK { + w.WriteHeader(classicStatus) + _, _ = w.Write([]byte(`{"error": {"code": "AuthorizationFailed", "message": "denied"}}`)) + return + } + _, _ = w.Write([]byte(classicClustersResponse)) + case strings.HasSuffix(r.URL.Path, "/providers/Microsoft.ServiceFabric/managedClusters"): + if r.URL.Query().Get("page") == "2" { + _, _ = w.Write([]byte(managedClustersPage2)) + return + } + _, _ = fmt.Fprintf(w, managedClustersPage1, server.URL) + default: + t.Errorf("unexpected request: %s", r.URL) + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(server.Close) + + return &serviceFabricProvider{ + id: "test", + SubscriptionID: testSubscriptionID, + Credential: fakeCredential{}, + extendedMetadata: true, + clientOptions: &arm.ClientOptions{ + ClientOptions: policy.ClientOptions{ + Cloud: cloud.Configuration{ + Services: map[cloud.ServiceName]cloud.ServiceConfiguration{ + cloud.ResourceManager: {Endpoint: server.URL, Audience: server.URL}, + }, + }, + Transport: server.Client(), + Retry: policy.RetryOptions{MaxRetries: -1}, + }, + }, + } +} + +func resourceByValue(resources *schema.Resources) map[string]*schema.Resource { + byValue := make(map[string]*schema.Resource) + for _, r := range resources.Items { + for _, v := range []string{r.DNSName, r.PublicIPv4, r.PublicIPv6} { + if v != "" { + byValue[v] = r + } + } + } + return byValue +} + +func TestServiceFabricGetResource(t *testing.T) { + resources, err := newTestServiceFabricProvider(t, http.StatusOK).GetResource(context.Background()) + require.NoError(t, err) + + byValue := resourceByValue(resources) + require.Len(t, byValue, 6, "classic endpoints, managed fqdn/ipv4/ipv6 across both pages") + + classic := byValue["classic1.eastus.cloudapp.azure.com"] + require.NotNil(t, classic, "classic management endpoint host must be extracted without scheme or port") + assert.Equal(t, "servicefabric", classic.Service) + assert.Equal(t, "classic", classic.Metadata["cluster_kind"]) + assert.Equal(t, "rg1", classic.Metadata["resource_group"]) + assert.Equal(t, "Silver", classic.Metadata["reliability_level"]) + assert.Equal(t, "env=prod", classic.Metadata["tags"]) + + assert.NotNil(t, byValue["20.42.0.10"], "classic endpoint addressed by IP must be kept") + assert.Nil(t, byValue["eastus.servicefabric.azure.com"], "the Azure-owned cluster endpoint is not an asset") + + managedIPv4 := byValue["20.42.0.20"] + require.NotNil(t, managedIPv4) + assert.True(t, managedIPv4.Public) + assert.Equal(t, "managed", managedIPv4.Metadata["cluster_kind"]) + assert.Equal(t, "19080", managedIPv4.Metadata["http_gateway_connection_port"]) + assert.Equal(t, "Standard", managedIPv4.Metadata["sku"]) + assert.NotNil(t, byValue["managed1.eastus.cloudapp.azure.com"]) + assert.NotNil(t, byValue["2603:1030:20e:3::23c"]) + assert.NotNil(t, byValue["managed2.westus.cloudapp.azure.com"], "second page must be fetched") +} + +func TestServiceFabricGetResourceClassicForbidden(t *testing.T) { + resources, err := newTestServiceFabricProvider(t, http.StatusForbidden).GetResource(context.Background()) + require.NoError(t, err, "a failing classic API must not hide managed clusters") + + byValue := resourceByValue(resources) + assert.NotNil(t, byValue["managed1.eastus.cloudapp.azure.com"]) + assert.Nil(t, byValue["classic1.eastus.cloudapp.azure.com"]) +} From 274bc5f17d81356357cf0f9ac02e436d16cdf241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Can=20Bak=C4=B1r?= Date: Thu, 8 Oct 2026 18:03:31 +0300 Subject: [PATCH 2/2] azure: warn when one service fabric listing fails --- pkg/providers/azure/servicefabric.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/providers/azure/servicefabric.go b/pkg/providers/azure/servicefabric.go index 52b3091b..03fc98fb 100644 --- a/pkg/providers/azure/servicefabric.go +++ b/pkg/providers/azure/servicefabric.go @@ -10,6 +10,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabric/armservicefabric" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicefabricmanagedclusters/armservicefabricmanagedclusters" "github.com/projectdiscovery/cloudlist/pkg/schema" + "github.com/projectdiscovery/gologger" ) // serviceFabricProvider is a provider for classic and managed Azure Service Fabric clusters @@ -82,6 +83,11 @@ func (sp *serviceFabricProvider) GetResource(ctx context.Context) (*schema.Resou if classicErr != nil && managedErr != nil { return nil, fmt.Errorf("%w; %w", classicErr, managedErr) } + for _, err := range []error{classicErr, managedErr} { + if err != nil { + gologger.Warning().Msgf("Error listing Service Fabric clusters for subscription %s: %s", sp.SubscriptionID, err) + } + } return list, nil }