Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
188 changes: 168 additions & 20 deletions pkg/cloudmetadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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
}

Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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")
}
Expand All @@ -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"),
Expand All @@ -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")
Expand All @@ -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"}

Expand All @@ -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"
Expand All @@ -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
}
Loading