diff --git a/go.mod b/go.mod index 684ea733fc..48fc23fdfb 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/anchore/syft v1.32.0 github.com/aquilax/truncate v1.0.0 - github.com/armosec/armoapi-go v0.0.663 + github.com/armosec/armoapi-go v0.0.667 github.com/armosec/utils-k8s-go v0.0.35 github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.3.0 @@ -33,7 +33,7 @@ require ( github.com/joncrlsn/dque v0.0.0-20241024143830-7723fd131a64 github.com/kubescape/backend v0.0.25 github.com/kubescape/go-logger v0.0.24 - github.com/kubescape/k8s-interface v0.0.200 + github.com/kubescape/k8s-interface v0.0.201 github.com/kubescape/storage v0.0.221 github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf github.com/moby/sys/mountinfo v0.7.2 diff --git a/go.sum b/go.sum index 4249ce6e70..2c527111a0 100644 --- a/go.sum +++ b/go.sum @@ -761,8 +761,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/armosec/armoapi-go v0.0.663 h1:Ht8eBIY8y3VbFhtvfzdwjMsgiVX7K3dURp6qfBwz8Jo= -github.com/armosec/armoapi-go v0.0.663/go.mod h1:9jAH0g8ZsryhiBDd/aNMX4+n10bGwTx/doWCyyjSxts= +github.com/armosec/armoapi-go v0.0.667 h1:LrFowKvthnL676Gx+hjhvqP4pQ2+CjykFO9SdIYDc/c= +github.com/armosec/armoapi-go v0.0.667/go.mod h1:9jAH0g8ZsryhiBDd/aNMX4+n10bGwTx/doWCyyjSxts= github.com/armosec/gojay v1.2.17 h1:VSkLBQzD1c2V+FMtlGFKqWXNsdNvIKygTKJI9ysY8eM= github.com/armosec/gojay v1.2.17/go.mod h1:vuvX3DlY0nbVrJ0qCklSS733AWMoQboq3cFyuQW9ybc= github.com/armosec/utils-go v0.0.58 h1:g9RnRkxZAmzTfPe2ruMo2OXSYLwVSegQSkSavOfmaIE= @@ -1469,8 +1469,8 @@ github.com/kubescape/backend v0.0.25 h1:PLESA7KGJskebR5hiSqPeJ1cPQ8Ra+4yNYXKyIej github.com/kubescape/backend v0.0.25/go.mod h1:FpazfN+c3Ucuvv4jZYCnk99moSBRNMVIxl5aWCZAEBo= github.com/kubescape/go-logger v0.0.24 h1:JRNlblY16Ty7hD6MSYNPvWYDxNzVAufsDDX/sZJayL0= github.com/kubescape/go-logger v0.0.24/go.mod h1:sMPVCr3VpW/e+SeMaXig5kClGvmZbDXN8YktUeNU4nY= -github.com/kubescape/k8s-interface v0.0.200 h1:Ff64dlDigg8dDYJuaeLFFjfTCHQNC1SStWNECWFRCYE= -github.com/kubescape/k8s-interface v0.0.200/go.mod h1:j9snZbH+RxOaa1yG/bWgTClj90q7To0rGgQepxy4b+k= +github.com/kubescape/k8s-interface v0.0.201 h1:gBONxCiRr3xcllsSjZDd/gNM6vpIKM8mPffnfVYmmfk= +github.com/kubescape/k8s-interface v0.0.201/go.mod h1:d4NVhL81bVXe8yEXlkT4ZHrt3iEppEIN39b8N1oXm5s= github.com/kubescape/storage v0.0.221 h1:HLWnNokkKgKo9ka/p797fFQdsbzKxSXT5/RpUWrKWzI= github.com/kubescape/storage v0.0.221/go.mod h1:L/fF3teor8cUj80TVujqy9E1rKsf+Dox2hZtkS1vjOU= github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf h1:hI0jVwrB6fT4GJWvuUjzObfci1CUknrZdRHfnRVtKM0= diff --git a/pkg/cloudmetadata/metadata.go b/pkg/cloudmetadata/metadata.go index c752632a95..d0469c68c3 100644 --- a/pkg/cloudmetadata/metadata.go +++ b/pkg/cloudmetadata/metadata.go @@ -8,7 +8,7 @@ import ( "strings" "time" - apitypes "github.com/armosec/armoapi-go/armotypes" + "github.com/armosec/armoapi-go/armotypes" "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" k8sInterfaceCloudMetadata "github.com/kubescape/k8s-interface/cloudmetadata" @@ -22,7 +22,7 @@ const ( ) // GetCloudMetadata retrieves cloud metadata for a given node -func GetCloudMetadata(ctx context.Context, client *k8sinterface.KubernetesApi, nodeName string) (*apitypes.CloudMetadata, error) { +func GetCloudMetadata(ctx context.Context, client *k8sinterface.KubernetesApi, nodeName string) (*armotypes.CloudMetadata, error) { node, err := client.GetKubernetesClient().CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("failed to get node %s: %v", nodeName, err) @@ -38,8 +38,8 @@ func GetCloudMetadata(ctx context.Context, client *k8sinterface.KubernetesApi, n return cMetadata, nil } -func enrichCloudMetadataForAWS(ctx context.Context, client *k8sinterface.KubernetesApi, cMetadata *apitypes.CloudMetadata) { - if cMetadata == nil || cMetadata.Provider != k8sInterfaceCloudMetadata.ProviderAWS || cMetadata.AccountID != "" { +func enrichCloudMetadataForAWS(ctx context.Context, client *k8sinterface.KubernetesApi, cMetadata *armotypes.CloudMetadata) { + if cMetadata == nil || cMetadata.Provider != armotypes.ProviderAws || cMetadata.AccountID != "" { return } @@ -58,7 +58,7 @@ func enrichCloudMetadataForAWS(ctx context.Context, client *k8sinterface.Kuberne } // GetCloudMetadataWithIMDS retrieves cloud metadata for a given node using IMDS -func GetCloudMetadataWithIMDS(ctx context.Context) (*apitypes.CloudMetadata, error) { +func GetCloudMetadataWithIMDS(ctx context.Context) (*armotypes.CloudMetadata, error) { cMetadataClient := k8sInterfaceCloudMetadata.NewMetadataClient(true) cMetadata, err := cMetadataClient.GetMetadata(ctx) @@ -71,11 +71,16 @@ func GetCloudMetadataWithIMDS(ctx context.Context) (*apitypes.CloudMetadata, err // Fallback strategy: try different providers fallbacks := []struct { name string - fetch func(context.Context) (*apitypes.CloudMetadata, error) + fetch func(context.Context) (*armotypes.CloudMetadata, error) }{ - {name: "DigitalOcean", fetch: fetchDigitalOceanMetadata}, - {name: "GCP", fetch: fetchGCPMetadata}, - {name: "Azure", fetch: fetchAzureMetadata}, + {name: string(armotypes.ProviderDigitalOcean), fetch: fetchDigitalOceanMetadata}, + {name: string(armotypes.ProviderGcp), fetch: fetchGCPMetadata}, + {name: string(armotypes.ProviderAzure), fetch: fetchAzureMetadata}, + {name: string(armotypes.ProviderAlibaba), fetch: fetchAlibabaMetadata}, + {name: string(armotypes.ProviderOracle), fetch: fetchOracleMetadata}, + {name: string(armotypes.ProviderOpenStack), fetch: fetchOpenStackMetadata}, + {name: string(armotypes.ProviderHetzner), fetch: fetchHetznerMetadata}, + {name: string(armotypes.ProviderLinode), fetch: fetchLinodeMetadata}, } for _, fb := range fallbacks { @@ -125,7 +130,7 @@ func getLastPathPart(val string) string { } // fetchDigitalOceanMetadata attempts to fetch basic metadata from DigitalOcean's metadata service. -func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { +func fetchDigitalOceanMetadata(ctx context.Context) (*armotypes.CloudMetadata, error) { base := "http://169.254.169.254/metadata/v1/" // Probe root to see whether the metadata endpoint responds and contains expected entries. @@ -134,7 +139,7 @@ func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, er return nil, err } - // Basic heuristic: the DO metadata root typically lists resources like 'id', 'hostname', 'region' etc. + // Basic heuristic: the DO metadata root typically lists resources like 'id', 'region' and 'hostname'. if !strings.Contains(body, "id") && !strings.Contains(body, "region") && !strings.Contains(body, "hostname") { return nil, fmt.Errorf("digitalocean metadata root missing expected entries") } @@ -153,8 +158,9 @@ func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, er instanceType = get("type") } - meta := &apitypes.CloudMetadata{ - Provider: "digitalocean", + meta := &armotypes.CloudMetadata{ + Provider: armotypes.ProviderDigitalOcean, + HostType: armotypes.HostTypeDroplet, InstanceID: id, InstanceType: instanceType, Region: get("region"), @@ -163,6 +169,11 @@ func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, er Hostname: get("hostname"), } + // Detect DOKS + if tags := get("tags"); tags != "" && strings.Contains(tags, "k8s") { + meta.HostType = armotypes.HostTypeDoks + } + // if nothing useful was obtained, return an error so callers can continue trying other fallbacks if meta.InstanceID == "" && meta.Hostname == "" && meta.Region == "" && meta.PrivateIP == "" && meta.PublicIP == "" && meta.InstanceType == "" { return nil, fmt.Errorf("digitalocean metadata endpoints returned no data") @@ -172,7 +183,7 @@ func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, er } // fetchGCPMetadata attempts to fetch basic metadata from GCP's metadata service. -func fetchGCPMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { +func fetchGCPMetadata(ctx context.Context) (*armotypes.CloudMetadata, error) { base := "http://metadata.google.internal/computeMetadata/v1/" headers := map[string]string{"Metadata-Flavor": "Google"} @@ -186,18 +197,28 @@ func fetchGCPMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { return nil, fmt.Errorf("not a GCP instance") } - return &apitypes.CloudMetadata{ - Provider: "gcp", + meta := &armotypes.CloudMetadata{ + Provider: armotypes.ProviderGcp, + HostType: armotypes.HostTypeGce, AccountID: get("project/project-id"), InstanceID: get("instance/id"), InstanceType: getLastPathPart(machineType), Zone: getLastPathPart(get("instance/zone")), Hostname: get("instance/hostname"), - }, nil + PrivateIP: get("instance/network-interfaces/0/ip"), + PublicIP: get("instance/network-interfaces/0/access-configs/0/external-ip"), + } + + // Detect GKE + if clusterName := get("instance/attributes/cluster-name"); clusterName != "" { + meta.HostType = armotypes.HostTypeGke + } + + return meta, nil } // fetchAzureMetadata attempts to fetch basic metadata from Azure's metadata service. -func fetchAzureMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { +func fetchAzureMetadata(ctx context.Context) (*armotypes.CloudMetadata, error) { base := "http://169.254.169.254/metadata/instance/compute/" headers := map[string]string{"Metadata": "true"} params := "?api-version=" + azureApiVersion + "&format=text" @@ -212,13 +233,140 @@ func fetchAzureMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { return nil, fmt.Errorf("not an Azure instance") } - return &apitypes.CloudMetadata{ - Provider: "azure", + meta := &armotypes.CloudMetadata{ + Provider: armotypes.ProviderAzure, + HostType: armotypes.HostTypeAzureVm, AccountID: get("subscriptionId"), InstanceID: get("vmId"), InstanceType: vmSize, Region: get("location"), Zone: get("zone"), Hostname: get("name"), + } + + // Detect AKS (heuristic: check for resource group or vmss tags common in AKS) + if strings.Contains(strings.ToLower(get("resourceGroupName")), "aks") { + meta.HostType = armotypes.HostTypeAks + } + + // Try to get IP info + networkBase := "http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/" + if ip, err := fetchHTTPMetadata(ctx, networkBase+"privateIpAddress"+params, headers); err == nil { + meta.PrivateIP = ip + } + if ip, err := fetchHTTPMetadata(ctx, networkBase+"publicIpAddress"+params, headers); err == nil { + meta.PublicIP = ip + } + + return meta, nil +} + +// fetchAlibabaMetadata attempts to fetch basic metadata from Alibaba Cloud's metadata service. +func fetchAlibabaMetadata(ctx context.Context) (*armotypes.CloudMetadata, error) { + base := "http://100.100.100.200/latest/meta-data/" + get := func(path string) string { + val, _ := fetchHTTPMetadata(ctx, base+path, nil) + return val + } + + instanceID := get("instance-id") + if instanceID == "" { + return nil, fmt.Errorf("not an Alibaba Cloud instance") + } + + return &armotypes.CloudMetadata{ + Provider: armotypes.ProviderAlibaba, + HostType: armotypes.HostTypeOther, + InstanceID: instanceID, + InstanceType: get("instance/instance-type"), + Region: get("region-id"), + Zone: get("zone-id"), + PrivateIP: get("private-ipv4"), + PublicIP: get("public-ipv4"), + Hostname: get("hostname"), + }, nil +} + +// fetchOracleMetadata attempts to fetch basic metadata from Oracle Cloud's metadata service. +func fetchOracleMetadata(ctx context.Context) (*armotypes.CloudMetadata, error) { + base := "http://169.254.169.254/opc/v1/instance/" + get := func(path string) string { + val, _ := fetchHTTPMetadata(ctx, base+path, nil) + return val + } + + id := get("id") + if id == "" { + return nil, fmt.Errorf("not an Oracle Cloud instance") + } + + return &armotypes.CloudMetadata{ + Provider: armotypes.ProviderOracle, + HostType: armotypes.HostTypeOther, + InstanceID: id, + InstanceType: get("shape"), + Region: get("region"), + Zone: get("availabilityDomain"), + Hostname: get("displayName"), + }, nil +} + +// fetchOpenStackMetadata attempts to fetch basic metadata from OpenStack's metadata service. +func fetchOpenStackMetadata(ctx context.Context) (*armotypes.CloudMetadata, error) { + // OpenStack metadata is typically a JSON, but we can probe the root first. + _, err := fetchHTTPMetadata(ctx, "http://169.254.169.254/openstack", nil) + if err != nil { + return nil, err + } + + // Just return provider for now to signify detection + return &armotypes.CloudMetadata{ + Provider: armotypes.ProviderOpenStack, + HostType: armotypes.HostTypeOther, + }, nil +} + +// fetchHetznerMetadata attempts to fetch basic metadata from Hetzner Cloud's metadata service. +func fetchHetznerMetadata(ctx context.Context) (*armotypes.CloudMetadata, error) { + base := "http://169.254.169.254/hetzner/v1/metadata" + _, err := fetchHTTPMetadata(ctx, base, nil) + if err != nil { + return nil, err + } + + get := func(path string) string { + val, _ := fetchHTTPMetadata(ctx, base+"/"+path, nil) + return val + } + + id := get("instance-id") + if id == "" { + return nil, fmt.Errorf("not a Hetzner Cloud instance") + } + + return &armotypes.CloudMetadata{ + Provider: armotypes.ProviderHetzner, + HostType: armotypes.HostTypeOther, + InstanceID: id, + InstanceType: get("instance-type"), + Region: get("region"), + Zone: get("availability-zone"), + PublicIP: get("public-ipv4"), + Hostname: get("hostname"), + }, nil +} + +// fetchLinodeMetadata attempts to fetch basic metadata from Linode's metadata service. +func fetchLinodeMetadata(ctx context.Context) (*armotypes.CloudMetadata, error) { + base := "http://169.254.169.254/v1/metadata" + // Linode returns a JSON usually, but let's check for response + _, err := fetchHTTPMetadata(ctx, base, nil) + if err != nil { + return nil, err + } + + return &armotypes.CloudMetadata{ + Provider: armotypes.ProviderLinode, + HostType: armotypes.HostTypeOther, }, nil }