internal/resources/fetching/fetchers/azure/assets_fetcher.go (163 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package fetchers
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"github.com/elastic/cloudbeat/internal/infra/clog"
"github.com/elastic/cloudbeat/internal/resources/fetching"
"github.com/elastic/cloudbeat/internal/resources/fetching/cycle"
"github.com/elastic/cloudbeat/internal/resources/providers/azurelib"
"github.com/elastic/cloudbeat/internal/resources/providers/azurelib/governance"
"github.com/elastic/cloudbeat/internal/resources/providers/azurelib/inventory"
)
type AzureAssetsFetcher struct {
log *clog.Logger
resourceCh chan fetching.ResourceInfo
provider azurelib.ProviderAPI
enrichers []AssetsEnricher
}
type AzureResource struct {
Type string
SubType string
Asset inventory.AzureAsset `json:"asset,omitempty"`
Subscription governance.Subscription
}
type typePair struct {
SubType string
Type string
}
var AzureAssetTypeToTypePair = map[string]typePair{
inventory.ClassicStorageAccountAssetType: {fetching.AzureClassicStorageAccountType, fetching.CloudStorage},
inventory.DiskAssetType: {fetching.AzureDiskType, fetching.CloudCompute},
inventory.DocumentDBDatabaseAccountAssetType: {fetching.AzureDocumentDBDatabaseAccountType, fetching.CloudDatabase},
inventory.MySQLDBAssetType: {fetching.AzureMySQLDBType, fetching.CloudDatabase},
inventory.FlexibleMySQLDBAssetType: {fetching.AzureFlexibleMySQLDBType, fetching.CloudDatabase},
inventory.NetworkWatchersFlowLogAssetType: {fetching.AzureNetworkWatchersFlowLogType, fetching.MonitoringIdentity},
inventory.FlexiblePostgreSQLDBAssetType: {fetching.AzureFlexiblePostgreSQLDBType, fetching.CloudDatabase},
inventory.PostgreSQLDBAssetType: {fetching.AzurePostgreSQLDBType, fetching.CloudDatabase},
inventory.SQLServersAssetType: {fetching.AzureSQLServerType, fetching.CloudDatabase},
inventory.StorageAccountAssetType: {fetching.AzureStorageAccountType, fetching.CloudStorage},
inventory.VirtualMachineAssetType: {fetching.AzureVMType, fetching.CloudCompute},
inventory.WebsitesAssetType: {fetching.AzureWebSiteType, fetching.CloudCompute},
inventory.VaultAssetType: {fetching.AzureVaultType, fetching.KeyManagement},
inventory.RoleDefinitionsType: {fetching.AzureRoleDefinitionType, fetching.CloudIdentity},
// This asset type is used only for enrichment purposes, but is sent to OPA layer, producing no findings.
inventory.NetworkSecurityGroupAssetType: {fetching.AzureNetworkSecurityGroupType, fetching.MonitoringIdentity},
}
// In order to simplify the mappings, we are trying to query all AzureAssetTypeToTypePair on every asset group
// Because this is done with an "|"" this means that we won't get irrelevant data
var AzureAssetGroups = []string{inventory.AssetGroupResources, inventory.AssetGroupAuthorizationResources}
func NewAzureAssetsFetcher(log *clog.Logger, ch chan fetching.ResourceInfo, provider azurelib.ProviderAPI) *AzureAssetsFetcher {
return &AzureAssetsFetcher{
log: log,
resourceCh: ch,
provider: provider,
enrichers: initEnrichers(provider),
}
}
func (f *AzureAssetsFetcher) Fetch(ctx context.Context, cycleMetadata cycle.Metadata) error {
f.log.Info("Starting AzureAssetsFetcher.Fetch")
var errAgg error
// This might be relevant if we'd like to fetch assets in parallel in order to evaluate a rule that uses multiple resources
var assets []inventory.AzureAsset
for _, assetGroup := range AzureAssetGroups {
// Fetching all types even if non-existent in asset group for simplicity
r, err := f.provider.ListAllAssetTypesByName(ctx, assetGroup, slices.Collect(maps.Keys(AzureAssetTypeToTypePair)))
if err != nil {
f.log.Errorf("AzureAssetsFetcher.Fetch failed to fetch asset group %s: %s", assetGroup, err.Error())
errAgg = errors.Join(errAgg, err)
continue
}
assets = append(assets, r...)
}
subscriptions, err := f.provider.GetSubscriptions(ctx, cycleMetadata)
if err != nil {
f.log.Errorf("Error fetching subscription information: %v", err)
}
for _, e := range f.enrichers {
if err := e.Enrich(ctx, cycleMetadata, assets); err != nil {
errAgg = errors.Join(errAgg, fmt.Errorf("error while enriching assets: %w", err))
}
}
for _, asset := range assets {
select {
case <-ctx.Done():
err := ctx.Err()
f.log.Infof("AzureAssetsFetcher.Fetch context err: %s", err.Error())
errAgg = errors.Join(errAgg, err)
return errAgg
case f.resourceCh <- resourceFromAsset(asset, cycleMetadata, subscriptions):
}
}
return errAgg
}
func resourceFromAsset(asset inventory.AzureAsset, cycleMetadata cycle.Metadata, subscriptions map[string]governance.Subscription) fetching.ResourceInfo {
pair := AzureAssetTypeToTypePair[asset.Type]
subscription, ok := subscriptions[asset.SubscriptionId]
if !ok {
subscription = governance.Subscription{
FullyQualifiedID: asset.SubscriptionId,
ShortID: "",
DisplayName: "",
ManagementGroup: governance.ManagementGroup{
FullyQualifiedID: "",
DisplayName: "",
},
}
}
return fetching.ResourceInfo{
CycleMetadata: cycleMetadata,
Resource: &AzureResource{
Type: pair.Type,
SubType: pair.SubType,
Asset: asset,
Subscription: subscription,
},
}
}
func (f *AzureAssetsFetcher) Stop() {}
func (r *AzureResource) GetData() any {
return r.Asset
}
func (r *AzureResource) GetMetadata() (fetching.ResourceMetadata, error) {
return fetching.ResourceMetadata{
ID: r.Asset.Id,
Type: r.Type,
SubType: r.SubType,
Name: r.Asset.Name,
Region: r.Asset.Location,
CloudAccountMetadata: r.Subscription.GetCloudAccountMetadata(),
}, nil
}
func (r *AzureResource) GetIds() []string {
return []string{r.Asset.Id}
}
func (r *AzureResource) GetElasticCommonData() (map[string]any, error) {
m := map[string]any{}
switch r.Asset.Type {
case inventory.VirtualMachineAssetType:
{
m["host.name"] = r.Asset.Name
// "host.hostname" = "properties.osProfile.computerName" if it exists
osProfileRaw, ok := r.Asset.Properties["osProfile"]
if !ok {
break
}
osProfile, ok := osProfileRaw.(map[string]any)
if !ok {
break
}
computerNameRaw, ok := osProfile["computerName"]
if !ok {
break
}
computerName, ok := computerNameRaw.(string)
if !ok {
break
}
m["host.hostname"] = computerName
}
case inventory.RoleDefinitionsType:
{
m["user.effective.id"] = r.Asset.Id
m["user.effective.name"] = r.Asset.Name
}
}
return m, nil
}