diff --git a/api/models/models.gen.go b/api/models/models.gen.go index 3b5df50e7..6328c9430 100644 --- a/api/models/models.gen.go +++ b/api/models/models.gen.go @@ -26,6 +26,7 @@ const ( const ( AWS CloudProvider = "AWS" Azure CloudProvider = "Azure" + GCP CloudProvider = "GCP" ) // Defines values for MisconfigurationSeverity. diff --git a/api/openapi.yaml b/api/openapi.yaml index f906756a6..e8306a8fd 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -916,6 +916,7 @@ components: enum: - AWS - Azure + - GCP Scans: type: object diff --git a/api/server/server.gen.go b/api/server/server.gen.go index 220cd426c..69a5ab975 100644 --- a/api/server/server.gen.go +++ b/api/server/server.gen.go @@ -1067,97 +1067,97 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/go.mod b/go.mod index b8febd914..f7f1384e7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openclarity/vmclarity go 1.20 require ( + cloud.google.com/go/compute v1.19.1 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1 @@ -26,6 +27,7 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 + github.com/googleapis/gax-go/v2 v2.8.0 github.com/labstack/echo/v4 v4.10.2 github.com/mitchellh/mapstructure v1.5.0 github.com/moby/sys/mountinfo v0.6.2 @@ -39,6 +41,7 @@ require ( github.com/spf13/viper v1.16.0 github.com/vulsio/go-exploitdb v0.4.5 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 + google.golang.org/api v0.122.0 google.golang.org/grpc v1.56.2 gopkg.in/yaml.v3 v3.0.1 gorm.io/datatypes v1.2.0 @@ -52,7 +55,6 @@ require ( require ( cloud.google.com/go v0.110.0 // indirect - cloud.google.com/go/compute v1.19.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v0.13.0 // indirect cloud.google.com/go/storage v1.29.0 // indirect @@ -221,7 +223,6 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/wire v0.5.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect @@ -414,7 +415,6 @@ require ( golang.org/x/tools v0.9.2 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gonum.org/v1/gonum v0.7.0 // indirect - google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/runtime_scan/pkg/orchestrator/assetscanwatcher/watcher.go b/runtime_scan/pkg/orchestrator/assetscanwatcher/watcher.go index 399d8cc30..90975efc4 100644 --- a/runtime_scan/pkg/orchestrator/assetscanwatcher/watcher.go +++ b/runtime_scan/pkg/orchestrator/assetscanwatcher/watcher.go @@ -320,6 +320,8 @@ func (w *Watcher) reconcileDone(ctx context.Context, assetScan *models.AssetScan // nolint:cyclop func (w *Watcher) cleanupResources(ctx context.Context, assetScan *models.AssetScan) error { + logger := log.GetLoggerFromContextOrDiscard(ctx) + assetScanID, ok := assetScan.GetID() if !ok { return errors.New("invalid AssetScan: ID is nil") @@ -383,11 +385,13 @@ func (w *Watcher) cleanupResources(ctx context.Context, assetScan *models.AssetS switch { case errors.As(err, &fatalError): assetScan.ResourceCleanup = utils.PointerTo(models.ResourceCleanupStateFailed) + logger.Errorf("resource cleanup failed: %v", fatalError) case errors.As(err, &retryableError): // nolint:wrapcheck return common.NewRequeueAfterError(retryableError.RetryAfter(), retryableError.Error()) case err != nil: assetScan.ResourceCleanup = utils.PointerTo(models.ResourceCleanupStateFailed) + logger.Errorf("resource cleanup failed: %v", err) default: assetScan.ResourceCleanup = utils.PointerTo(models.ResourceCleanupStateDone) } diff --git a/runtime_scan/pkg/orchestrator/config.go b/runtime_scan/pkg/orchestrator/config.go index 6a8a2d649..4bbde3615 100644 --- a/runtime_scan/pkg/orchestrator/config.go +++ b/runtime_scan/pkg/orchestrator/config.go @@ -136,6 +136,8 @@ func LoadConfig(backendHost string, backendPort int, baseURL string) (*Config, e switch strings.ToLower(viper.GetString(ProviderKind)) { case strings.ToLower(string(models.Azure)): providerKind = models.Azure + case strings.ToLower(string(models.GCP)): + providerKind = models.GCP case strings.ToLower(string(models.AWS)): fallthrough default: diff --git a/runtime_scan/pkg/orchestrator/orchestrator.go b/runtime_scan/pkg/orchestrator/orchestrator.go index ed2faea28..3ae7dec9e 100644 --- a/runtime_scan/pkg/orchestrator/orchestrator.go +++ b/runtime_scan/pkg/orchestrator/orchestrator.go @@ -29,6 +29,7 @@ import ( "github.com/openclarity/vmclarity/runtime_scan/pkg/provider" "github.com/openclarity/vmclarity/runtime_scan/pkg/provider/aws" "github.com/openclarity/vmclarity/runtime_scan/pkg/provider/azure" + "github.com/openclarity/vmclarity/runtime_scan/pkg/provider/gcp" "github.com/openclarity/vmclarity/shared/pkg/backendclient" "github.com/openclarity/vmclarity/shared/pkg/log" ) @@ -102,6 +103,8 @@ func NewProvider(ctx context.Context, kind models.CloudProvider) (provider.Provi return azure.New(ctx) case models.AWS: return aws.New(ctx) + case models.GCP: + return gcp.New(ctx) default: return nil, fmt.Errorf("unsupported provider: %s", kind) } diff --git a/runtime_scan/pkg/orchestrator/scanconfigwatcher/schedule.go b/runtime_scan/pkg/orchestrator/scanconfigwatcher/schedule.go index 8769f45b6..d86f9bc7e 100644 --- a/runtime_scan/pkg/orchestrator/scanconfigwatcher/schedule.go +++ b/runtime_scan/pkg/orchestrator/scanconfigwatcher/schedule.go @@ -67,7 +67,7 @@ func (w ScheduleWindow) Next() *ScheduleWindow { } } -// Next returns a new ScheduleWindow which represents the timeframe before w ScheduleWindow. +// Prev returns a new ScheduleWindow which represents the timeframe before w ScheduleWindow. func (w ScheduleWindow) Prev() *ScheduleWindow { return &ScheduleWindow{ start: w.start.Add(-1 * w.end.Sub(w.start)), @@ -116,7 +116,7 @@ func (o OperationTime) Next() *OperationTime { return &o } -// Next returns a new OperationTime representing time which is after then the provided t time. It returns itself +// NextAfter returns a new OperationTime representing time which is after then the provided t time. It returns itself // if t is zero or o has no cron set. func (o OperationTime) NextAfter(t time.Time) *OperationTime { if t.IsZero() { diff --git a/runtime_scan/pkg/orchestrator/scanconfigwatcher/watcher.go b/runtime_scan/pkg/orchestrator/scanconfigwatcher/watcher.go index 5dd565d0c..9448aabb6 100644 --- a/runtime_scan/pkg/orchestrator/scanconfigwatcher/watcher.go +++ b/runtime_scan/pkg/orchestrator/scanconfigwatcher/watcher.go @@ -127,7 +127,7 @@ func (w *Watcher) Reconcile(ctx context.Context, event ScanConfigReconcileEvent) scanConfigSchedule, err := NewScanConfigSchedule(scanConfig, scheduleWindow) if err != nil { - return fmt.Errorf("failed to determine ScanConfig state: %w", err) + return fmt.Errorf("failed to create new ScanConfig schedule: %w", err) } switch scanConfigSchedule.State { diff --git a/runtime_scan/pkg/provider/gcp/client.go b/runtime_scan/pkg/provider/gcp/client.go new file mode 100644 index 000000000..39a71cd51 --- /dev/null +++ b/runtime_scan/pkg/provider/gcp/client.go @@ -0,0 +1,371 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed 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 gcp + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + "github.com/sirupsen/logrus" + "google.golang.org/api/iterator" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/runtime_scan/pkg/provider" + "github.com/openclarity/vmclarity/shared/pkg/log" + "github.com/openclarity/vmclarity/shared/pkg/utils" +) + +type Client struct { + snapshotsClient *compute.SnapshotsClient + disksClient *compute.DisksClient + instancesClient *compute.InstancesClient + regionsClient *compute.RegionsClient + + gcpConfig Config +} + +func New(ctx context.Context) (*Client, error) { + config, err := NewConfig() + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %w", err) + } + + err = config.Validate() + if err != nil { + return nil, fmt.Errorf("failed to validate configuration: %w", err) + } + + client := Client{ + gcpConfig: config, + } + + regionsClient, err := compute.NewRegionsRESTClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create regions client: %w", err) + } + client.regionsClient = regionsClient + + instancesClient, err := compute.NewInstancesRESTClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create instance client: %w", err) + } + client.instancesClient = instancesClient + + snapshotsClient, err := compute.NewSnapshotsRESTClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create snapshot client: %w", err) + } + client.snapshotsClient = snapshotsClient + + disksClient, err := compute.NewDisksRESTClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create disks client: %w", err) + } + client.disksClient = disksClient + + return &client, nil +} + +func (c Client) Kind() models.CloudProvider { + return models.GCP +} + +// nolint:cyclop +func (c *Client) RunAssetScan(ctx context.Context, config *provider.ScanJobConfig) error { + // convert AssetInfo to vmInfo + vminfo, err := config.AssetInfo.AsVMInfo() + if err != nil { + return provider.FatalErrorf("unable to get vminfo from AssetInfo: %w", err) + } + + logger := log.GetLoggerFromContextOrDefault(ctx).WithFields(logrus.Fields{ + "AssetScanID": config.AssetScanID, + "AssetLocation": vminfo.Location, + "InstanceID": vminfo.InstanceID, + "ScannerZone": c.gcpConfig.ScannerZone, + "Provider": string(c.Kind()), + }) + logger.Debugf("Running asset scan") + + targetName := vminfo.InstanceID + targetZone := vminfo.Location + + // get the target instance to scan from gcp. + targetVM, err := c.instancesClient.Get(ctx, &computepb.GetInstanceRequest{ + Instance: targetName, + Project: c.gcpConfig.ProjectID, + Zone: targetZone, + }) + if err != nil { + _, err := handleGcpRequestError(err, "getting target virtual machine %v", targetName) + return err + } + logger.Debugf("Got target VM: %v", targetVM.Name) + + // get target instance boot disk + bootDisk, err := getInstanceBootDisk(targetVM) + if err != nil { + return provider.FatalErrorf("unable to get instance boot disk: %w", err) + } + logger.Debugf("Got target boot disk: %v", bootDisk.GetSource()) + + // ensure that a snapshot was created from the target instance root disk. (create if not) + snapshot, err := c.ensureSnapshotFromAttachedDisk(ctx, config, bootDisk) + if err != nil { + return fmt.Errorf("failed to ensure snapshot for vm root volume: %w", err) + } + logger.Debugf("Created snapshot: %v", snapshot.Name) + + // create a disk from the snapshot. + // Snapshots are global resources, so any snapshot is accessible by any resource within the same project. + var diskFromSnapshot *computepb.Disk + diskFromSnapshot, err = c.ensureDiskFromSnapshot(ctx, config, snapshot) + if err != nil { + return fmt.Errorf("failed to ensure disk created from snapshot: %w", err) + } + logger.Debugf("Created disk from snapshot: %v", diskFromSnapshot.Name) + + // create the scanner instance + scannerVM, err := c.ensureScannerVirtualMachine(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure scanner virtual machine: %w", err) + } + logger.Debugf("Created scanner virtual machine: %v", scannerVM.Name) + + // attach the disk from snapshot to the scanner instance + err = c.ensureDiskAttachedToScannerVM(ctx, scannerVM, diskFromSnapshot) + if err != nil { + return fmt.Errorf("failed to ensure target disk is attached to virtual machine: %w", err) + } + logger.Debugf("Attached disk to scanner virtual machine") + + return nil +} + +func (c *Client) RemoveAssetScan(ctx context.Context, config *provider.ScanJobConfig) error { + logger := log.GetLoggerFromContextOrDefault(ctx).WithFields(logrus.Fields{ + "AssetScanID": config.AssetScanID, + "ScannerZone": c.gcpConfig.ScannerZone, + "Provider": string(c.Kind()), + }) + + err := c.ensureScannerVirtualMachineDeleted(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure scanner virtual machine deleted: %w", err) + } + logger.Debugf("Deleted scanner virtual machine") + + err = c.ensureTargetDiskDeleted(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure target disk deleted: %w", err) + } + logger.Debugf("Deleted disk") + + err = c.ensureSnapshotDeleted(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure snapshot deleted: %w", err) + } + logger.Debugf("Deleted snapshot") + + return nil +} + +// nolint: cyclop +func (c *Client) DiscoverAssets(ctx context.Context) ([]models.AssetType, error) { + var ret []models.AssetType + + regions, err := c.listAllRegions(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list all regions: %v", err) + } + + var zones []string + for _, region := range regions { + zones = append(zones, getZonesLastPart(region.Zones)...) + } + + for _, zone := range zones { + assets, err := c.listInstances(ctx, nil, zone) + if err != nil { + return nil, fmt.Errorf("failed to list instances: %w", err) + } + + ret = append(ret, assets...) + } + + return ret, nil +} + +// getZonesLastPart converts a list of zone URLs into a list of zone IDs. +// For example input: +// +// [ +// +// https://www.googleapis.com/compute/v1/projects/gcp-etigcp-nprd-12855/zones/us-central1-c, +// https://www.googleapis.com/compute/v1/projects/gcp-etigcp-nprd-12855/zones/us-central1-a +// +// ] +// +// returns [us-central1-c, us-central1-a]. +func getZonesLastPart(zones []string) []string { + ret := make([]string, 0, len(zones)) + for _, zone := range zones { + z := zone + ret = append(ret, getLastURLPart(&z)) + } + return ret +} + +func getInstanceBootDisk(vm *computepb.Instance) (*computepb.AttachedDisk, error) { + for _, disk := range vm.Disks { + if disk.Boot != nil && *disk.Boot { + return disk, nil + } + } + return nil, fmt.Errorf("failed to find instance boot disk") +} + +func (c *Client) listInstances(ctx context.Context, filter *string, zone string) ([]models.AssetType, error) { + var ret []models.AssetType + + it := c.instancesClient.List(ctx, &computepb.ListInstancesRequest{ + Filter: filter, + MaxResults: utils.PointerTo[uint32](maxResults), + Project: c.gcpConfig.ProjectID, + Zone: zone, + }) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + _, err = handleGcpRequestError(err, "listing instances for project %s zone %s", c.gcpConfig.ProjectID, zone) + return nil, err + } + + info, err := c.getVMInfoFromVirtualMachine(ctx, resp) + if err != nil { + return nil, fmt.Errorf("failed to get vminfo from virtual machine: %w", err) + } + ret = append(ret, info) + } + + return ret, nil +} + +func (c *Client) listAllRegions(ctx context.Context) ([]*computepb.Region, error) { + var ret []*computepb.Region + + it := c.regionsClient.List(ctx, &computepb.ListRegionsRequest{ + MaxResults: utils.PointerTo[uint32](maxResults), + Project: c.gcpConfig.ProjectID, + }) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + _, err := handleGcpRequestError(err, "list regions") + return nil, err + } + + ret = append(ret, resp) + } + return ret, nil +} + +func (c *Client) getVMInfoFromVirtualMachine(ctx context.Context, vm *computepb.Instance) (models.AssetType, error) { + assetType := models.AssetType{} + launchTime, err := time.Parse(time.RFC3339, *vm.CreationTimestamp) + if err != nil { + return models.AssetType{}, fmt.Errorf("failed to parse time: %v", *vm.CreationTimestamp) + } + // get boot disk name + diskName := getLastURLPart(vm.Disks[0].Source) + + var platform string + var image string + + // get disk from gcp + disk, err := c.disksClient.Get(ctx, &computepb.GetDiskRequest{ + Disk: diskName, + Project: c.gcpConfig.ProjectID, + Zone: getLastURLPart(vm.Zone), + }) + if err != nil { + logrus.Warnf("failed to get disk %v: %v", diskName, err) + } else { + platform = *disk.Architecture + image = getLastURLPart(disk.SourceImage) + } + + err = assetType.FromVMInfo(models.VMInfo{ + InstanceProvider: utils.PointerTo(models.GCP), + InstanceID: *vm.Name, + Image: image, + InstanceType: getLastURLPart(vm.MachineType), + LaunchTime: launchTime, + Location: getLastURLPart(vm.Zone), + Platform: platform, + SecurityGroups: &[]models.SecurityGroup{}, + Tags: convertTags(vm.Tags), + }) + if err != nil { + return models.AssetType{}, provider.FatalErrorf("failed to create AssetType from VMInfo: %w", err) + } + + return assetType, nil +} + +// convertTags converts gcp instance tags in the form []string{key1=val1} into +// models.Tag{Key: key1, Value: val1}. If the tag does not contain the equals +// sign, the Key will be the tag and the Value will be empty. +func convertTags(tags *computepb.Tags) *[]models.Tag { + ret := make([]models.Tag, 0, len(tags.Items)) + for _, item := range tags.Items { + key, val := getKeyValue(item) + ret = append(ret, models.Tag{ + Key: key, + Value: val, + }) + } + return &ret +} + +// TODO(sambetts) Remove this unused function. +func convertTagsToMap(tags *computepb.Tags) map[string]string { + ret := make(map[string]string, len(tags.Items)) + for _, item := range tags.Items { + key, val := getKeyValue(item) + ret[key] = val + } + return ret +} + +func getKeyValue(str string) (string, string) { + key, value, found := strings.Cut(str, "=") + if found { + return key, value + } + return str, "" +} diff --git a/runtime_scan/pkg/provider/gcp/client_test.go b/runtime_scan/pkg/provider/gcp/client_test.go new file mode 100644 index 000000000..b8c8239b0 --- /dev/null +++ b/runtime_scan/pkg/provider/gcp/client_test.go @@ -0,0 +1,216 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed 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 gcp + +import ( + "reflect" + "testing" + + "cloud.google.com/go/compute/apiv1/computepb" + + "github.com/google/go-cmp/cmp" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/shared/pkg/utils" +) + +func Test_convertTags(t *testing.T) { + type args struct { + tags *computepb.Tags + } + tests := []struct { + name string + args args + want *[]models.Tag + }{ + { + name: "sanity", + args: args{ + tags: &computepb.Tags{ + Items: []string{"tag1", "tag2=val2", "tag3="}, + }, + }, + want: &[]models.Tag{ + { + Key: "tag1", + Value: "", + }, + { + Key: "tag2", + Value: "val2", + }, + { + Key: "tag3", + Value: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := convertTags(tt.args.tags); !reflect.DeepEqual(got, tt.want) { + t.Errorf("convertTags() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_convertTagsToMap(t *testing.T) { + type args struct { + tags *computepb.Tags + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "Items=nil", + args: args{ + tags: &computepb.Tags{}, + }, + want: map[string]string{}, + }, + { + name: "no tags", + args: args{ + tags: &computepb.Tags{ + Items: []string{}, + }, + }, + want: map[string]string{}, + }, + { + name: "sanity", + args: args{ + tags: &computepb.Tags{ + Items: []string{"key1=val1", "key2"}, + }, + }, + want: map[string]string{ + "key1": "val1", + "key2": "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := convertTagsToMap(tt.args.tags); !reflect.DeepEqual(got, tt.want) { + t.Errorf("convertTagsToMap() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getInstanceBootDisk(t *testing.T) { + type args struct { + vm *computepb.Instance + } + tests := []struct { + name string + args args + want *computepb.AttachedDisk + wantErr bool + }{ + { + name: "found", + args: args{ + vm: &computepb.Instance{ + Disks: []*computepb.AttachedDisk{ + { + DeviceName: utils.PointerTo("device1"), + Boot: utils.PointerTo(true), + }, + { + DeviceName: utils.PointerTo("device2"), + Boot: utils.PointerTo(false), + }, + }, + }, + }, + want: &computepb.AttachedDisk{ + DeviceName: utils.PointerTo("device1"), + Boot: utils.PointerTo(true), + }, + wantErr: false, + }, + { + name: "not found", + args: args{ + vm: &computepb.Instance{ + Disks: []*computepb.AttachedDisk{ + { + DeviceName: utils.PointerTo("device1"), + Boot: utils.PointerTo(false), + }, + { + DeviceName: utils.PointerTo("device2"), + Boot: utils.PointerTo(false), + }, + }, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getInstanceBootDisk(tt.args.vm) + if (err != nil) != tt.wantErr { + t.Errorf("getInstanceBootDisk() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getInstanceBootDisk() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getZonesLastPart(t *testing.T) { + type args struct { + zones []string + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "empty", + args: args{ + zones: []string{}, + }, + want: []string{}, + }, + { + name: "get two zones", + args: args{ + zones: []string{"https://www.googleapis.com/compute/v1/projects/gcp-etigcp-nprd-12855/zones/us-central1-c", "https://www.googleapis.com/compute/v1/projects/gcp-etigcp-nprd-12855/zones/us-central1-a"}, + }, + want: []string{"us-central1-c", "us-central1-a"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getZonesLastPart(tt.args.zones) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("getZonesLastPart() mismatch (-want, +got):\n%s", diff) + } + }) + } +} diff --git a/runtime_scan/pkg/provider/gcp/common.go b/runtime_scan/pkg/provider/gcp/common.go new file mode 100644 index 000000000..f652a8b5a --- /dev/null +++ b/runtime_scan/pkg/provider/gcp/common.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed 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 gcp + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "path" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/api/googleapi" + + "github.com/openclarity/vmclarity/runtime_scan/pkg/provider" +) + +func handleGcpRequestError(err error, actionTmpl string, parts ...interface{}) (bool, error) { + action := fmt.Sprintf(actionTmpl, parts...) + + var gAPIError *googleapi.Error + if errors.As(err, &gAPIError) { + sc := gAPIError.Code + switch { + case sc >= http.StatusBadRequest && sc < http.StatusInternalServerError: + // Client errors (BadRequest/Unauthorized etc) are Fatal. We + // also return true to indicate we have NotFound which is a + // special case in a lot of processing. + return sc == http.StatusNotFound, provider.FatalErrorf("error from gcp while %s: %w", action, gAPIError) + default: + // Everything else is a normal error which can be + // logged as a failure and then the reconciler will try + // again on the next loop. + return false, fmt.Errorf("error from gcp while %s: %w", action, gAPIError) + } + } else { + // Error should be a googleapi.Error + return false, provider.FatalErrorf("unexpected error from gcp while %s: %w", action, err) + } +} + +func ensureDeleted(resourceType string, getFunc func() error, deleteFunc func() error, estimateTime time.Duration) error { + err := getFunc() + if err != nil { + notFound, err := handleGcpRequestError(err, "getting %s", resourceType) + // NotFound means that the resource has been deleted + // successfully, all other errors are raised. + if notFound { + return nil + } + return err + } + + err = deleteFunc() + if err != nil { + _, err := handleGcpRequestError(err, "deleting %s", resourceType) + return err + } + + return provider.RetryableErrorf(estimateTime, "%s delete issued", resourceType) +} + +// example: https://www.googleapis.com/compute/v1/projects/gcp-etigcp-nprd-12855/zones/us-central1-c/machineTypes/e2-medium -> returns e2-medium +func getLastURLPart(str *string) string { + if str == nil { + return "" + } + + urlParsed, err := url.Parse(*str) + if err != nil { + log.Error(err) + return "" + } + + return path.Base(urlParsed.Path) +} diff --git a/runtime_scan/pkg/provider/gcp/config.go b/runtime_scan/pkg/provider/gcp/config.go new file mode 100644 index 000000000..a1bf37a35 --- /dev/null +++ b/runtime_scan/pkg/provider/gcp/config.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed 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 gcp + +import ( + "fmt" + "strings" + + "github.com/mitchellh/mapstructure" + "github.com/spf13/viper" +) + +const ( + DefaultEnvPrefix = "VMCLARITY_GCP" + + projectID = "project_id" + scannerZone = "scanner_zone" + scannerSubnetwork = "scanner_subnetwork" + scannerMachineType = "scanner_machine_type" + scannerSourceImage = "scanner_source_image" + scannerSSHPublicKey = "scanner_ssh_public_key" +) + +type Config struct { + ProjectID string `mapstructure:"project_id"` + ScannerZone string `mapstructure:"scanner_zone"` + ScannerSubnetwork string `mapstructure:"scanner_subnetwork"` + ScannerMachineType string `mapstructure:"scanner_machine_type"` + ScannerSourceImage string `mapstructure:"scanner_source_image"` + ScannerSSHPublicKey string `mapstructure:"scanner_ssh_public_key"` +} + +func NewConfig() (Config, error) { + // Avoid modifying the global instance + v := viper.New() + + v.SetEnvPrefix(DefaultEnvPrefix) + v.AllowEmptyEnv(true) + v.AutomaticEnv() + + _ = v.BindEnv(projectID) + _ = v.BindEnv(scannerZone) + _ = v.BindEnv(scannerSubnetwork) + _ = v.BindEnv(scannerMachineType) + _ = v.BindEnv(scannerSourceImage) + _ = v.BindEnv(scannerSSHPublicKey) + + config := Config{} + if err := v.Unmarshal(&config, viper.DecodeHook(mapstructure.TextUnmarshallerHookFunc())); err != nil { + return Config{}, fmt.Errorf("failed to parse provider configuration. Provider=GCP: %w", err) + } + return config, nil +} + +// nolint:cyclop +func (c Config) Validate() error { + if c.ProjectID == "" { + return fmt.Errorf("parameter ProjectID must be provided by setting %v_%v environment variable", DefaultEnvPrefix, strings.ToUpper(projectID)) + } + + if c.ScannerZone == "" { + return fmt.Errorf("parameter ScannerZone must be provided by setting %v_%v environment variable", DefaultEnvPrefix, strings.ToUpper(scannerZone)) + } + + if c.ScannerSubnetwork == "" { + return fmt.Errorf("parameter ScannerSubnetwork must be provided by setting %v_%v environment variable", DefaultEnvPrefix, strings.ToUpper(scannerSubnetwork)) + } + + if c.ScannerMachineType == "" { + return fmt.Errorf("parameter ScannerMachineType must be provided by setting %v_%v environment variable", DefaultEnvPrefix, strings.ToUpper(scannerMachineType)) + } + + if c.ScannerSourceImage == "" { + return fmt.Errorf("parameter ScannerSourceImage must be provided by setting %v_%v environment variable", DefaultEnvPrefix, strings.ToUpper(scannerSourceImage)) + } + + return nil +} diff --git a/runtime_scan/pkg/provider/gcp/scannerVm.go b/runtime_scan/pkg/provider/gcp/scannerVm.go new file mode 100644 index 000000000..3bc2901a5 --- /dev/null +++ b/runtime_scan/pkg/provider/gcp/scannerVm.go @@ -0,0 +1,195 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed 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 gcp + +import ( + "context" + "fmt" + "time" + + "cloud.google.com/go/compute/apiv1/computepb" + + "github.com/openclarity/vmclarity/runtime_scan/pkg/provider" + "github.com/openclarity/vmclarity/runtime_scan/pkg/provider/cloudinit" + "github.com/openclarity/vmclarity/shared/pkg/utils" +) + +var ( + VMCreateEstimateProvisionTime = 2 * time.Minute + VMDiskAttachEstimateTime = 2 * time.Minute + VMDeleteEstimateTime = 2 * time.Minute +) + +const ( + DiskSizeGB = 10 +) + +func scannerVMNameFromJobConfig(config *provider.ScanJobConfig) string { + return fmt.Sprintf("vmclarity-scanner-%s", config.AssetScanID) +} + +func (c *Client) ensureScannerVirtualMachine(ctx context.Context, config *provider.ScanJobConfig) (*computepb.Instance, error) { + vmName := scannerVMNameFromJobConfig(config) + + instanceRes, err := c.instancesClient.Get(ctx, &computepb.GetInstanceRequest{ + Instance: vmName, + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + }) + if err == nil { + if *instanceRes.Status != InstanceStateRunning { + return instanceRes, provider.RetryableErrorf(VMCreateEstimateProvisionTime, "virtual machine is not ready yet. status: %v", *instanceRes.Status) + } + + // Everything is good, the instance exists and running. + return instanceRes, nil + } + + notFound, err := handleGcpRequestError(err, "getting scanner virtual machine: %v", vmName) + // ignore not found error as it is expected + if !notFound { + return nil, err + } + + // create the instance if not exists + userData, err := cloudinit.New(config) + if err != nil { + return nil, provider.FatalErrorf("failed to generate cloud-init: %v", err) + } + + zone := c.gcpConfig.ScannerZone + instanceName := vmName + + req := &computepb.InsertInstanceRequest{ + Project: c.gcpConfig.ProjectID, + Zone: zone, + InstanceResource: &computepb.Instance{ + Metadata: &computepb.Metadata{ + Items: []*computepb.Items{ + { + Key: utils.PointerTo("user-data"), + Value: utils.PointerTo(userData), + }, + }, + }, + Description: utils.PointerTo("VMClarity scanner"), + Name: &instanceName, + Disks: []*computepb.AttachedDisk{ + { + InitializeParams: &computepb.AttachedDiskInitializeParams{ + DiskType: utils.PointerTo(fmt.Sprintf("zones/%s/diskTypes/pd-balanced", zone)), + DiskSizeGb: utils.PointerTo[int64](DiskSizeGB), + SourceImage: &c.gcpConfig.ScannerSourceImage, + }, + AutoDelete: utils.PointerTo(true), + Boot: utils.PointerTo(true), + Type: utils.PointerTo(computepb.AttachedDisk_PERSISTENT.String()), + }, + }, + MachineType: utils.PointerTo(fmt.Sprintf("zones/%s/machineTypes/%s", zone, c.gcpConfig.ScannerMachineType)), + NetworkInterfaces: []*computepb.NetworkInterface{ + { + Subnetwork: &c.gcpConfig.ScannerSubnetwork, + }, + }, + }, + } + + if c.gcpConfig.ScannerSSHPublicKey != "" { + req.InstanceResource.Metadata.Items = append( + req.InstanceResource.Metadata.Items, + &computepb.Items{ + Key: utils.PointerTo("ssh-keys"), + Value: utils.PointerTo(fmt.Sprintf("vmclarity:%s", c.gcpConfig.ScannerSSHPublicKey)), + }, + ) + } + + _, err = c.instancesClient.Insert(ctx, req) + if err != nil { + _, err := handleGcpRequestError(err, "unable to create instance %v", vmName) + return nil, err + } + + return nil, provider.RetryableErrorf(VMCreateEstimateProvisionTime, "vm creating") +} + +func (c *Client) ensureScannerVirtualMachineDeleted(ctx context.Context, config *provider.ScanJobConfig) error { + vmName := scannerVMNameFromJobConfig(config) + + return ensureDeleted( + "VirtualMachine", + func() error { + _, err := c.instancesClient.Get(ctx, &computepb.GetInstanceRequest{ + Instance: vmName, + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + }) + return err // nolint: wrapcheck + }, + func() error { + _, err := c.instancesClient.Delete(ctx, &computepb.DeleteInstanceRequest{ + Instance: vmName, + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + }) + return err // nolint: wrapcheck + }, + VMDeleteEstimateTime, + ) +} + +func (c *Client) ensureDiskAttachedToScannerVM(ctx context.Context, vm *computepb.Instance, disk *computepb.Disk) error { + var diskAttached bool + for _, attachedDisk := range vm.Disks { + diskName := getLastURLPart(attachedDisk.Source) + if diskName == *disk.Name { + diskAttached = true + break + } + } + + if !diskAttached { + req := &computepb.AttachDiskInstanceRequest{ + AttachedDiskResource: &computepb.AttachedDisk{Source: utils.PointerTo(disk.GetSelfLink())}, + Instance: *vm.Name, + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + } + + _, err := c.instancesClient.AttachDisk(ctx, req) + if err != nil { + _, err = handleGcpRequestError(err, "attach disk %v to VM %v", *disk.Name, *vm.Name) + return err + } + } + + diskResp, err := c.disksClient.Get(ctx, &computepb.GetDiskRequest{ + Disk: *disk.Name, + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + }) + if err != nil { + _, err = handleGcpRequestError(err, "get disk %v", *disk.Name) + return err + } + + if *diskResp.Status != ProvisioningStateReady { + return provider.RetryableErrorf(VMDiskAttachEstimateTime, "disk is not yet attached, status: %v", *disk.Status) + } + + return nil +} diff --git a/runtime_scan/pkg/provider/gcp/snapshot.go b/runtime_scan/pkg/provider/gcp/snapshot.go new file mode 100644 index 000000000..1301d6a6d --- /dev/null +++ b/runtime_scan/pkg/provider/gcp/snapshot.go @@ -0,0 +1,97 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed 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 gcp + +import ( + "context" + "fmt" + "time" + + "cloud.google.com/go/compute/apiv1/computepb" + + "github.com/openclarity/vmclarity/runtime_scan/pkg/provider" +) + +var ( + SnapshotCreateEstimateProvisionTime = 2 * time.Minute + SnapshotDeleteEstimateTime = 2 * time.Minute +) + +func snapshotNameFromJobConfig(config *provider.ScanJobConfig) string { + return fmt.Sprintf("snapshot-%s", config.AssetScanID) +} + +func (c *Client) ensureSnapshotFromAttachedDisk(ctx context.Context, config *provider.ScanJobConfig, disk *computepb.AttachedDisk) (*computepb.Snapshot, error) { + snapshotName := snapshotNameFromJobConfig(config) + + snapshotRes, err := c.snapshotsClient.Get(ctx, &computepb.GetSnapshotRequest{ + Project: c.gcpConfig.ProjectID, + Snapshot: snapshotName, + }) + if err == nil { + if *snapshotRes.Status != ProvisioningStateReady { + return snapshotRes, provider.RetryableErrorf(SnapshotCreateEstimateProvisionTime, "snapshot is not ready yet. status: %v", *snapshotRes.Status) + } + + // Everything is good, the snapshot exists and is provisioned successfully + return snapshotRes, nil + } + + notFound, err := handleGcpRequestError(err, "getting snapshot %s", snapshotName) + if !notFound { + return nil, err + } + + // Snapshot not found, Create the snapshot + req := &computepb.InsertSnapshotRequest{ + Project: c.gcpConfig.ProjectID, + SnapshotResource: &computepb.Snapshot{ + Name: &snapshotName, + SourceDisk: disk.Source, + }, + } + + _, err = c.snapshotsClient.Insert(ctx, req) + if err != nil { + _, err := handleGcpRequestError(err, "create snapshot %s", snapshotName) + return nil, err + } + + return &computepb.Snapshot{}, provider.RetryableErrorf(SnapshotCreateEstimateProvisionTime, "snapshot creating") +} + +func (c *Client) ensureSnapshotDeleted(ctx context.Context, config *provider.ScanJobConfig) error { + snapshotName := snapshotNameFromJobConfig(config) + + return ensureDeleted( + "snapshot", + func() error { + _, err := c.snapshotsClient.Get(ctx, &computepb.GetSnapshotRequest{ + Project: c.gcpConfig.ProjectID, + Snapshot: snapshotName, + }) + return err // nolint: wrapcheck + }, + func() error { + _, err := c.snapshotsClient.Delete(ctx, &computepb.DeleteSnapshotRequest{ + Project: c.gcpConfig.ProjectID, + Snapshot: snapshotName, + }) + return err // nolint: wrapcheck + }, + SnapshotDeleteEstimateTime, + ) +} diff --git a/runtime_scan/pkg/provider/gcp/targetDisk.go b/runtime_scan/pkg/provider/gcp/targetDisk.go new file mode 100644 index 000000000..b04f351e8 --- /dev/null +++ b/runtime_scan/pkg/provider/gcp/targetDisk.go @@ -0,0 +1,105 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed 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 gcp + +import ( + "context" + "fmt" + "time" + + "cloud.google.com/go/compute/apiv1/computepb" + + "github.com/openclarity/vmclarity/runtime_scan/pkg/provider" + "github.com/openclarity/vmclarity/shared/pkg/utils" +) + +var ( + DiskEstimateProvisionTime = 2 * time.Minute + DiskDeleteEstimateTime = 2 * time.Minute +) + +func diskNameFromJobConfig(config *provider.ScanJobConfig) string { + return fmt.Sprintf("targetvolume-%s", config.AssetScanID) +} + +func (c *Client) ensureDiskFromSnapshot(ctx context.Context, config *provider.ScanJobConfig, snapshot *computepb.Snapshot) (*computepb.Disk, error) { + diskName := diskNameFromJobConfig(config) + + diskRes, err := c.disksClient.Get(ctx, &computepb.GetDiskRequest{ + Disk: diskName, + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + }) + if err == nil { + if *diskRes.Status != ProvisioningStateReady { + return diskRes, provider.RetryableErrorf(DiskEstimateProvisionTime, "disk is not ready yet. status: %v", *diskRes.Status) + } + + // Everything is good, the disk exists and is provisioned successfully + return diskRes, nil + } + + notFound, err := handleGcpRequestError(err, "getting disk %s", diskName) + if !notFound { + return nil, err + } + + // create the disk if not exists + req := &computepb.InsertDiskRequest{ + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + DiskResource: &computepb.Disk{ + Name: &diskName, + // Use pd-balanced so that we have SSD not spinning HDD + Type: utils.PointerTo(fmt.Sprintf("zones/%v/diskTypes/pd-balanced", c.gcpConfig.ScannerZone)), + SourceSnapshot: utils.PointerTo(snapshot.GetSelfLink()), + SizeGb: snapshot.DiskSizeGb, // specify the size of the source disk (target scan) + }, + } + + _, err = c.disksClient.Insert(ctx, req) + if err != nil { + _, err := handleGcpRequestError(err, "create disk") + return nil, err + } + + return nil, provider.RetryableErrorf(DiskEstimateProvisionTime, "disk creating") +} + +func (c *Client) ensureTargetDiskDeleted(ctx context.Context, config *provider.ScanJobConfig) error { + diskName := diskNameFromJobConfig(config) + + return ensureDeleted( + "disk", + func() error { + _, err := c.disksClient.Get(ctx, &computepb.GetDiskRequest{ + Disk: diskName, + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + }) + return err // nolint: wrapcheck + }, + func() error { + _, err := c.disksClient.Delete(ctx, &computepb.DeleteDiskRequest{ + Disk: diskName, + Project: c.gcpConfig.ProjectID, + Zone: c.gcpConfig.ScannerZone, + }) + return err // nolint: wrapcheck + }, + DiskDeleteEstimateTime, + ) +} diff --git a/runtime_scan/pkg/provider/gcp/types.go b/runtime_scan/pkg/provider/gcp/types.go new file mode 100644 index 000000000..53710726e --- /dev/null +++ b/runtime_scan/pkg/provider/gcp/types.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed 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 gcp + +const ( + maxResults = 500 +) + +const ( + ProvisioningStateReady = "READY" + InstanceStateRunning = "RUNNING" +) diff --git a/ui_backend/api/models/models.gen.go b/ui_backend/api/models/models.gen.go index 422b4e900..04c9f9ca4 100644 --- a/ui_backend/api/models/models.gen.go +++ b/ui_backend/api/models/models.gen.go @@ -11,6 +11,7 @@ import ( const ( AWSEC2Instance AssetType = "AWS EC2 Instance" AzureInstance AssetType = "Azure Instance" + GCPInstance AssetType = "GCP Instance" ) // Defines values for FindingType. diff --git a/ui_backend/api/openapi.yaml b/ui_backend/api/openapi.yaml index fa69840f8..8ee774dd9 100644 --- a/ui_backend/api/openapi.yaml +++ b/ui_backend/api/openapi.yaml @@ -493,6 +493,7 @@ components: enum: - 'AWS EC2 Instance' - 'Azure Instance' + - 'GCP Instance' responses: UnknownError: diff --git a/ui_backend/api/server/server.gen.go b/ui_backend/api/server/server.gen.go index 0f82d6c4c..1070f6af1 100644 --- a/ui_backend/api/server/server.gen.go +++ b/ui_backend/api/server/server.gen.go @@ -136,32 +136,32 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/ui_backend/pkg/rest/dashboard_riskiest_assets.go b/ui_backend/pkg/rest/dashboard_riskiest_assets.go index 678b14db8..eb9ae0267 100644 --- a/ui_backend/pkg/rest/dashboard_riskiest_assets.go +++ b/ui_backend/pkg/rest/dashboard_riskiest_assets.go @@ -238,6 +238,8 @@ func getAssetType(provider *backendmodels.CloudProvider) (*models.AssetType, err return utils.PointerTo(models.AWSEC2Instance), nil case backendmodels.Azure: return utils.PointerTo(models.AzureInstance), nil + case backendmodels.GCP: + return utils.PointerTo(models.GCPInstance), nil default: return nil, fmt.Errorf("unsupported provider: %v", *provider) }